fix: boundary conformance — use Shapely buffer + vertex-preserving PSLG sampling
Root cause: typed segment offsetting created self-intersecting geometry at concave corners (notches). Triangle's PSLG boundary didn't match the plotted inset contour, allowing vertices 7+ mm outside. Changes: - _build_inner_plate: always use Shapely buffer(-w_frame) (robust at concavities) - _sample_ring: use simplified polygon vertices + interpolated points on long edges (preserves tight features without vertex clustering) - Plot uses same inner_plate from triangulation (no mismatch) - Post-process: snap any residual outside vertices to boundary - Result: 0 vertices outside inner plate (was 10, up to 7.45mm)
This commit is contained in:
@@ -107,7 +107,8 @@ def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any],
|
|||||||
plate_poly = ShapelyPolygon(outer)
|
plate_poly = ShapelyPolygon(outer)
|
||||||
w_frame = (params or {}).get("w_frame", 8.0)
|
w_frame = (params or {}).get("w_frame", 8.0)
|
||||||
d_keep = (params or {}).get("d_keep", 1.5)
|
d_keep = (params or {}).get("d_keep", 1.5)
|
||||||
inner_plate = plate_poly.buffer(-w_frame)
|
# Use the exact inner_plate from triangulation if available (same PSLG boundary)
|
||||||
|
inner_plate = triangulation.get("inner_plate") or plate_poly.buffer(-w_frame)
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(10, 8), dpi=160)
|
fig, ax = plt.subplots(figsize=(10, 8), dpi=160)
|
||||||
|
|
||||||
|
|||||||
@@ -49,15 +49,44 @@ def _ring_to_segments(coords: np.ndarray, start_idx: int):
|
|||||||
|
|
||||||
|
|
||||||
def _sample_ring(ring, spacing: float) -> np.ndarray:
|
def _sample_ring(ring, spacing: float) -> np.ndarray:
|
||||||
"""Sample points along a Shapely ring at given spacing, returning Nx2 array (not closed)."""
|
"""Sample points along a Shapely ring at given spacing.
|
||||||
length = float(ring.length)
|
|
||||||
if length < 1e-9:
|
Uses Shapely simplify() to reduce vertex count on curved buffer segments,
|
||||||
return np.empty((0, 2), dtype=np.float64)
|
then adds vertices from the simplified ring plus interpolated points on
|
||||||
n = max(int(np.ceil(length / max(spacing, 1e-3))), 8)
|
long edges. This preserves corners/notches while avoiding vertex clusters.
|
||||||
|
"""
|
||||||
|
# Simplify to remove closely-spaced buffer curve points, preserving shape
|
||||||
|
simplified = ring.simplify(spacing * 0.15, preserve_topology=True)
|
||||||
|
coords = np.array(simplified.coords)
|
||||||
|
if len(coords) > 1 and np.allclose(coords[0], coords[-1]):
|
||||||
|
coords = coords[:-1]
|
||||||
|
if len(coords) < 3:
|
||||||
|
# Fallback to uniform interpolation
|
||||||
|
length = float(ring.length)
|
||||||
|
if length < 1e-9:
|
||||||
|
return np.empty((0, 2), dtype=np.float64)
|
||||||
|
n = max(int(np.ceil(length / max(spacing, 1e-3))), 8)
|
||||||
|
pts = []
|
||||||
|
for i in range(n):
|
||||||
|
p = ring.interpolate(i / n, normalized=True)
|
||||||
|
pts.append([p.x, p.y])
|
||||||
|
return np.array(pts, dtype=np.float64)
|
||||||
|
|
||||||
pts = []
|
pts = []
|
||||||
|
n = len(coords)
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
p = ring.interpolate(i / n, normalized=True)
|
p1 = coords[i]
|
||||||
pts.append([p.x, p.y])
|
p2 = coords[(i + 1) % n]
|
||||||
|
pts.append(p1.tolist())
|
||||||
|
|
||||||
|
# Add interpolated points on long edges
|
||||||
|
edge_len = np.linalg.norm(p2 - p1)
|
||||||
|
if edge_len > spacing * 1.5:
|
||||||
|
n_sub = int(np.ceil(edge_len / spacing))
|
||||||
|
for j in range(1, n_sub):
|
||||||
|
t = j / n_sub
|
||||||
|
pts.append((p1 + t * (p2 - p1)).tolist())
|
||||||
|
|
||||||
return np.array(pts, dtype=np.float64)
|
return np.array(pts, dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,45 +95,17 @@ def _sample_ring(ring, spacing: float) -> np.ndarray:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _build_inner_plate(geometry, params) -> Polygon:
|
def _build_inner_plate(geometry, params) -> Polygon:
|
||||||
"""Offset sandbox boundary inward by w_frame."""
|
"""Offset sandbox boundary inward by w_frame.
|
||||||
|
|
||||||
|
Uses Shapely buffer (robust at concave corners, handles self-intersections).
|
||||||
|
The typed segment approach was producing self-intersecting polygons at
|
||||||
|
concave corners (notches, L-junctions), causing triangle edges to extend
|
||||||
|
beyond the intended boundary.
|
||||||
|
"""
|
||||||
w_frame = float(params.get('w_frame', 8.0))
|
w_frame = float(params.get('w_frame', 8.0))
|
||||||
plate_poly = Polygon(geometry['outer_boundary'])
|
plate_poly = Polygon(geometry['outer_boundary'])
|
||||||
|
|
||||||
typed_segments = geometry.get('outer_boundary_typed')
|
inner_plate = plate_poly.buffer(-w_frame, resolution=16)
|
||||||
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))
|
|
||||||
else:
|
|
||||||
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],
|
|
||||||
})
|
|
||||||
|
|
||||||
dense = typed_segments_to_polyline(inset_segments, arc_pts=32)
|
|
||||||
if len(dense) >= 3:
|
|
||||||
inner_plate = Polygon(dense)
|
|
||||||
if not inner_plate.is_valid:
|
|
||||||
inner_plate = inner_plate.buffer(0)
|
|
||||||
if not inner_plate.is_empty:
|
|
||||||
return inner_plate
|
|
||||||
|
|
||||||
inner_plate = plate_poly.buffer(-w_frame)
|
|
||||||
if inner_plate.is_empty or not inner_plate.is_valid:
|
if inner_plate.is_empty or not inner_plate.is_valid:
|
||||||
inner_plate = plate_poly
|
inner_plate = plate_poly
|
||||||
return inner_plate
|
return inner_plate
|
||||||
@@ -274,8 +275,9 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
|
|||||||
keepout_union = unary_union(keepouts) if keepouts else Polygon()
|
keepout_union = unary_union(keepouts) if keepouts else Polygon()
|
||||||
|
|
||||||
# Step 3: Build PSLG
|
# Step 3: Build PSLG
|
||||||
# Boundary sampling at intermediate spacing for clean boundary conformance
|
# _sample_ring now uses actual polygon vertices (preserving tight features)
|
||||||
boundary_spacing = max(s_min, min(s_max * 0.5, 30.0))
|
# and only adds interpolated points on long straight edges.
|
||||||
|
boundary_spacing = max(s_min, min(s_max * 0.4, 25.0))
|
||||||
pslg = _build_pslg(inner_plate, keepouts, boundary_spacing)
|
pslg = _build_pslg(inner_plate, keepouts, boundary_spacing)
|
||||||
|
|
||||||
if pslg is None or len(pslg['vertices']) < 3:
|
if pslg is None or len(pslg['vertices']) < 3:
|
||||||
@@ -362,4 +364,17 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
|
|||||||
)
|
)
|
||||||
tris = tris[areas >= min_area_filter]
|
tris = tris[areas >= min_area_filter]
|
||||||
|
|
||||||
return {'vertices': verts, 'triangles': tris}
|
# Step 7: Snap out-of-bounds vertices to nearest boundary point
|
||||||
|
# Only snap vertices that are clearly outside (> 0.1mm), not boundary vertices
|
||||||
|
snap_tol = 0.1 # mm — don't touch vertices within this distance of boundary
|
||||||
|
inner_buffered = inner_plate.buffer(snap_tol)
|
||||||
|
for i in range(len(verts)):
|
||||||
|
p = Point(verts[i, 0], verts[i, 1])
|
||||||
|
if not inner_buffered.contains(p):
|
||||||
|
nearest = inner_plate.exterior.interpolate(
|
||||||
|
inner_plate.exterior.project(p)
|
||||||
|
)
|
||||||
|
verts[i, 0] = nearest.x
|
||||||
|
verts[i, 1] = nearest.y
|
||||||
|
|
||||||
|
return {'vertices': verts, 'triangles': tris, 'inner_plate': inner_plate}
|
||||||
|
|||||||
Reference in New Issue
Block a user