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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user