refactor(brain): structured pocket output — 3 lines + 3 arcs per pocket
Replaced Shapely buffer-based fillet (59-pt polylines) with exact geometric fillet computation. Each pocket now outputs: - 3 straight edges (line start/end pairs) - 3 fillet arcs (center, radius, tangent points, angles) NX import updated to use SketchLineBuilder + SketchArcBuilder (3-point). Total NX entities: ~2,600 (was ~13,000). Includes arc fallback to 2-line segments if SketchArcBuilder fails. Also outputs circular hole definitions for future NX circle creation.
This commit is contained in:
@@ -89,8 +89,10 @@ def _plot_final_profile(geometry, pockets, ribbed_plate, out_path: Path) -> None
|
|||||||
ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], "k-", lw=1.8, label="Outer boundary")
|
ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], "k-", lw=1.8, label="Outer boundary")
|
||||||
|
|
||||||
for pocket in pockets:
|
for pocket in pockets:
|
||||||
pv = np.asarray(pocket["vertices"])
|
polyline = pocket.get("polyline", pocket.get("vertices", []))
|
||||||
ax.fill(pv[:, 0], pv[:, 1], color="#88ccee", alpha=0.35, lw=0.0)
|
pv = np.asarray(polyline)
|
||||||
|
if len(pv) >= 3:
|
||||||
|
ax.fill(pv[:, 0], pv[:, 1], color="#88ccee", alpha=0.35, lw=0.0)
|
||||||
|
|
||||||
if ribbed_plate.geom_type == "Polygon":
|
if ribbed_plate.geom_type == "Polygon":
|
||||||
geoms = [ribbed_plate]
|
geoms = [ribbed_plate]
|
||||||
@@ -122,7 +124,7 @@ def run_pipeline(geometry_path: Path, params_path: Path | None, output_dir: Path
|
|||||||
ribbed_plate = assemble_profile(geometry, pockets, params)
|
ribbed_plate = assemble_profile(geometry, pockets, params)
|
||||||
is_valid, checks = validate_profile(ribbed_plate, params)
|
is_valid, checks = validate_profile(ribbed_plate, params)
|
||||||
|
|
||||||
profile_json = profile_to_json(ribbed_plate, params)
|
profile_json = profile_to_json(ribbed_plate, pockets, geometry, params)
|
||||||
profile_json["checks"] = checks
|
profile_json["checks"] = checks
|
||||||
profile_json["pipeline"] = {
|
profile_json["pipeline"] = {
|
||||||
"geometry_file": str(geometry_path),
|
"geometry_file": str(geometry_path),
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Pocket profile generation — convert triangulation into manufacturable pocket cutouts.
|
Pocket profile generation — convert triangulation into manufacturable pocket cutouts.
|
||||||
|
|
||||||
Each triangle → inset by half-rib-thickness per edge → fillet corners → pocket polygon.
|
Each triangle → inset by half-rib-thickness per edge → fillet corners → pocket.
|
||||||
|
|
||||||
|
Output is **structured geometry**: 3 straight edges + 3 fillet arcs per pocket,
|
||||||
|
NOT dense polylines. This keeps NX sketch imports lightweight (~6 entities per
|
||||||
|
pocket instead of ~60 line segments).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from shapely.geometry import Polygon
|
from typing import List, Dict, Optional, Tuple
|
||||||
from shapely.ops import unary_union
|
|
||||||
|
|
||||||
from .density_field import evaluate_density, density_to_rib_thickness
|
from .density_field import evaluate_density, density_to_rib_thickness
|
||||||
|
|
||||||
@@ -21,9 +24,43 @@ def get_unique_edges(triangles):
|
|||||||
return edges
|
return edges
|
||||||
|
|
||||||
|
|
||||||
def get_triangle_edge_thicknesses(tri_vertices, tri_edges_thickness):
|
def triangle_area(p0, p1, p2):
|
||||||
"""Get thickness for each edge of a triangle."""
|
"""Signed area of triangle."""
|
||||||
return [tri_edges_thickness.get(e, 2.0) for e in tri_vertices]
|
return 0.5 * abs(
|
||||||
|
(p1[0] - p0[0]) * (p2[1] - p0[1]) -
|
||||||
|
(p2[0] - p0[0]) * (p1[1] - p0[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _edge_normal_inward(a, b, opposite):
|
||||||
|
"""Unit inward normal of edge a→b (toward opposite vertex)."""
|
||||||
|
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||||
|
length = np.sqrt(dx**2 + dy**2)
|
||||||
|
if length < 1e-10:
|
||||||
|
return np.array([0.0, 0.0])
|
||||||
|
n1 = np.array([-dy / length, dx / length])
|
||||||
|
n2 = np.array([dy / length, -dx / length])
|
||||||
|
mid = (np.array(a) + np.array(b)) / 2.0
|
||||||
|
if np.dot(n1, np.array(opposite) - mid) > 0:
|
||||||
|
return n1
|
||||||
|
return n2
|
||||||
|
|
||||||
|
|
||||||
|
def _line_intersection(a1, b1, a2, b2):
|
||||||
|
"""Find intersection of two lines (a1→b1) and (a2→b2)."""
|
||||||
|
d1 = b1 - a1
|
||||||
|
d2 = b2 - a2
|
||||||
|
cross = d1[0] * d2[1] - d1[1] * d2[0]
|
||||||
|
if abs(cross) < 1e-12:
|
||||||
|
return None
|
||||||
|
t = ((a2[0] - a1[0]) * d2[1] - (a2[1] - a1[1]) * d2[0]) / cross
|
||||||
|
return a1 + t * d1
|
||||||
|
|
||||||
|
|
||||||
|
def _unit(v):
|
||||||
|
"""Normalize vector."""
|
||||||
|
n = np.linalg.norm(v)
|
||||||
|
return v / n if n > 1e-12 else v
|
||||||
|
|
||||||
|
|
||||||
def inset_triangle(p0, p1, p2, d0, d1, d2):
|
def inset_triangle(p0, p1, p2, d0, d1, d2):
|
||||||
@@ -34,83 +71,212 @@ def inset_triangle(p0, p1, p2, d0, d1, d2):
|
|||||||
Edge 1: p1→p2, inset by d1
|
Edge 1: p1→p2, inset by d1
|
||||||
Edge 2: p2→p0, inset by d2
|
Edge 2: p2→p0, inset by d2
|
||||||
|
|
||||||
Returns inset triangle vertices or None if triangle collapses.
|
Returns inset triangle vertices [v0, v1, v2] or None if collapsed.
|
||||||
"""
|
"""
|
||||||
def edge_normal_inward(a, b, opposite):
|
points = [np.array(p0, dtype=float), np.array(p1, dtype=float), np.array(p2, dtype=float)]
|
||||||
"""Unit inward normal of edge a→b (toward opposite vertex)."""
|
|
||||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
|
||||||
length = np.sqrt(dx**2 + dy**2)
|
|
||||||
if length < 1e-10:
|
|
||||||
return np.array([0.0, 0.0])
|
|
||||||
# Two possible normals
|
|
||||||
n1 = np.array([-dy / length, dx / length])
|
|
||||||
n2 = np.array([dy / length, -dx / length])
|
|
||||||
# Pick the one pointing toward the opposite vertex
|
|
||||||
mid = (np.array(a) + np.array(b)) / 2.0
|
|
||||||
if np.dot(n1, np.array(opposite) - mid) > 0:
|
|
||||||
return n1
|
|
||||||
return n2
|
|
||||||
|
|
||||||
points = [np.array(p0), np.array(p1), np.array(p2)]
|
|
||||||
offsets = [d0, d1, d2]
|
offsets = [d0, d1, d2]
|
||||||
|
|
||||||
# Compute inset lines for each edge
|
|
||||||
inset_lines = []
|
|
||||||
edges = [(0, 1, 2), (1, 2, 0), (2, 0, 1)]
|
edges = [(0, 1, 2), (1, 2, 0), (2, 0, 1)]
|
||||||
for (i, j, k), d in zip(edges, offsets):
|
|
||||||
n = edge_normal_inward(points[i], points[j], points[k])
|
|
||||||
# Offset edge inward
|
|
||||||
a_off = points[i] + n * d
|
|
||||||
b_off = points[j] + n * d
|
|
||||||
inset_lines.append((a_off, b_off))
|
|
||||||
|
|
||||||
# Intersect consecutive inset lines to get new vertices
|
inset_lines = []
|
||||||
|
for (i, j, k), d in zip(edges, offsets):
|
||||||
|
n = _edge_normal_inward(points[i], points[j], points[k])
|
||||||
|
inset_lines.append((points[i] + n * d, points[j] + n * d))
|
||||||
|
|
||||||
new_verts = []
|
new_verts = []
|
||||||
for idx in range(3):
|
for idx in range(3):
|
||||||
a1, b1 = inset_lines[idx]
|
a1, b1 = inset_lines[idx]
|
||||||
a2, b2 = inset_lines[(idx + 1) % 3]
|
a2, b2 = inset_lines[(idx + 1) % 3]
|
||||||
pt = line_intersection(a1, b1, a2, b2)
|
pt = _line_intersection(a1, b1, a2, b2)
|
||||||
if pt is None:
|
if pt is None:
|
||||||
return None
|
return None
|
||||||
new_verts.append(pt)
|
new_verts.append(pt)
|
||||||
|
|
||||||
# Check if triangle is valid (positive area)
|
if triangle_area(new_verts[0], new_verts[1], new_verts[2]) < 0.1:
|
||||||
area = triangle_area(new_verts[0], new_verts[1], new_verts[2])
|
|
||||||
if area < 0.1: # mm² — too small
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return new_verts
|
return new_verts
|
||||||
|
|
||||||
|
|
||||||
def line_intersection(a1, b1, a2, b2):
|
def _fillet_corner(
|
||||||
"""Find intersection of two lines (a1→b1) and (a2→b2)."""
|
v_prev: np.ndarray,
|
||||||
d1 = b1 - a1
|
v_corner: np.ndarray,
|
||||||
d2 = b2 - a2
|
v_next: np.ndarray,
|
||||||
cross = d1[0] * d2[1] - d1[1] * d2[0]
|
r_f: float,
|
||||||
if abs(cross) < 1e-12:
|
) -> Optional[Dict]:
|
||||||
return None # parallel
|
"""
|
||||||
t = ((a2[0] - a1[0]) * d2[1] - (a2[1] - a1[1]) * d2[0]) / cross
|
Compute a fillet arc at v_corner between edges v_prev→v_corner and
|
||||||
return a1 + t * d1
|
v_corner→v_next.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
tangent_start: point where arc begins (on edge prev→corner)
|
||||||
|
tangent_end: point where arc ends (on edge corner→next)
|
||||||
|
center: arc center
|
||||||
|
radius: actual fillet radius used
|
||||||
|
start_angle: angle of tangent_start from center (radians)
|
||||||
|
end_angle: angle of tangent_end from center (radians)
|
||||||
|
or None if fillet can't be applied.
|
||||||
|
"""
|
||||||
|
d1 = _unit(v_prev - v_corner)
|
||||||
|
d2 = _unit(v_next - v_corner)
|
||||||
|
|
||||||
def triangle_area(p0, p1, p2):
|
# Half-angle between the two edges
|
||||||
"""Signed area of triangle."""
|
cos_half = np.dot(d1, d2)
|
||||||
return 0.5 * abs(
|
cos_half = np.clip(cos_half, -1.0, 1.0)
|
||||||
(p1[0] - p0[0]) * (p2[1] - p0[1]) -
|
half_angle = np.arccos(cos_half) / 2.0
|
||||||
(p2[0] - p0[0]) * (p1[1] - p0[1])
|
|
||||||
|
if half_angle < 1e-6 or half_angle > np.pi / 2 - 1e-6:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Distance from corner to tangent point
|
||||||
|
tan_dist = r_f / np.tan(half_angle)
|
||||||
|
|
||||||
|
# Check we don't exceed half the edge length
|
||||||
|
max_dist = min(
|
||||||
|
np.linalg.norm(v_prev - v_corner) * 0.45,
|
||||||
|
np.linalg.norm(v_next - v_corner) * 0.45,
|
||||||
)
|
)
|
||||||
|
if tan_dist > max_dist:
|
||||||
|
# Reduce fillet radius to fit
|
||||||
|
r_f = max_dist * np.tan(half_angle)
|
||||||
|
tan_dist = max_dist
|
||||||
|
if r_f < 0.05:
|
||||||
|
return None
|
||||||
|
|
||||||
|
tangent_start = v_corner + d1 * tan_dist
|
||||||
|
tangent_end = v_corner + d2 * tan_dist
|
||||||
|
|
||||||
|
# Arc center: move from corner along bisector
|
||||||
|
bisector = _unit(d1 + d2)
|
||||||
|
center_dist = r_f / np.sin(half_angle)
|
||||||
|
center = v_corner + bisector * center_dist
|
||||||
|
|
||||||
|
# Angles
|
||||||
|
start_angle = float(np.arctan2(tangent_start[1] - center[1], tangent_start[0] - center[0]))
|
||||||
|
end_angle = float(np.arctan2(tangent_end[1] - center[1], tangent_end[0] - center[0]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tangent_start': tangent_start.tolist(),
|
||||||
|
'tangent_end': tangent_end.tolist(),
|
||||||
|
'center': center.tolist(),
|
||||||
|
'radius': float(r_f),
|
||||||
|
'start_angle': start_angle,
|
||||||
|
'end_angle': end_angle,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_filleted_pocket(inset_verts: List[np.ndarray], r_f: float) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Compute structured pocket geometry: 3 lines + up to 3 arcs.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
lines: list of [start, end] pairs (the straight rib edges)
|
||||||
|
arcs: list of arc dicts (fillet arcs at corners)
|
||||||
|
vertices: inset triangle vertices (for reference)
|
||||||
|
"""
|
||||||
|
n = len(inset_verts) # always 3 for triangles
|
||||||
|
fillets = []
|
||||||
|
for i in range(n):
|
||||||
|
v_prev = inset_verts[(i - 1) % n]
|
||||||
|
v_corner = inset_verts[i]
|
||||||
|
v_next = inset_verts[(i + 1) % n]
|
||||||
|
fillet = _fillet_corner(v_prev, v_corner, v_next, r_f)
|
||||||
|
fillets.append(fillet)
|
||||||
|
|
||||||
|
# Build lines connecting fillet tangent points
|
||||||
|
lines = []
|
||||||
|
for i in range(n):
|
||||||
|
# Line from end of fillet at vertex i to start of fillet at vertex i+1
|
||||||
|
if fillets[i] is not None:
|
||||||
|
line_start = fillets[i]['tangent_end']
|
||||||
|
else:
|
||||||
|
line_start = inset_verts[i].tolist()
|
||||||
|
|
||||||
|
next_i = (i + 1) % n
|
||||||
|
if fillets[next_i] is not None:
|
||||||
|
line_end = fillets[next_i]['tangent_start']
|
||||||
|
else:
|
||||||
|
line_end = inset_verts[next_i].tolist()
|
||||||
|
|
||||||
|
# Skip degenerate lines
|
||||||
|
d = np.sqrt((line_start[0] - line_end[0])**2 + (line_start[1] - line_end[1])**2)
|
||||||
|
if d > 0.01:
|
||||||
|
lines.append([line_start, line_end])
|
||||||
|
|
||||||
|
arcs = [f for f in fillets if f is not None]
|
||||||
|
|
||||||
|
if len(lines) == 0 and len(arcs) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'lines': lines,
|
||||||
|
'arcs': arcs,
|
||||||
|
'vertices': [v.tolist() for v in inset_verts],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _pocket_to_polyline(pocket_geom: Dict, arc_segments: int = 8) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Convert structured pocket geometry to a simple polyline (for Shapely
|
||||||
|
operations like area calculation and visualization).
|
||||||
|
Uses a small number of arc segments since this is only for validation.
|
||||||
|
"""
|
||||||
|
# Build ordered point list: line, arc, line, arc, ...
|
||||||
|
# The pocket geometry alternates: fillet arc at vertex i, then line to vertex i+1
|
||||||
|
pts = []
|
||||||
|
n_lines = len(pocket_geom['lines'])
|
||||||
|
n_arcs = len(pocket_geom['arcs'])
|
||||||
|
|
||||||
|
if n_arcs == 0:
|
||||||
|
# No fillets — just straight triangle
|
||||||
|
for line in pocket_geom['lines']:
|
||||||
|
pts.append(line[0])
|
||||||
|
if pocket_geom['lines']:
|
||||||
|
pts.append(pocket_geom['lines'][0][0]) # close
|
||||||
|
return pts
|
||||||
|
|
||||||
|
# Interleave: for each vertex, output arc then line
|
||||||
|
arc_idx = 0
|
||||||
|
for i in range(3):
|
||||||
|
# Find if this vertex has a fillet
|
||||||
|
# Arcs are stored in order of vertices that had fillets
|
||||||
|
if arc_idx < n_arcs:
|
||||||
|
arc = pocket_geom['arcs'][arc_idx]
|
||||||
|
# Sample arc
|
||||||
|
sa = arc['start_angle']
|
||||||
|
ea = arc['end_angle']
|
||||||
|
# Ensure we go the short way around
|
||||||
|
da = ea - sa
|
||||||
|
if da > np.pi:
|
||||||
|
da -= 2 * np.pi
|
||||||
|
elif da < -np.pi:
|
||||||
|
da += 2 * np.pi
|
||||||
|
for j in range(arc_segments + 1):
|
||||||
|
t = sa + da * j / arc_segments
|
||||||
|
px = arc['center'][0] + arc['radius'] * np.cos(t)
|
||||||
|
py = arc['center'][1] + arc['radius'] * np.sin(t)
|
||||||
|
pts.append([px, py])
|
||||||
|
arc_idx += 1
|
||||||
|
|
||||||
|
# Line to next vertex
|
||||||
|
if i < n_lines:
|
||||||
|
pts.append(pocket_geom['lines'][i][1])
|
||||||
|
|
||||||
|
# Close
|
||||||
|
if pts:
|
||||||
|
pts.append(pts[0])
|
||||||
|
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
def generate_pockets(triangulation, geometry, params):
|
def generate_pockets(triangulation, geometry, params):
|
||||||
"""
|
"""
|
||||||
Generate pocket profiles from triangulation.
|
Generate pocket profiles from triangulation.
|
||||||
|
|
||||||
Each triangle becomes a pocket (inset + filleted).
|
Each triangle becomes a pocket: inset by half-rib-thickness, filleted corners.
|
||||||
Small triangles are left solid (no pocket).
|
Output is structured geometry (lines + arcs), not dense polylines.
|
||||||
|
|
||||||
Returns
|
Returns list[dict] with keys:
|
||||||
-------
|
triangle_index, lines, arcs, vertices, area, polyline (for viz)
|
||||||
list[dict] : pocket definitions with vertices and metadata.
|
|
||||||
"""
|
"""
|
||||||
vertices = triangulation['vertices']
|
vertices = triangulation['vertices']
|
||||||
triangles = triangulation['triangles']
|
triangles = triangulation['triangles']
|
||||||
@@ -134,12 +300,10 @@ def generate_pockets(triangulation, geometry, params):
|
|||||||
p1 = vertices[tri[1]]
|
p1 = vertices[tri[1]]
|
||||||
p2 = vertices[tri[2]]
|
p2 = vertices[tri[2]]
|
||||||
|
|
||||||
# Leave tiny triangles solid to avoid unpocketable slivers
|
|
||||||
tri_area = triangle_area(p0, p1, p2)
|
tri_area = triangle_area(p0, p1, p2)
|
||||||
if tri_area < min_triangle_area:
|
if tri_area < min_triangle_area:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get edge thicknesses (half for inset)
|
|
||||||
e01 = tuple(sorted([tri[0], tri[1]]))
|
e01 = tuple(sorted([tri[0], tri[1]]))
|
||||||
e12 = tuple(sorted([tri[1], tri[2]]))
|
e12 = tuple(sorted([tri[1], tri[2]]))
|
||||||
e20 = tuple(sorted([tri[2], tri[0]]))
|
e20 = tuple(sorted([tri[2], tri[0]]))
|
||||||
@@ -152,31 +316,36 @@ def generate_pockets(triangulation, geometry, params):
|
|||||||
if inset is None:
|
if inset is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check minimum pocket size via inscribed circle approximation
|
# Check minimum pocket size via inscribed circle
|
||||||
pocket_poly = Polygon(inset)
|
inset_area = triangle_area(inset[0], inset[1], inset[2])
|
||||||
if not pocket_poly.is_valid or pocket_poly.is_empty:
|
perimeter = sum(
|
||||||
|
np.linalg.norm(inset[(i+1)%3] - inset[i]) for i in range(3)
|
||||||
|
)
|
||||||
|
if perimeter < 1e-6:
|
||||||
continue
|
continue
|
||||||
|
inscribed_r = inset_area / (perimeter / 2.0)
|
||||||
# Approximate inscribed radius
|
|
||||||
inscribed_r = pocket_poly.area / (pocket_poly.length / 2.0)
|
|
||||||
if inscribed_r < min_pocket_radius:
|
if inscribed_r < min_pocket_radius:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Apply fillet (buffer negative then positive = round inward corners)
|
# Compute fillet amount (don't over-fillet)
|
||||||
fillet_amount = min(r_f, inscribed_r * 0.4) # don't over-fillet
|
fillet_amount = min(r_f, inscribed_r * 0.4)
|
||||||
if fillet_amount > 0.1:
|
if fillet_amount < 0.1:
|
||||||
filleted = pocket_poly.buffer(-fillet_amount).buffer(fillet_amount)
|
fillet_amount = 0.0
|
||||||
else:
|
|
||||||
filleted = pocket_poly
|
|
||||||
|
|
||||||
if filleted.is_empty or not filleted.is_valid:
|
pocket_geom = _compute_filleted_pocket(inset, fillet_amount)
|
||||||
|
if pocket_geom is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
coords = list(filleted.exterior.coords)
|
# Generate a low-res polyline for area calc and visualization
|
||||||
|
polyline = _pocket_to_polyline(pocket_geom, arc_segments=6)
|
||||||
|
|
||||||
pockets.append({
|
pockets.append({
|
||||||
'triangle_index': tri_idx,
|
'triangle_index': tri_idx,
|
||||||
'vertices': coords,
|
'lines': pocket_geom['lines'],
|
||||||
'area': filleted.area,
|
'arcs': pocket_geom['arcs'],
|
||||||
|
'vertices': pocket_geom['vertices'],
|
||||||
|
'polyline': polyline,
|
||||||
|
'area': inset_area, # approximate
|
||||||
})
|
})
|
||||||
|
|
||||||
return pockets
|
return pockets
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Profile assembly — combine plate boundary, pockets, and holes
|
Profile assembly — combine plate boundary, pockets, and holes
|
||||||
into the final 2D ribbed plate profile for NX import.
|
into the final 2D ribbed plate profile for NX import.
|
||||||
|
|
||||||
Output: plate boundary - pockets - holes = ribbed plate (Shapely geometry)
|
Output format: structured geometry with lines + arcs (not dense polylines).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -27,89 +27,89 @@ def hole_to_actual_polygon(hole):
|
|||||||
|
|
||||||
def assemble_profile(geometry, pockets, params):
|
def assemble_profile(geometry, pockets, params):
|
||||||
"""
|
"""
|
||||||
Create the final 2D ribbed plate profile.
|
Create the final 2D ribbed plate profile using Shapely for
|
||||||
|
boolean operations and validation.
|
||||||
|
|
||||||
Plate boundary - pockets - holes = ribbed plate
|
Returns Shapely Polygon/MultiPolygon for visualization and validation.
|
||||||
|
The actual NX import uses the structured pocket geometry directly.
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
geometry : dict
|
|
||||||
Plate geometry with outer_boundary and holes.
|
|
||||||
pockets : list[dict]
|
|
||||||
Pocket definitions from pocket_profiles.generate_pockets().
|
|
||||||
params : dict
|
|
||||||
Must contain w_frame (perimeter frame width).
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Shapely Polygon/MultiPolygon : the ribbed plate profile.
|
|
||||||
"""
|
"""
|
||||||
plate = Polygon(geometry['outer_boundary'])
|
plate = Polygon(geometry['outer_boundary'])
|
||||||
w_frame = params['w_frame']
|
w_frame = params['w_frame']
|
||||||
|
|
||||||
# Inner boundary (frame inset)
|
|
||||||
if w_frame > 0:
|
if w_frame > 0:
|
||||||
inner_plate = plate.buffer(-w_frame)
|
inner_plate = plate.buffer(-w_frame)
|
||||||
else:
|
else:
|
||||||
inner_plate = plate
|
inner_plate = plate
|
||||||
|
|
||||||
if inner_plate.is_empty:
|
if inner_plate.is_empty:
|
||||||
# Frame too wide for plate — return solid plate minus holes
|
|
||||||
ribbed = plate
|
ribbed = plate
|
||||||
for hole in geometry['holes']:
|
for hole in geometry['holes']:
|
||||||
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
||||||
return ribbed
|
return ribbed
|
||||||
|
|
||||||
# Union all pocket polygons
|
# Build pocket polygons from low-res polylines (for Shapely ops only)
|
||||||
pocket_polys = []
|
pocket_polys = []
|
||||||
for p in pockets:
|
for p in pockets:
|
||||||
poly = Polygon(p['vertices'])
|
polyline = p.get('polyline', [])
|
||||||
if poly.is_valid and not poly.is_empty:
|
if len(polyline) >= 3:
|
||||||
pocket_polys.append(poly)
|
poly = Polygon(polyline)
|
||||||
|
if poly.is_valid and not poly.is_empty:
|
||||||
|
pocket_polys.append(poly)
|
||||||
|
|
||||||
if pocket_polys:
|
if pocket_polys:
|
||||||
all_pockets = unary_union(pocket_polys)
|
all_pockets = unary_union(pocket_polys)
|
||||||
# Clip pockets to inner plate (don't cut into frame)
|
|
||||||
clipped_pockets = all_pockets.intersection(inner_plate)
|
clipped_pockets = all_pockets.intersection(inner_plate)
|
||||||
# Subtract pockets from plate
|
|
||||||
ribbed = plate.difference(clipped_pockets)
|
ribbed = plate.difference(clipped_pockets)
|
||||||
else:
|
else:
|
||||||
ribbed = plate
|
ribbed = plate
|
||||||
|
|
||||||
# Subtract actual hole boundaries (not boss keepouts)
|
|
||||||
for hole in geometry['holes']:
|
for hole in geometry['holes']:
|
||||||
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
||||||
|
|
||||||
return ribbed
|
return ribbed
|
||||||
|
|
||||||
|
|
||||||
def profile_to_json(ribbed_plate, params):
|
def profile_to_json(ribbed_plate, pockets, geometry, params):
|
||||||
"""
|
"""
|
||||||
Convert Shapely ribbed plate geometry to JSON-serializable dict
|
Convert to JSON-serializable dict for NX import.
|
||||||
for NXOpen import.
|
|
||||||
|
Output contains:
|
||||||
|
- outer_boundary: plate outline points
|
||||||
|
- pockets: list of structured pocket geometry (lines + arcs)
|
||||||
|
- holes: circular hole definitions
|
||||||
|
- parameters_used: the params dict
|
||||||
"""
|
"""
|
||||||
if ribbed_plate.is_empty:
|
if ribbed_plate.is_empty:
|
||||||
return {'valid': False, 'reason': 'empty_geometry'}
|
return {'valid': False, 'reason': 'empty_geometry'}
|
||||||
|
|
||||||
# Separate exterior and interiors
|
outer = geometry['outer_boundary']
|
||||||
if ribbed_plate.geom_type == 'MultiPolygon':
|
if hasattr(outer, 'tolist'):
|
||||||
# Take the largest polygon (should be the plate)
|
outer = outer.tolist()
|
||||||
largest = max(ribbed_plate.geoms, key=lambda g: g.area)
|
|
||||||
else:
|
else:
|
||||||
largest = ribbed_plate
|
outer = [list(p) for p in outer]
|
||||||
|
|
||||||
# Classify interiors as pockets or holes
|
# Structured pocket data (lines + arcs)
|
||||||
outer_coords = list(largest.exterior.coords)
|
pocket_data = []
|
||||||
pocket_coords = []
|
for p in pockets:
|
||||||
hole_coords = []
|
pocket_data.append({
|
||||||
|
'lines': p['lines'],
|
||||||
|
'arcs': p['arcs'],
|
||||||
|
})
|
||||||
|
|
||||||
for interior in largest.interiors:
|
# Hole data for NX (circles)
|
||||||
coords = list(interior.coords)
|
hole_data = []
|
||||||
pocket_coords.append(coords)
|
for hole in geometry.get('holes', []):
|
||||||
|
if hole.get('is_circular', False) and 'center' in hole and 'diameter' in hole:
|
||||||
|
hole_data.append({
|
||||||
|
'center': list(hole['center']),
|
||||||
|
'radius': float(hole['diameter']) / 2.0,
|
||||||
|
'is_circular': True,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'valid': True,
|
'valid': True,
|
||||||
'outer_boundary': outer_coords,
|
'outer_boundary': outer,
|
||||||
'pockets': pocket_coords,
|
'pockets': pocket_data,
|
||||||
|
'holes': hole_data,
|
||||||
'parameters_used': params,
|
'parameters_used': params,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ NXOpen script — Import rib profile into NX as a sketch.
|
|||||||
Reads `rib_profile_<sandbox_id>.json` (output from Python Brain) and creates
|
Reads `rib_profile_<sandbox_id>.json` (output from Python Brain) and creates
|
||||||
an NX sketch on the sandbox plane containing:
|
an NX sketch on the sandbox plane containing:
|
||||||
- Outer boundary polyline
|
- Outer boundary polyline
|
||||||
- All pocket cutout polylines
|
- Structured pocket geometry (3 lines + 3 arcs per triangular pocket)
|
||||||
|
|
||||||
The sketch is placed in the idealized part. Antoine extrudes manually the first
|
The sketch is placed in the idealized part. Antoine extrudes manually the first
|
||||||
time; subsequent iterations only update the sketch and the extrude regenerates.
|
time; subsequent iterations only update the sketch and the extrude regenerates.
|
||||||
@@ -55,6 +55,22 @@ def unproject_to_3d(
|
|||||||
return pts3d
|
return pts3d
|
||||||
|
|
||||||
|
|
||||||
|
def unproject_point_to_3d(
|
||||||
|
pt2d: List[float],
|
||||||
|
transform: Dict[str, List[float]],
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
|
"""Convert a single 2D point to 3D."""
|
||||||
|
ox, oy, oz = transform["origin"]
|
||||||
|
xx, xy, xz = transform["x_axis"]
|
||||||
|
yx, yy, yz = transform["y_axis"]
|
||||||
|
x, y = pt2d[0], pt2d[1]
|
||||||
|
return (
|
||||||
|
ox + x * xx + y * yx,
|
||||||
|
oy + x * xy + y * yy,
|
||||||
|
oz + x * xz + y * yz,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# NX sketch creation
|
# NX sketch creation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -82,7 +98,6 @@ def _find_or_create_sketch(
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if fname == sketch_name:
|
if fname == sketch_name:
|
||||||
# Get the sketch from the feature
|
|
||||||
try:
|
try:
|
||||||
existing_sketch = feat.GetEntities()[0]
|
existing_sketch = feat.GetEntities()[0]
|
||||||
lister.WriteLine(f"[import] Found existing sketch: {sketch_name}")
|
lister.WriteLine(f"[import] Found existing sketch: {sketch_name}")
|
||||||
@@ -93,7 +108,6 @@ def _find_or_create_sketch(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if existing_sketch is not None:
|
if existing_sketch is not None:
|
||||||
# Clear existing geometry for update
|
|
||||||
try:
|
try:
|
||||||
existing_sketch.Activate(False)
|
existing_sketch.Activate(False)
|
||||||
all_geom = existing_sketch.GetAllGeometry()
|
all_geom = existing_sketch.GetAllGeometry()
|
||||||
@@ -116,31 +130,20 @@ def _find_or_create_sketch(
|
|||||||
normal_vec = NXOpen.Vector3d(normal[0], normal[1], normal[2])
|
normal_vec = NXOpen.Vector3d(normal[0], normal[1], normal[2])
|
||||||
x_vec = NXOpen.Vector3d(x_axis[0], x_axis[1], x_axis[2])
|
x_vec = NXOpen.Vector3d(x_axis[0], x_axis[1], x_axis[2])
|
||||||
|
|
||||||
# Create a Plane (SmartObject) — NOT a DatumPlane (DisplayableObject)
|
# Create a Plane (SmartObject)
|
||||||
# PlaneCollection.CreatePlane(method, alternate, origin, normal, expr, flip, percent, geometry)
|
|
||||||
plane = part.Planes.CreatePlane(
|
plane = part.Planes.CreatePlane(
|
||||||
NXOpen.PlaneTypes.MethodType.Fixed, # Fixed plane
|
NXOpen.PlaneTypes.MethodType.Fixed,
|
||||||
NXOpen.PlaneTypes.AlternateType.One,
|
NXOpen.PlaneTypes.AlternateType.One,
|
||||||
origin_pt,
|
origin_pt,
|
||||||
normal_vec,
|
normal_vec,
|
||||||
"", # expression
|
"",
|
||||||
False, # flip
|
False,
|
||||||
False, # percent
|
False,
|
||||||
[], # geometry refs
|
[],
|
||||||
)
|
)
|
||||||
lister.WriteLine(f"[import] Created plane: {plane} (type={type(plane).__name__})")
|
lister.WriteLine(f"[import] Created plane: {plane} (type={type(plane).__name__})")
|
||||||
|
|
||||||
if plane is None:
|
if plane is None:
|
||||||
# Fallback: try Distance method
|
|
||||||
plane = part.Planes.CreatePlane(
|
|
||||||
NXOpen.PlaneTypes.MethodType.Distance,
|
|
||||||
NXOpen.PlaneTypes.AlternateType.One,
|
|
||||||
origin_pt, normal_vec, "", False, False, [],
|
|
||||||
)
|
|
||||||
lister.WriteLine(f"[import] Fallback plane: {plane}")
|
|
||||||
|
|
||||||
if plane is None:
|
|
||||||
# Fallback 2: CreateFixedTypePlane
|
|
||||||
y_axis_t = transform["y_axis"]
|
y_axis_t = transform["y_axis"]
|
||||||
mtx = NXOpen.Matrix3x3()
|
mtx = NXOpen.Matrix3x3()
|
||||||
mtx.Xx = x_axis[0]; mtx.Xy = x_axis[1]; mtx.Xz = x_axis[2]
|
mtx.Xx = x_axis[0]; mtx.Xy = x_axis[1]; mtx.Xz = x_axis[2]
|
||||||
@@ -149,94 +152,50 @@ def _find_or_create_sketch(
|
|||||||
plane = part.Planes.CreateFixedTypePlane(
|
plane = part.Planes.CreateFixedTypePlane(
|
||||||
origin_pt, mtx, NXOpen.SmartObject.UpdateOption.WithinModeling,
|
origin_pt, mtx, NXOpen.SmartObject.UpdateOption.WithinModeling,
|
||||||
)
|
)
|
||||||
lister.WriteLine(f"[import] Fallback2 plane: {plane}")
|
lister.WriteLine(f"[import] Fallback plane: {plane}")
|
||||||
|
|
||||||
# Create sketch-in-place builder
|
# Build sketch
|
||||||
sketch_builder = part.Sketches.CreateSketchInPlaceBuilder2(NXOpen.Sketch.Null)
|
sketch_builder = part.Sketches.CreateSketchInPlaceBuilder2(NXOpen.Sketch.Null)
|
||||||
|
|
||||||
# NXOpen Python naming collision: property getter and setter method share
|
|
||||||
# the same name. builder.PlaneReference returns None (getter), so calling
|
|
||||||
# builder.PlaneReference(plane) = None(plane) = TypeError.
|
|
||||||
# Solution: use setattr() to bypass the getter and hit the setter.
|
|
||||||
origin_point = part.Points.CreatePoint(origin_pt)
|
origin_point = part.Points.CreatePoint(origin_pt)
|
||||||
axis_dir = part.Directions.CreateDirection(
|
axis_dir = part.Directions.CreateDirection(
|
||||||
origin_pt, x_vec, NXOpen.SmartObject.UpdateOption.WithinModeling,
|
origin_pt, x_vec, NXOpen.SmartObject.UpdateOption.WithinModeling,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try multiple setter patterns
|
# Use setattr to bypass property/method naming collision
|
||||||
for attr, val, label in [
|
for attr, val, label in [
|
||||||
("PlaneReference", plane, "plane"),
|
("PlaneReference", plane, "plane"),
|
||||||
("SketchOrigin", origin_point, "origin"),
|
("SketchOrigin", origin_point, "origin"),
|
||||||
("AxisReference", axis_dir, "axis"),
|
("AxisReference", axis_dir, "axis"),
|
||||||
]:
|
]:
|
||||||
# Pattern 1: setattr (property assignment)
|
|
||||||
try:
|
try:
|
||||||
setattr(sketch_builder, attr, val)
|
setattr(sketch_builder, attr, val)
|
||||||
lister.WriteLine(f"[import] Set {label} via setattr")
|
lister.WriteLine(f"[import] Set {label} via setattr")
|
||||||
continue
|
|
||||||
except Exception as e1:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Pattern 2: find the setter method with Set prefix
|
|
||||||
setter_name = "Set" + attr
|
|
||||||
try:
|
|
||||||
setter = getattr(sketch_builder, setter_name, None)
|
|
||||||
if setter:
|
|
||||||
setter(val)
|
|
||||||
lister.WriteLine(f"[import] Set {label} via {setter_name}()")
|
|
||||||
continue
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
lister.WriteLine(f"[import] WARNING: Could not set {label}")
|
||||||
|
|
||||||
# Pattern 3: direct call (in case NX version supports it)
|
|
||||||
try:
|
|
||||||
getattr(sketch_builder, attr)(val)
|
|
||||||
lister.WriteLine(f"[import] Set {label} via method call")
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
lister.WriteLine(f"[import] WARNING: Could not set {label} ({attr})")
|
|
||||||
|
|
||||||
# Commit — Builder.Commit() returns NXObject
|
|
||||||
sketch_obj = sketch_builder.Commit()
|
sketch_obj = sketch_builder.Commit()
|
||||||
lister.WriteLine(f"[import] Commit returned: {sketch_obj} (type={type(sketch_obj).__name__})")
|
lister.WriteLine(f"[import] Commit returned: {sketch_obj} (type={type(sketch_obj).__name__})")
|
||||||
|
|
||||||
# Also get committed objects for inspection
|
|
||||||
committed = sketch_builder.GetCommittedObjects()
|
committed = sketch_builder.GetCommittedObjects()
|
||||||
lister.WriteLine(f"[import] Committed objects: {len(committed)}")
|
lister.WriteLine(f"[import] Committed objects: {len(committed)}")
|
||||||
for i, obj in enumerate(committed):
|
|
||||||
lister.WriteLine(f"[import] [{i}] {type(obj).__name__}: {obj}")
|
|
||||||
|
|
||||||
sketch_builder.Destroy()
|
sketch_builder.Destroy()
|
||||||
|
|
||||||
# Find the Sketch object from committed objects or return value
|
# Get Sketch object
|
||||||
sketch = None
|
sketch = None
|
||||||
if isinstance(sketch_obj, NXOpen.Sketch):
|
if isinstance(sketch_obj, NXOpen.Sketch):
|
||||||
sketch = sketch_obj
|
sketch = sketch_obj
|
||||||
else:
|
else:
|
||||||
# Search committed objects
|
|
||||||
for obj in committed:
|
for obj in committed:
|
||||||
if isinstance(obj, NXOpen.Sketch):
|
if isinstance(obj, NXOpen.Sketch):
|
||||||
sketch = obj
|
sketch = obj
|
||||||
break
|
break
|
||||||
# Try GetObject
|
|
||||||
if sketch is None:
|
|
||||||
try:
|
|
||||||
sketch = sketch_builder.GetObject()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Last resort: find by name in part sketches
|
|
||||||
if sketch is None:
|
if sketch is None:
|
||||||
for s in part.Sketches:
|
for s in part.Sketches:
|
||||||
sketch = s # take the last one (just created)
|
sketch = s
|
||||||
lister.WriteLine(f"[import] Found sketch by iteration: {sketch}")
|
|
||||||
|
|
||||||
if sketch is None:
|
if sketch is None:
|
||||||
raise RuntimeError("Could not get Sketch object after commit")
|
raise RuntimeError("Could not get Sketch object after commit")
|
||||||
|
|
||||||
# Rename
|
|
||||||
try:
|
try:
|
||||||
sketch.Name = sketch_name
|
sketch.Name = sketch_name
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -246,90 +205,133 @@ def _find_or_create_sketch(
|
|||||||
return sketch
|
return sketch
|
||||||
|
|
||||||
|
|
||||||
def _draw_polylines_batch(
|
def _resolve_enum(paths):
|
||||||
part: Any,
|
"""Try multiple dotted paths to resolve an NXOpen enum value."""
|
||||||
sketch: Any,
|
for path in paths:
|
||||||
polylines_3d: List[List[Tuple[float, float, float]]],
|
try:
|
||||||
lister: Any,
|
parts_p = path.split(".")
|
||||||
close: bool = True,
|
obj = __import__(parts_p[0])
|
||||||
) -> int:
|
for attr in parts_p[1:]:
|
||||||
"""
|
obj = getattr(obj, attr)
|
||||||
Draw multiple closed polylines directly in the active sketch using
|
return obj, path
|
||||||
SketchLineBuilder (creates native sketch geometry, not model curves).
|
except (AttributeError, ImportError):
|
||||||
|
|
||||||
The sketch must be Activate'd before calling this.
|
|
||||||
|
|
||||||
Returns total number of line segments created.
|
|
||||||
"""
|
|
||||||
import NXOpen
|
|
||||||
|
|
||||||
total_lines = 0
|
|
||||||
|
|
||||||
for points_3d in polylines_3d:
|
|
||||||
if len(points_3d) < 2:
|
|
||||||
continue
|
continue
|
||||||
|
return None, None
|
||||||
n = len(points_3d)
|
|
||||||
# Strip closing duplicate
|
|
||||||
if close and n >= 3:
|
|
||||||
d = math.sqrt(sum((a - b) ** 2 for a, b in zip(points_3d[0], points_3d[-1])))
|
|
||||||
if d < 0.001:
|
|
||||||
n = n - 1
|
|
||||||
|
|
||||||
segments = n if close else (n - 1)
|
|
||||||
|
|
||||||
for i in range(segments):
|
|
||||||
p1 = points_3d[i]
|
|
||||||
p2 = points_3d[(i + 1) % n]
|
|
||||||
|
|
||||||
try:
|
|
||||||
lb = part.Sketches.CreateLineBuilder()
|
|
||||||
lb.SetStartPoint(NXOpen.Point3d(p1[0], p1[1], p1[2]))
|
|
||||||
lb.SetEndPoint(NXOpen.Point3d(p2[0], p2[1], p2[2]))
|
|
||||||
lb.Commit()
|
|
||||||
lb.Destroy()
|
|
||||||
total_lines += 1
|
|
||||||
except Exception as exc:
|
|
||||||
if total_lines == 0:
|
|
||||||
lister.WriteLine(f"[import] SketchLineBuilder failed on first line: {exc}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Progress every 50 polylines
|
|
||||||
if (total_lines > 0) and (len(polylines_3d) > 50) and \
|
|
||||||
(polylines_3d.index(points_3d) % 50 == 49):
|
|
||||||
lister.WriteLine(f"[import] ... {total_lines} lines so far")
|
|
||||||
|
|
||||||
return total_lines
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_circle_in_sketch(
|
# ---------------------------------------------------------------------------
|
||||||
part: Any,
|
# Sketch drawing: lines and arcs
|
||||||
sketch: Any,
|
# ---------------------------------------------------------------------------
|
||||||
center_3d: Tuple[float, float, float],
|
|
||||||
radius: float,
|
def _draw_line(part, p1_3d, p2_3d):
|
||||||
normal: List[float],
|
"""Draw a single line in the active sketch. Returns True on success."""
|
||||||
lister: Any,
|
|
||||||
) -> bool:
|
|
||||||
"""Draw a circle in the sketch."""
|
|
||||||
import NXOpen
|
import NXOpen
|
||||||
|
lb = part.Sketches.CreateLineBuilder()
|
||||||
|
lb.SetStartPoint(NXOpen.Point3d(p1_3d[0], p1_3d[1], p1_3d[2]))
|
||||||
|
lb.SetEndPoint(NXOpen.Point3d(p2_3d[0], p2_3d[1], p2_3d[2]))
|
||||||
|
lb.Commit()
|
||||||
|
lb.Destroy()
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
|
||||||
circle_builder = part.Sketches.CreateCircleBuilder()
|
def _draw_arc_3pt(part, start_3d, mid_3d, end_3d):
|
||||||
center_pt = NXOpen.Point3d(center_3d[0], center_3d[1], center_3d[2])
|
"""
|
||||||
circle_builder.SetCenterPoint(center_pt)
|
Draw a 3-point arc in the active sketch.
|
||||||
# Size point = center + radius along x
|
SketchArcBuilder: SetStartPoint, SetEndPoint, SetThirdPoint, Commit.
|
||||||
size_pt = NXOpen.Point3d(
|
"""
|
||||||
center_3d[0] + radius,
|
import NXOpen
|
||||||
center_3d[1],
|
ab = part.Sketches.CreateArcBuilder()
|
||||||
center_3d[2],
|
ab.SetStartPoint(NXOpen.Point3d(start_3d[0], start_3d[1], start_3d[2]))
|
||||||
)
|
ab.SetEndPoint(NXOpen.Point3d(end_3d[0], end_3d[1], end_3d[2]))
|
||||||
circle_builder.SetSizePoint(size_pt)
|
ab.SetThirdPoint(NXOpen.Point3d(mid_3d[0], mid_3d[1], mid_3d[2]))
|
||||||
circle_builder.Commit()
|
ab.Commit()
|
||||||
circle_builder.Destroy()
|
ab.Destroy()
|
||||||
return True
|
return True
|
||||||
except Exception as exc:
|
|
||||||
lister.WriteLine(f"[import] Circle creation failed: {exc}")
|
|
||||||
return False
|
def _arc_midpoint_2d(arc):
|
||||||
|
"""Compute the midpoint of a fillet arc in 2D (for 3-point arc creation)."""
|
||||||
|
cx, cy = arc['center']
|
||||||
|
r = arc['radius']
|
||||||
|
sa = arc['start_angle']
|
||||||
|
ea = arc['end_angle']
|
||||||
|
# Go the short way around
|
||||||
|
da = ea - sa
|
||||||
|
if da > math.pi:
|
||||||
|
da -= 2 * math.pi
|
||||||
|
elif da < -math.pi:
|
||||||
|
da += 2 * math.pi
|
||||||
|
mid_angle = sa + da / 2.0
|
||||||
|
return [cx + r * math.cos(mid_angle), cy + r * math.sin(mid_angle)]
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_outer_boundary(part, outer_2d, transform, lister):
|
||||||
|
"""Draw the outer boundary as a closed polyline."""
|
||||||
|
outer_3d = unproject_to_3d(outer_2d, transform)
|
||||||
|
n = len(outer_3d)
|
||||||
|
if n < 2:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Strip closing duplicate
|
||||||
|
if n >= 3:
|
||||||
|
d = math.sqrt(sum((a - b) ** 2 for a, b in zip(outer_3d[0], outer_3d[-1])))
|
||||||
|
if d < 0.001:
|
||||||
|
n -= 1
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for i in range(n):
|
||||||
|
p1 = outer_3d[i]
|
||||||
|
p2 = outer_3d[(i + 1) % n]
|
||||||
|
try:
|
||||||
|
_draw_line(part, p1, p2)
|
||||||
|
count += 1
|
||||||
|
except Exception as exc:
|
||||||
|
if count == 0:
|
||||||
|
lister.WriteLine(f"[import] ERROR: First line failed: {exc}")
|
||||||
|
return 0
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_structured_pocket(part, pocket, transform, lister):
|
||||||
|
"""
|
||||||
|
Draw a structured pocket (lines + arcs) in the active sketch.
|
||||||
|
Returns (num_lines, num_arcs) created.
|
||||||
|
"""
|
||||||
|
lines_drawn = 0
|
||||||
|
arcs_drawn = 0
|
||||||
|
|
||||||
|
# Draw straight edges
|
||||||
|
for line in pocket.get('lines', []):
|
||||||
|
start_3d = unproject_point_to_3d(line[0], transform)
|
||||||
|
end_3d = unproject_point_to_3d(line[1], transform)
|
||||||
|
try:
|
||||||
|
_draw_line(part, start_3d, end_3d)
|
||||||
|
lines_drawn += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Draw fillet arcs (3-point: start, midpoint, end)
|
||||||
|
for arc in pocket.get('arcs', []):
|
||||||
|
start_3d = unproject_point_to_3d(arc['tangent_start'], transform)
|
||||||
|
end_3d = unproject_point_to_3d(arc['tangent_end'], transform)
|
||||||
|
mid_2d = _arc_midpoint_2d(arc)
|
||||||
|
mid_3d = unproject_point_to_3d(mid_2d, transform)
|
||||||
|
try:
|
||||||
|
_draw_arc_3pt(part, start_3d, mid_3d, end_3d)
|
||||||
|
arcs_drawn += 1
|
||||||
|
except Exception as exc:
|
||||||
|
if arcs_drawn == 0:
|
||||||
|
lister.WriteLine(f"[import] Arc creation failed: {exc}")
|
||||||
|
# Fallback: draw as 2 line segments through midpoint
|
||||||
|
try:
|
||||||
|
_draw_line(part, start_3d, mid_3d)
|
||||||
|
_draw_line(part, mid_3d, end_3d)
|
||||||
|
lines_drawn += 2
|
||||||
|
lister.WriteLine("[import] Arc fallback: 2 lines")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return lines_drawn, arcs_drawn
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -352,7 +354,6 @@ def main():
|
|||||||
part_name = work_part.Name if hasattr(work_part, "Name") else ""
|
part_name = work_part.Name if hasattr(work_part, "Name") else ""
|
||||||
lister.WriteLine(f"[import] Work part: {part_name}")
|
lister.WriteLine(f"[import] Work part: {part_name}")
|
||||||
|
|
||||||
# If not in idealized part, find it
|
|
||||||
if not part_name.endswith("_i"):
|
if not part_name.endswith("_i"):
|
||||||
for part in session.Parts:
|
for part in session.Parts:
|
||||||
pname = part.Name if hasattr(part, "Name") else ""
|
pname = part.Name if hasattr(part, "Name") else ""
|
||||||
@@ -362,7 +363,6 @@ def main():
|
|||||||
lister.WriteLine(f"[import] Switched to idealized part: {pname}")
|
lister.WriteLine(f"[import] Switched to idealized part: {pname}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Find data directory
|
|
||||||
try:
|
try:
|
||||||
part_dir = os.path.dirname(work_part.FullPath)
|
part_dir = os.path.dirname(work_part.FullPath)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -374,22 +374,18 @@ def main():
|
|||||||
lister.WriteLine(f"[import] ERROR: Data directory not found: {data_dir}")
|
lister.WriteLine(f"[import] ERROR: Data directory not found: {data_dir}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find all rib profile + geometry JSON pairs
|
# Find rib profile JSONs
|
||||||
profile_files = sorted([
|
profile_files = sorted([
|
||||||
f for f in os.listdir(data_dir)
|
f for f in os.listdir(data_dir)
|
||||||
if f.startswith("rib_profile_") and f.endswith(".json")
|
if f.startswith("rib_profile_") and f.endswith(".json")
|
||||||
])
|
])
|
||||||
|
|
||||||
if not profile_files:
|
if not profile_files:
|
||||||
# Also check for sandbox1_rib_profile.json pattern
|
|
||||||
profile_files = sorted([
|
profile_files = sorted([
|
||||||
f for f in os.listdir(data_dir)
|
f for f in os.listdir(data_dir)
|
||||||
if "rib_profile" in f and f.endswith(".json")
|
if "rib_profile" in f and f.endswith(".json")
|
||||||
])
|
])
|
||||||
|
|
||||||
if not profile_files:
|
if not profile_files:
|
||||||
lister.WriteLine(f"[import] ERROR: No rib_profile*.json found in {data_dir}")
|
lister.WriteLine(f"[import] ERROR: No rib_profile*.json in {data_dir}")
|
||||||
lister.WriteLine(f"[import] Files present: {os.listdir(data_dir)}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
lister.WriteLine(f"[import] Found {len(profile_files)} profile(s) to import")
|
lister.WriteLine(f"[import] Found {len(profile_files)} profile(s) to import")
|
||||||
@@ -397,21 +393,17 @@ def main():
|
|||||||
for profile_file in profile_files:
|
for profile_file in profile_files:
|
||||||
profile_path = os.path.join(data_dir, profile_file)
|
profile_path = os.path.join(data_dir, profile_file)
|
||||||
|
|
||||||
# Determine sandbox_id from filename
|
|
||||||
# Expected: rib_profile_sandbox_1.json or sandbox1_rib_profile.json
|
|
||||||
sandbox_id = profile_file.replace("rib_profile_", "").replace(".json", "")
|
sandbox_id = profile_file.replace("rib_profile_", "").replace(".json", "")
|
||||||
if not sandbox_id.startswith("sandbox"):
|
if not sandbox_id.startswith("sandbox"):
|
||||||
sandbox_id = "sandbox_1" # fallback
|
sandbox_id = "sandbox_1"
|
||||||
|
|
||||||
# Load corresponding geometry JSON for the transform
|
|
||||||
geom_path = os.path.join(data_dir, f"geometry_{sandbox_id}.json")
|
geom_path = os.path.join(data_dir, f"geometry_{sandbox_id}.json")
|
||||||
if not os.path.exists(geom_path):
|
if not os.path.exists(geom_path):
|
||||||
# Try alternate names
|
|
||||||
candidates = [f for f in os.listdir(data_dir) if f.startswith("geometry_") and f.endswith(".json")]
|
candidates = [f for f in os.listdir(data_dir) if f.startswith("geometry_") and f.endswith(".json")]
|
||||||
if candidates:
|
if candidates:
|
||||||
geom_path = os.path.join(data_dir, candidates[0])
|
geom_path = os.path.join(data_dir, candidates[0])
|
||||||
else:
|
else:
|
||||||
lister.WriteLine(f"[import] ERROR: No geometry JSON found for transform data")
|
lister.WriteLine("[import] ERROR: No geometry JSON for transform")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lister.WriteLine(f"\n--- Importing {profile_file} ---")
|
lister.WriteLine(f"\n--- Importing {profile_file} ---")
|
||||||
@@ -429,7 +421,7 @@ def main():
|
|||||||
transform = geometry["transform"]
|
transform = geometry["transform"]
|
||||||
sketch_name = SKETCH_NAME_PREFIX + sandbox_id
|
sketch_name = SKETCH_NAME_PREFIX + sandbox_id
|
||||||
|
|
||||||
# Find or create sketch
|
# Create sketch
|
||||||
try:
|
try:
|
||||||
sketch = _find_or_create_sketch(work_part, sketch_name, transform, lister)
|
sketch = _find_or_create_sketch(work_part, sketch_name, transform, lister)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -438,86 +430,81 @@ def main():
|
|||||||
lister.WriteLine(traceback.format_exc())
|
lister.WriteLine(traceback.format_exc())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Activate sketch for drawing
|
# Activate sketch
|
||||||
try:
|
try:
|
||||||
# ViewReorient enum — try multiple access paths
|
view_false, path = _resolve_enum([
|
||||||
view_false = None
|
|
||||||
for path in [
|
|
||||||
"NXOpen.ViewReorient.FalseValue",
|
|
||||||
"NXOpen.Sketch.ViewReorient.FalseValue",
|
"NXOpen.Sketch.ViewReorient.FalseValue",
|
||||||
"NXOpen.SketchViewReorient.FalseValue",
|
"NXOpen.ViewReorient.FalseValue",
|
||||||
]:
|
])
|
||||||
try:
|
|
||||||
parts_p = path.split(".")
|
|
||||||
obj = __import__(parts_p[0])
|
|
||||||
for attr in parts_p[1:]:
|
|
||||||
obj = getattr(obj, attr)
|
|
||||||
view_false = obj
|
|
||||||
lister.WriteLine(f"[import] ViewReorient resolved via: {path}")
|
|
||||||
break
|
|
||||||
except (AttributeError, ImportError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if view_false is None:
|
if view_false is None:
|
||||||
# Last resort: pass False as boolean
|
|
||||||
view_false = False
|
view_false = False
|
||||||
lister.WriteLine("[import] ViewReorient: using False (boolean fallback)")
|
lister.WriteLine(f"[import] ViewReorient: {path or 'boolean fallback'}")
|
||||||
|
|
||||||
sketch.Activate(view_false)
|
sketch.Activate(view_false)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
lister.WriteLine(f"[import] ERROR activating sketch: {exc}")
|
lister.WriteLine(f"[import] ERROR activating sketch: {exc}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
total_lines = 0
|
# --- Draw geometry ---
|
||||||
|
|
||||||
# Collect all polylines to draw
|
|
||||||
all_polylines_3d = []
|
|
||||||
|
|
||||||
# Outer boundary
|
|
||||||
outer_2d = profile.get("outer_boundary", [])
|
|
||||||
if outer_2d:
|
|
||||||
outer_3d = unproject_to_3d(outer_2d, transform)
|
|
||||||
all_polylines_3d.append(outer_3d)
|
|
||||||
|
|
||||||
# Pocket cutouts
|
|
||||||
pockets = profile.get("pockets", [])
|
pockets = profile.get("pockets", [])
|
||||||
lister.WriteLine(f"[import] Preparing {len(pockets)} pockets + outer boundary...")
|
outer_2d = profile.get("outer_boundary", [])
|
||||||
|
|
||||||
for pocket_pts in pockets:
|
# Check if pockets are structured (lines+arcs) or legacy (point lists)
|
||||||
if len(pocket_pts) < 3:
|
is_structured = (len(pockets) > 0 and isinstance(pockets[0], dict)
|
||||||
continue
|
and 'lines' in pockets[0])
|
||||||
pocket_3d = unproject_to_3d(pocket_pts, transform)
|
|
||||||
all_polylines_3d.append(pocket_3d)
|
|
||||||
|
|
||||||
# Draw everything in one batch
|
if is_structured:
|
||||||
lister.WriteLine(f"[import] Creating {len(all_polylines_3d)} polylines as NX curves...")
|
lister.WriteLine(f"[import] Structured format: {len(pockets)} pockets + outer boundary")
|
||||||
total_lines = _draw_polylines_batch(
|
|
||||||
work_part, sketch, all_polylines_3d, lister, close=True,
|
# Outer boundary
|
||||||
)
|
outer_lines = _draw_outer_boundary(work_part, outer_2d, transform, lister)
|
||||||
|
lister.WriteLine(f"[import] Outer boundary: {outer_lines} lines ({len(outer_2d)} pts)")
|
||||||
|
|
||||||
|
# Pockets
|
||||||
|
total_lines = outer_lines
|
||||||
|
total_arcs = 0
|
||||||
|
for idx, pocket in enumerate(pockets):
|
||||||
|
nl, na = _draw_structured_pocket(work_part, pocket, transform, lister)
|
||||||
|
total_lines += nl
|
||||||
|
total_arcs += na
|
||||||
|
if (idx + 1) % 50 == 0:
|
||||||
|
lister.WriteLine(f"[import] ... {idx+1}/{len(pockets)} pockets drawn")
|
||||||
|
|
||||||
|
lister.WriteLine(f"[import] Done: {total_lines} lines + {total_arcs} arcs in sketch")
|
||||||
|
lister.WriteLine(f"[import] Expected: ~{len(pockets)*3} lines + ~{len(pockets)*3} arcs")
|
||||||
|
else:
|
||||||
|
# Legacy format: pockets are point lists
|
||||||
|
lister.WriteLine(f"[import] Legacy format: {len(pockets)} pocket polylines")
|
||||||
|
outer_lines = _draw_outer_boundary(work_part, outer_2d, transform, lister)
|
||||||
|
total_lines = outer_lines
|
||||||
|
for pocket_pts in pockets:
|
||||||
|
if len(pocket_pts) < 3:
|
||||||
|
continue
|
||||||
|
pocket_3d = unproject_to_3d(pocket_pts, transform)
|
||||||
|
n = len(pocket_3d)
|
||||||
|
d = math.sqrt(sum((a - b)**2 for a, b in zip(pocket_3d[0], pocket_3d[-1])))
|
||||||
|
if d < 0.001:
|
||||||
|
n -= 1
|
||||||
|
for i in range(n):
|
||||||
|
try:
|
||||||
|
_draw_line(work_part, pocket_3d[i], pocket_3d[(i+1) % n])
|
||||||
|
total_lines += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
lister.WriteLine(f"[import] Done: {total_lines} lines in sketch")
|
||||||
|
|
||||||
# Deactivate sketch
|
# Deactivate sketch
|
||||||
try:
|
try:
|
||||||
# Resolve UpdateLevel enum at runtime
|
update_model, _ = _resolve_enum([
|
||||||
update_model = None
|
"NXOpen.Sketch.UpdateLevel.Model",
|
||||||
for path in ["NXOpen.Sketch.UpdateLevel.Model", "NXOpen.UpdateLevel.Model"]:
|
"NXOpen.UpdateLevel.Model",
|
||||||
try:
|
])
|
||||||
parts_p = path.split(".")
|
|
||||||
obj = __import__(parts_p[0])
|
|
||||||
for attr in parts_p[1:]:
|
|
||||||
obj = getattr(obj, attr)
|
|
||||||
update_model = obj
|
|
||||||
break
|
|
||||||
except (AttributeError, ImportError):
|
|
||||||
continue
|
|
||||||
if update_model is None:
|
if update_model is None:
|
||||||
update_model = 1 # numeric fallback
|
update_model = 1
|
||||||
sketch.Deactivate(view_false, update_model)
|
sketch.Deactivate(view_false, update_model)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
lister.WriteLine(f"[import] Warning deactivating: {exc}")
|
lister.WriteLine(f"[import] Warning deactivating: {exc}")
|
||||||
|
|
||||||
lister.WriteLine(f"[import] Done: {total_lines} total line segments in sketch '{sketch_name}'")
|
lister.WriteLine(f"[import] Pockets: {len(pockets)}")
|
||||||
lister.WriteLine(f"[import] Outer boundary: {len(outer_2d)} pts")
|
|
||||||
lister.WriteLine(f"[import] Pockets: {len(pockets)}")
|
|
||||||
|
|
||||||
lister.WriteLine("\n" + "=" * 60)
|
lister.WriteLine("\n" + "=" * 60)
|
||||||
lister.WriteLine(" Import complete — extrude the sketch to rib thickness")
|
lister.WriteLine(" Import complete — extrude the sketch to rib thickness")
|
||||||
|
|||||||
19
tools/adaptive-isogrid/tests/machining_params_6mm.json
Normal file
19
tools/adaptive-isogrid/tests/machining_params_6mm.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"eta_0": 0.1,
|
||||||
|
"alpha": 1.0,
|
||||||
|
"R_0": 30.0,
|
||||||
|
"kappa": 1.0,
|
||||||
|
"p": 2.0,
|
||||||
|
"beta": 0.3,
|
||||||
|
"R_edge": 15.0,
|
||||||
|
"s_min": 12.0,
|
||||||
|
"s_max": 35.0,
|
||||||
|
"t_min": 2.5,
|
||||||
|
"t_0": 3.0,
|
||||||
|
"gamma": 1.0,
|
||||||
|
"w_frame": 8.0,
|
||||||
|
"r_f": 3.06,
|
||||||
|
"d_keep": 1.5,
|
||||||
|
"min_pocket_radius": 3.5,
|
||||||
|
"min_triangle_area": 50.0
|
||||||
|
}
|
||||||
37881
tools/adaptive-isogrid/tests/rib_profile_sandbox_1.json
Normal file
37881
tools/adaptive-isogrid/tests/rib_profile_sandbox_1.json
Normal file
File diff suppressed because it is too large
Load Diff
37881
tools/adaptive-isogrid/tests/sandbox1_rib_profile_legacy.json
Normal file
37881
tools/adaptive-isogrid/tests/sandbox1_rib_profile_legacy.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user