brain: add arc-aware inset boundary handling

This commit is contained in:
2026-02-17 14:05:28 +00:00
parent 18a8347765
commit 7d5bd33bb5
4 changed files with 221 additions and 47 deletions

View File

@@ -14,9 +14,10 @@ a viable machining pocket.
import numpy as np
from scipy.spatial import Delaunay
from shapely.geometry import Polygon, Point, MultiPoint
from shapely.geometry import Polygon, Point, MultiPoint, LinearRing
from shapely.ops import unary_union
from src.shared.arc_utils import inset_arc, typed_segments_to_polyline, typed_segments_to_sparse
from .density_field import evaluate_density, density_to_spacing
@@ -149,58 +150,93 @@ def _adaptive_hex_grid(geometry, params):
def _add_boundary_vertices(points, geometry, params, keepout_union):
"""
Add vertices along the outer boundary and hole keepout boundaries.
This ensures triangles conform to boundaries rather than just being
clipped. Points are spaced at approximately s_min along boundaries.
KEY: Enforce explicit vertices at every corner of the inset boundary.
This guarantees no triangle can cross a corner — the Delaunay triangulation
is forced to use these corner points as vertices.
"""
"""Add inset boundary vertices (arc-aware) and keepout boundary points."""
s_min = params['s_min']
w_frame = params.get('w_frame', 8.0)
new_pts = list(points)
plate_poly = Polygon(geometry['outer_boundary'])
inner_frame = plate_poly.buffer(-w_frame)
if not inner_frame.is_empty:
# Handle MultiPolygon from buffer on complex shapes
if inner_frame.geom_type == 'MultiPolygon':
inner_polys = list(inner_frame.geoms)
typed_segments = geometry.get('outer_boundary_typed')
if typed_segments:
ring = LinearRing(geometry['outer_boundary'])
is_ccw = bool(ring.is_ccw)
inset_segments = []
for seg in typed_segments:
stype = seg.get('type', 'line')
if stype == 'arc':
center_inside = plate_poly.contains(Point(seg['center']))
inset_segments.append(inset_arc({**seg, 'center_inside': center_inside}, w_frame))
continue
x1, y1 = seg['start']
x2, y2 = seg['end']
dx, dy = (x2 - x1), (y2 - y1)
ln = np.hypot(dx, dy)
if ln < 1e-12:
continue
nx_l, ny_l = (-dy / ln), (dx / ln)
nx, ny = (nx_l, ny_l) if is_ccw else (-nx_l, -ny_l)
inset_segments.append({
'type': 'line',
'start': [x1 + w_frame * nx, y1 + w_frame * ny],
'end': [x2 + w_frame * nx, y2 + w_frame * ny],
})
# Sparse points forced into Delaunay (3/arc, 2/line)
sparse_pts = typed_segments_to_sparse(inset_segments)
new_pts.extend(sparse_pts)
# Dense inset polygon for containment checks
dense = typed_segments_to_polyline(inset_segments, arc_pts=16)
if len(dense) >= 3:
inner_plate = Polygon(dense)
if not inner_plate.is_valid:
inner_plate = inner_plate.buffer(0)
if inner_plate.is_empty:
inner_plate = plate_poly.buffer(-w_frame)
else:
inner_polys = [inner_frame]
inner_plate = plate_poly.buffer(-w_frame)
for inner_poly in inner_polys:
ring = inner_poly.exterior
# 1) ENFORCE corner vertices: add every vertex of the inset boundary
# These are the actual corner points — critical for preventing crossovers
coords = list(ring.coords)[:-1] # skip closing duplicate
for cx, cy in coords:
new_pts.append([cx, cy])
# 2) Add evenly spaced points along edges for density
length = ring.length
n_pts = max(int(length / s_min), 4)
for i in range(n_pts):
frac = i / n_pts
pt = ring.interpolate(frac, normalized=True)
new_pts.append([pt.x, pt.y])
# Also add inner ring vertices (for any holes in the inset boundary)
for interior in inner_poly.interiors:
for cx, cy in list(interior.coords)[:-1]:
# Even spacing along inset boundary (as before)
if not inner_plate.is_empty and inner_plate.is_valid:
if inner_plate.geom_type == 'MultiPolygon':
inner_polys = list(inner_plate.geoms)
else:
inner_polys = [inner_plate]
for inner_poly in inner_polys:
ext = inner_poly.exterior
length = ext.length
n_pts = max(int(length / s_min), 4)
for i in range(n_pts):
frac = i / n_pts
pt = ext.interpolate(frac, normalized=True)
new_pts.append([pt.x, pt.y])
else:
inner_plate = plate_poly.buffer(-w_frame)
if not inner_plate.is_empty:
if inner_plate.geom_type == 'MultiPolygon':
inner_polys = list(inner_plate.geoms)
else:
inner_polys = [inner_plate]
for inner_poly in inner_polys:
ext = inner_poly.exterior
coords = list(ext.coords)[:-1]
for cx, cy in coords:
new_pts.append([cx, cy])
length = ext.length
n_pts = max(int(length / s_min), 4)
for i in range(n_pts):
frac = i / n_pts
pt = ext.interpolate(frac, normalized=True)
new_pts.append([pt.x, pt.y])
# Add points along hole keepout boundaries
if not keepout_union.is_empty:
geoms = [keepout_union] if keepout_union.geom_type == 'Polygon' else list(keepout_union.geoms)
for geom in geoms:
ring = geom.exterior
# Enforce corner vertices on keepout boundaries too
for cx, cy in list(ring.coords)[:-1]:
new_pts.append([cx, cy])
length = ring.length
@@ -210,7 +246,10 @@ def _add_boundary_vertices(points, geometry, params, keepout_union):
pt = ring.interpolate(frac, normalized=True)
new_pts.append([pt.x, pt.y])
return np.array(new_pts, dtype=np.float64)
if inner_plate.is_empty or not inner_plate.is_valid:
inner_plate = plate_poly
return np.array(new_pts, dtype=np.float64), inner_plate
def generate_triangulation(geometry, params, max_refinement_passes=3):
@@ -225,8 +264,8 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
if len(grid_pts) < 3:
return {'vertices': grid_pts, 'triangles': np.empty((0, 3), dtype=int)}
# Add boundary-conforming vertices
all_pts = _add_boundary_vertices(grid_pts, geometry, params, keepout_union)
# Add boundary-conforming vertices and get inset plate polygon for clipping
all_pts, inner_plate = _add_boundary_vertices(grid_pts, geometry, params, keepout_union)
# Deduplicate close points
all_pts = np.unique(np.round(all_pts, 4), axis=0)
@@ -240,8 +279,6 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
# Filter: keep only triangles whose centroid is inside valid region
plate_poly = Polygon(geometry['outer_boundary'])
w_frame = params.get('w_frame', 8.0)
inner_plate = plate_poly.buffer(-w_frame)
if inner_plate.is_empty:
inner_plate = plate_poly