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")
|
||||
|
||||
for pocket in pockets:
|
||||
pv = np.asarray(pocket["vertices"])
|
||||
ax.fill(pv[:, 0], pv[:, 1], color="#88ccee", alpha=0.35, lw=0.0)
|
||||
polyline = pocket.get("polyline", pocket.get("vertices", []))
|
||||
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":
|
||||
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)
|
||||
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["pipeline"] = {
|
||||
"geometry_file": str(geometry_path),
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
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
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
from .density_field import evaluate_density, density_to_rib_thickness
|
||||
|
||||
@@ -21,78 +24,6 @@ def get_unique_edges(triangles):
|
||||
return edges
|
||||
|
||||
|
||||
def get_triangle_edge_thicknesses(tri_vertices, tri_edges_thickness):
|
||||
"""Get thickness for each edge of a triangle."""
|
||||
return [tri_edges_thickness.get(e, 2.0) for e in tri_vertices]
|
||||
|
||||
|
||||
def inset_triangle(p0, p1, p2, d0, d1, d2):
|
||||
"""
|
||||
Inset a triangle by distances d0, d1, d2 on each edge.
|
||||
|
||||
Edge 0: p0→p1, inset by d0
|
||||
Edge 1: p1→p2, inset by d1
|
||||
Edge 2: p2→p0, inset by d2
|
||||
|
||||
Returns inset triangle vertices or None if triangle collapses.
|
||||
"""
|
||||
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])
|
||||
# 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]
|
||||
|
||||
# Compute inset lines for each edge
|
||||
inset_lines = []
|
||||
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
|
||||
new_verts = []
|
||||
for idx in range(3):
|
||||
a1, b1 = inset_lines[idx]
|
||||
a2, b2 = inset_lines[(idx + 1) % 3]
|
||||
pt = line_intersection(a1, b1, a2, b2)
|
||||
if pt is None:
|
||||
return None
|
||||
new_verts.append(pt)
|
||||
|
||||
# Check if triangle is valid (positive area)
|
||||
area = triangle_area(new_verts[0], new_verts[1], new_verts[2])
|
||||
if area < 0.1: # mm² — too small
|
||||
return None
|
||||
|
||||
return new_verts
|
||||
|
||||
|
||||
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 # parallel
|
||||
t = ((a2[0] - a1[0]) * d2[1] - (a2[1] - a1[1]) * d2[0]) / cross
|
||||
return a1 + t * d1
|
||||
|
||||
|
||||
def triangle_area(p0, p1, p2):
|
||||
"""Signed area of triangle."""
|
||||
return 0.5 * abs(
|
||||
@@ -101,23 +32,258 @@ def triangle_area(p0, p1, p2):
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Inset a triangle by distances d0, d1, d2 on each edge.
|
||||
|
||||
Edge 0: p0→p1, inset by d0
|
||||
Edge 1: p1→p2, inset by d1
|
||||
Edge 2: p2→p0, inset by d2
|
||||
|
||||
Returns inset triangle vertices [v0, v1, v2] or None if collapsed.
|
||||
"""
|
||||
points = [np.array(p0, dtype=float), np.array(p1, dtype=float), np.array(p2, dtype=float)]
|
||||
offsets = [d0, d1, d2]
|
||||
edges = [(0, 1, 2), (1, 2, 0), (2, 0, 1)]
|
||||
|
||||
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 = []
|
||||
for idx in range(3):
|
||||
a1, b1 = inset_lines[idx]
|
||||
a2, b2 = inset_lines[(idx + 1) % 3]
|
||||
pt = _line_intersection(a1, b1, a2, b2)
|
||||
if pt is None:
|
||||
return None
|
||||
new_verts.append(pt)
|
||||
|
||||
if triangle_area(new_verts[0], new_verts[1], new_verts[2]) < 0.1:
|
||||
return None
|
||||
|
||||
return new_verts
|
||||
|
||||
|
||||
def _fillet_corner(
|
||||
v_prev: np.ndarray,
|
||||
v_corner: np.ndarray,
|
||||
v_next: np.ndarray,
|
||||
r_f: float,
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
Compute a fillet arc at v_corner between edges v_prev→v_corner and
|
||||
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)
|
||||
|
||||
# Half-angle between the two edges
|
||||
cos_half = np.dot(d1, d2)
|
||||
cos_half = np.clip(cos_half, -1.0, 1.0)
|
||||
half_angle = np.arccos(cos_half) / 2.0
|
||||
|
||||
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):
|
||||
"""
|
||||
Generate pocket profiles from triangulation.
|
||||
|
||||
Each triangle becomes a pocket (inset + filleted).
|
||||
Small triangles are left solid (no pocket).
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict] : pocket definitions with vertices and metadata.
|
||||
|
||||
Each triangle becomes a pocket: inset by half-rib-thickness, filleted corners.
|
||||
Output is structured geometry (lines + arcs), not dense polylines.
|
||||
|
||||
Returns list[dict] with keys:
|
||||
triangle_index, lines, arcs, vertices, area, polyline (for viz)
|
||||
"""
|
||||
vertices = triangulation['vertices']
|
||||
triangles = triangulation['triangles']
|
||||
r_f = params['r_f']
|
||||
min_pocket_radius = params.get('min_pocket_radius', 1.5)
|
||||
min_triangle_area = params.get('min_triangle_area', 20.0)
|
||||
|
||||
|
||||
# Precompute edge thicknesses
|
||||
edge_thickness = {}
|
||||
for tri in triangles:
|
||||
@@ -127,56 +293,59 @@ def generate_pockets(triangulation, geometry, params):
|
||||
mid = (vertices[edge[0]] + vertices[edge[1]]) / 2.0
|
||||
eta = evaluate_density(mid[0], mid[1], geometry, params)
|
||||
edge_thickness[edge] = density_to_rib_thickness(eta, params)
|
||||
|
||||
|
||||
pockets = []
|
||||
for tri_idx, tri in enumerate(triangles):
|
||||
p0 = vertices[tri[0]]
|
||||
p1 = vertices[tri[1]]
|
||||
p2 = vertices[tri[2]]
|
||||
|
||||
# Leave tiny triangles solid to avoid unpocketable slivers
|
||||
tri_area = triangle_area(p0, p1, p2)
|
||||
if tri_area < min_triangle_area:
|
||||
continue
|
||||
|
||||
# Get edge thicknesses (half for inset)
|
||||
e01 = tuple(sorted([tri[0], tri[1]]))
|
||||
e12 = tuple(sorted([tri[1], tri[2]]))
|
||||
e20 = tuple(sorted([tri[2], tri[0]]))
|
||||
|
||||
|
||||
d0 = edge_thickness[e01] / 2.0
|
||||
d1 = edge_thickness[e12] / 2.0
|
||||
d2 = edge_thickness[e20] / 2.0
|
||||
|
||||
|
||||
inset = inset_triangle(p0, p1, p2, d0, d1, d2)
|
||||
if inset is None:
|
||||
continue
|
||||
|
||||
# Check minimum pocket size via inscribed circle approximation
|
||||
pocket_poly = Polygon(inset)
|
||||
if not pocket_poly.is_valid or pocket_poly.is_empty:
|
||||
|
||||
# Check minimum pocket size via inscribed circle
|
||||
inset_area = triangle_area(inset[0], inset[1], inset[2])
|
||||
perimeter = sum(
|
||||
np.linalg.norm(inset[(i+1)%3] - inset[i]) for i in range(3)
|
||||
)
|
||||
if perimeter < 1e-6:
|
||||
continue
|
||||
|
||||
# Approximate inscribed radius
|
||||
inscribed_r = pocket_poly.area / (pocket_poly.length / 2.0)
|
||||
inscribed_r = inset_area / (perimeter / 2.0)
|
||||
if inscribed_r < min_pocket_radius:
|
||||
continue
|
||||
|
||||
# Apply fillet (buffer negative then positive = round inward corners)
|
||||
fillet_amount = min(r_f, inscribed_r * 0.4) # don't over-fillet
|
||||
if fillet_amount > 0.1:
|
||||
filleted = pocket_poly.buffer(-fillet_amount).buffer(fillet_amount)
|
||||
else:
|
||||
filleted = pocket_poly
|
||||
|
||||
if filleted.is_empty or not filleted.is_valid:
|
||||
|
||||
# Compute fillet amount (don't over-fillet)
|
||||
fillet_amount = min(r_f, inscribed_r * 0.4)
|
||||
if fillet_amount < 0.1:
|
||||
fillet_amount = 0.0
|
||||
|
||||
pocket_geom = _compute_filleted_pocket(inset, fillet_amount)
|
||||
if pocket_geom is None:
|
||||
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({
|
||||
'triangle_index': tri_idx,
|
||||
'vertices': coords,
|
||||
'area': filleted.area,
|
||||
'lines': pocket_geom['lines'],
|
||||
'arcs': pocket_geom['arcs'],
|
||||
'vertices': pocket_geom['vertices'],
|
||||
'polyline': polyline,
|
||||
'area': inset_area, # approximate
|
||||
})
|
||||
|
||||
|
||||
return pockets
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Profile assembly — combine plate boundary, pockets, and holes
|
||||
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
|
||||
@@ -27,89 +27,89 @@ def hole_to_actual_polygon(hole):
|
||||
|
||||
def assemble_profile(geometry, pockets, params):
|
||||
"""
|
||||
Create the final 2D ribbed plate profile.
|
||||
|
||||
Plate boundary - pockets - holes = ribbed plate
|
||||
|
||||
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.
|
||||
Create the final 2D ribbed plate profile using Shapely for
|
||||
boolean operations and validation.
|
||||
|
||||
Returns Shapely Polygon/MultiPolygon for visualization and validation.
|
||||
The actual NX import uses the structured pocket geometry directly.
|
||||
"""
|
||||
plate = Polygon(geometry['outer_boundary'])
|
||||
w_frame = params['w_frame']
|
||||
|
||||
# Inner boundary (frame inset)
|
||||
|
||||
if w_frame > 0:
|
||||
inner_plate = plate.buffer(-w_frame)
|
||||
else:
|
||||
inner_plate = plate
|
||||
|
||||
|
||||
if inner_plate.is_empty:
|
||||
# Frame too wide for plate — return solid plate minus holes
|
||||
ribbed = plate
|
||||
for hole in geometry['holes']:
|
||||
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
||||
return ribbed
|
||||
|
||||
# Union all pocket polygons
|
||||
|
||||
# Build pocket polygons from low-res polylines (for Shapely ops only)
|
||||
pocket_polys = []
|
||||
for p in pockets:
|
||||
poly = Polygon(p['vertices'])
|
||||
if poly.is_valid and not poly.is_empty:
|
||||
pocket_polys.append(poly)
|
||||
|
||||
polyline = p.get('polyline', [])
|
||||
if len(polyline) >= 3:
|
||||
poly = Polygon(polyline)
|
||||
if poly.is_valid and not poly.is_empty:
|
||||
pocket_polys.append(poly)
|
||||
|
||||
if 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)
|
||||
# Subtract pockets from plate
|
||||
ribbed = plate.difference(clipped_pockets)
|
||||
else:
|
||||
ribbed = plate
|
||||
|
||||
# Subtract actual hole boundaries (not boss keepouts)
|
||||
|
||||
for hole in geometry['holes']:
|
||||
ribbed = ribbed.difference(hole_to_actual_polygon(hole))
|
||||
|
||||
|
||||
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
|
||||
for NXOpen import.
|
||||
Convert to JSON-serializable dict for NX 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:
|
||||
return {'valid': False, 'reason': 'empty_geometry'}
|
||||
|
||||
# Separate exterior and interiors
|
||||
if ribbed_plate.geom_type == 'MultiPolygon':
|
||||
# Take the largest polygon (should be the plate)
|
||||
largest = max(ribbed_plate.geoms, key=lambda g: g.area)
|
||||
|
||||
outer = geometry['outer_boundary']
|
||||
if hasattr(outer, 'tolist'):
|
||||
outer = outer.tolist()
|
||||
else:
|
||||
largest = ribbed_plate
|
||||
|
||||
# Classify interiors as pockets or holes
|
||||
outer_coords = list(largest.exterior.coords)
|
||||
pocket_coords = []
|
||||
hole_coords = []
|
||||
|
||||
for interior in largest.interiors:
|
||||
coords = list(interior.coords)
|
||||
pocket_coords.append(coords)
|
||||
|
||||
outer = [list(p) for p in outer]
|
||||
|
||||
# Structured pocket data (lines + arcs)
|
||||
pocket_data = []
|
||||
for p in pockets:
|
||||
pocket_data.append({
|
||||
'lines': p['lines'],
|
||||
'arcs': p['arcs'],
|
||||
})
|
||||
|
||||
# Hole data for NX (circles)
|
||||
hole_data = []
|
||||
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 {
|
||||
'valid': True,
|
||||
'outer_boundary': outer_coords,
|
||||
'pockets': pocket_coords,
|
||||
'outer_boundary': outer,
|
||||
'pockets': pocket_data,
|
||||
'holes': hole_data,
|
||||
'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
|
||||
an NX sketch on the sandbox plane containing:
|
||||
- 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
|
||||
time; subsequent iterations only update the sketch and the extrude regenerates.
|
||||
@@ -55,6 +55,22 @@ def unproject_to_3d(
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -82,7 +98,6 @@ def _find_or_create_sketch(
|
||||
except Exception:
|
||||
continue
|
||||
if fname == sketch_name:
|
||||
# Get the sketch from the feature
|
||||
try:
|
||||
existing_sketch = feat.GetEntities()[0]
|
||||
lister.WriteLine(f"[import] Found existing sketch: {sketch_name}")
|
||||
@@ -93,7 +108,6 @@ def _find_or_create_sketch(
|
||||
pass
|
||||
|
||||
if existing_sketch is not None:
|
||||
# Clear existing geometry for update
|
||||
try:
|
||||
existing_sketch.Activate(False)
|
||||
all_geom = existing_sketch.GetAllGeometry()
|
||||
@@ -116,31 +130,20 @@ def _find_or_create_sketch(
|
||||
normal_vec = NXOpen.Vector3d(normal[0], normal[1], normal[2])
|
||||
x_vec = NXOpen.Vector3d(x_axis[0], x_axis[1], x_axis[2])
|
||||
|
||||
# Create a Plane (SmartObject) — NOT a DatumPlane (DisplayableObject)
|
||||
# PlaneCollection.CreatePlane(method, alternate, origin, normal, expr, flip, percent, geometry)
|
||||
# Create a Plane (SmartObject)
|
||||
plane = part.Planes.CreatePlane(
|
||||
NXOpen.PlaneTypes.MethodType.Fixed, # Fixed plane
|
||||
NXOpen.PlaneTypes.MethodType.Fixed,
|
||||
NXOpen.PlaneTypes.AlternateType.One,
|
||||
origin_pt,
|
||||
normal_vec,
|
||||
"", # expression
|
||||
False, # flip
|
||||
False, # percent
|
||||
[], # geometry refs
|
||||
"",
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
)
|
||||
lister.WriteLine(f"[import] Created plane: {plane} (type={type(plane).__name__})")
|
||||
|
||||
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"]
|
||||
mtx = NXOpen.Matrix3x3()
|
||||
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(
|
||||
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)
|
||||
|
||||
# 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)
|
||||
axis_dir = part.Directions.CreateDirection(
|
||||
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 [
|
||||
("PlaneReference", plane, "plane"),
|
||||
("SketchOrigin", origin_point, "origin"),
|
||||
("AxisReference", axis_dir, "axis"),
|
||||
]:
|
||||
# Pattern 1: setattr (property assignment)
|
||||
try:
|
||||
setattr(sketch_builder, attr, val)
|
||||
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:
|
||||
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()
|
||||
lister.WriteLine(f"[import] Commit returned: {sketch_obj} (type={type(sketch_obj).__name__})")
|
||||
|
||||
# Also get committed objects for inspection
|
||||
committed = sketch_builder.GetCommittedObjects()
|
||||
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()
|
||||
|
||||
# Find the Sketch object from committed objects or return value
|
||||
# Get Sketch object
|
||||
sketch = None
|
||||
if isinstance(sketch_obj, NXOpen.Sketch):
|
||||
sketch = sketch_obj
|
||||
else:
|
||||
# Search committed objects
|
||||
for obj in committed:
|
||||
if isinstance(obj, NXOpen.Sketch):
|
||||
sketch = obj
|
||||
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:
|
||||
for s in part.Sketches:
|
||||
sketch = s # take the last one (just created)
|
||||
lister.WriteLine(f"[import] Found sketch by iteration: {sketch}")
|
||||
|
||||
sketch = s
|
||||
if sketch is None:
|
||||
raise RuntimeError("Could not get Sketch object after commit")
|
||||
|
||||
# Rename
|
||||
try:
|
||||
sketch.Name = sketch_name
|
||||
except Exception:
|
||||
@@ -246,90 +205,133 @@ def _find_or_create_sketch(
|
||||
return sketch
|
||||
|
||||
|
||||
def _draw_polylines_batch(
|
||||
part: Any,
|
||||
sketch: Any,
|
||||
polylines_3d: List[List[Tuple[float, float, float]]],
|
||||
lister: Any,
|
||||
close: bool = True,
|
||||
) -> int:
|
||||
"""
|
||||
Draw multiple closed polylines directly in the active sketch using
|
||||
SketchLineBuilder (creates native sketch geometry, not model curves).
|
||||
|
||||
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:
|
||||
def _resolve_enum(paths):
|
||||
"""Try multiple dotted paths to resolve an NXOpen enum value."""
|
||||
for path in paths:
|
||||
try:
|
||||
parts_p = path.split(".")
|
||||
obj = __import__(parts_p[0])
|
||||
for attr in parts_p[1:]:
|
||||
obj = getattr(obj, attr)
|
||||
return obj, path
|
||||
except (AttributeError, ImportError):
|
||||
continue
|
||||
|
||||
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
|
||||
return None, None
|
||||
|
||||
|
||||
def _draw_circle_in_sketch(
|
||||
part: Any,
|
||||
sketch: Any,
|
||||
center_3d: Tuple[float, float, float],
|
||||
radius: float,
|
||||
normal: List[float],
|
||||
lister: Any,
|
||||
) -> bool:
|
||||
"""Draw a circle in the sketch."""
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sketch drawing: lines and arcs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _draw_line(part, p1_3d, p2_3d):
|
||||
"""Draw a single line in the active sketch. Returns True on success."""
|
||||
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()
|
||||
center_pt = NXOpen.Point3d(center_3d[0], center_3d[1], center_3d[2])
|
||||
circle_builder.SetCenterPoint(center_pt)
|
||||
# Size point = center + radius along x
|
||||
size_pt = NXOpen.Point3d(
|
||||
center_3d[0] + radius,
|
||||
center_3d[1],
|
||||
center_3d[2],
|
||||
)
|
||||
circle_builder.SetSizePoint(size_pt)
|
||||
circle_builder.Commit()
|
||||
circle_builder.Destroy()
|
||||
return True
|
||||
except Exception as exc:
|
||||
lister.WriteLine(f"[import] Circle creation failed: {exc}")
|
||||
return False
|
||||
|
||||
def _draw_arc_3pt(part, start_3d, mid_3d, end_3d):
|
||||
"""
|
||||
Draw a 3-point arc in the active sketch.
|
||||
SketchArcBuilder: SetStartPoint, SetEndPoint, SetThirdPoint, Commit.
|
||||
"""
|
||||
import NXOpen
|
||||
ab = part.Sketches.CreateArcBuilder()
|
||||
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]))
|
||||
ab.SetThirdPoint(NXOpen.Point3d(mid_3d[0], mid_3d[1], mid_3d[2]))
|
||||
ab.Commit()
|
||||
ab.Destroy()
|
||||
return True
|
||||
|
||||
|
||||
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 ""
|
||||
lister.WriteLine(f"[import] Work part: {part_name}")
|
||||
|
||||
# If not in idealized part, find it
|
||||
if not part_name.endswith("_i"):
|
||||
for part in session.Parts:
|
||||
pname = part.Name if hasattr(part, "Name") else ""
|
||||
@@ -362,7 +363,6 @@ def main():
|
||||
lister.WriteLine(f"[import] Switched to idealized part: {pname}")
|
||||
break
|
||||
|
||||
# Find data directory
|
||||
try:
|
||||
part_dir = os.path.dirname(work_part.FullPath)
|
||||
except Exception:
|
||||
@@ -374,22 +374,18 @@ def main():
|
||||
lister.WriteLine(f"[import] ERROR: Data directory not found: {data_dir}")
|
||||
return
|
||||
|
||||
# Find all rib profile + geometry JSON pairs
|
||||
# Find rib profile JSONs
|
||||
profile_files = sorted([
|
||||
f for f in os.listdir(data_dir)
|
||||
if f.startswith("rib_profile_") and f.endswith(".json")
|
||||
])
|
||||
|
||||
if not profile_files:
|
||||
# Also check for sandbox1_rib_profile.json pattern
|
||||
profile_files = sorted([
|
||||
f for f in os.listdir(data_dir)
|
||||
if "rib_profile" in f and f.endswith(".json")
|
||||
])
|
||||
|
||||
if not profile_files:
|
||||
lister.WriteLine(f"[import] ERROR: No rib_profile*.json found in {data_dir}")
|
||||
lister.WriteLine(f"[import] Files present: {os.listdir(data_dir)}")
|
||||
lister.WriteLine(f"[import] ERROR: No rib_profile*.json in {data_dir}")
|
||||
return
|
||||
|
||||
lister.WriteLine(f"[import] Found {len(profile_files)} profile(s) to import")
|
||||
@@ -397,21 +393,17 @@ def main():
|
||||
for profile_file in profile_files:
|
||||
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", "")
|
||||
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")
|
||||
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")]
|
||||
if candidates:
|
||||
geom_path = os.path.join(data_dir, candidates[0])
|
||||
else:
|
||||
lister.WriteLine(f"[import] ERROR: No geometry JSON found for transform data")
|
||||
lister.WriteLine("[import] ERROR: No geometry JSON for transform")
|
||||
continue
|
||||
|
||||
lister.WriteLine(f"\n--- Importing {profile_file} ---")
|
||||
@@ -429,7 +421,7 @@ def main():
|
||||
transform = geometry["transform"]
|
||||
sketch_name = SKETCH_NAME_PREFIX + sandbox_id
|
||||
|
||||
# Find or create sketch
|
||||
# Create sketch
|
||||
try:
|
||||
sketch = _find_or_create_sketch(work_part, sketch_name, transform, lister)
|
||||
except Exception as exc:
|
||||
@@ -438,86 +430,81 @@ def main():
|
||||
lister.WriteLine(traceback.format_exc())
|
||||
continue
|
||||
|
||||
# Activate sketch for drawing
|
||||
# Activate sketch
|
||||
try:
|
||||
# ViewReorient enum — try multiple access paths
|
||||
view_false = None
|
||||
for path in [
|
||||
"NXOpen.ViewReorient.FalseValue",
|
||||
view_false, path = _resolve_enum([
|
||||
"NXOpen.Sketch.ViewReorient.FalseValue",
|
||||
"NXOpen.SketchViewReorient.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
|
||||
|
||||
"NXOpen.ViewReorient.FalseValue",
|
||||
])
|
||||
if view_false is None:
|
||||
# Last resort: pass False as boolean
|
||||
view_false = False
|
||||
lister.WriteLine("[import] ViewReorient: using False (boolean fallback)")
|
||||
|
||||
lister.WriteLine(f"[import] ViewReorient: {path or 'boolean fallback'}")
|
||||
sketch.Activate(view_false)
|
||||
except Exception as exc:
|
||||
lister.WriteLine(f"[import] ERROR activating sketch: {exc}")
|
||||
continue
|
||||
|
||||
total_lines = 0
|
||||
|
||||
# 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
|
||||
# --- Draw geometry ---
|
||||
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:
|
||||
if len(pocket_pts) < 3:
|
||||
continue
|
||||
pocket_3d = unproject_to_3d(pocket_pts, transform)
|
||||
all_polylines_3d.append(pocket_3d)
|
||||
# Check if pockets are structured (lines+arcs) or legacy (point lists)
|
||||
is_structured = (len(pockets) > 0 and isinstance(pockets[0], dict)
|
||||
and 'lines' in pockets[0])
|
||||
|
||||
# Draw everything in one batch
|
||||
lister.WriteLine(f"[import] Creating {len(all_polylines_3d)} polylines as NX curves...")
|
||||
total_lines = _draw_polylines_batch(
|
||||
work_part, sketch, all_polylines_3d, lister, close=True,
|
||||
)
|
||||
if is_structured:
|
||||
lister.WriteLine(f"[import] Structured format: {len(pockets)} pockets + outer boundary")
|
||||
|
||||
# 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
|
||||
try:
|
||||
# Resolve UpdateLevel enum at runtime
|
||||
update_model = None
|
||||
for path in ["NXOpen.Sketch.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
|
||||
update_model, _ = _resolve_enum([
|
||||
"NXOpen.Sketch.UpdateLevel.Model",
|
||||
"NXOpen.UpdateLevel.Model",
|
||||
])
|
||||
if update_model is None:
|
||||
update_model = 1 # numeric fallback
|
||||
update_model = 1
|
||||
sketch.Deactivate(view_false, update_model)
|
||||
except Exception as 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] Outer boundary: {len(outer_2d)} pts")
|
||||
lister.WriteLine(f"[import] Pockets: {len(pockets)}")
|
||||
lister.WriteLine(f"[import] Pockets: {len(pockets)}")
|
||||
|
||||
lister.WriteLine("\n" + "=" * 60)
|
||||
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