refactor: rewrite triangulation using Triangle library (constrained Delaunay + quality refinement)
- Replace scipy.spatial.Delaunay with Shewchuk's Triangle (PSLG-based) - Boundary conforming: PSLG constrains edges along inset contour + hole keepout rings - Quality: min angle 25°, no slivers - Per-triangle density-based area refinement (s_min=20, s_max=80) - Clean boundary plotting (no more crooked v1 line resampling) - Triangulation plot shows inset contour (red dashed) + keepout rings (orange dashed) - Add sandbox2_brain_input.json geometry file
This commit is contained in:
@@ -43,44 +43,21 @@ def _merge_params(geometry: Dict[str, Any], params_file: Path | None) -> Dict[st
|
||||
|
||||
|
||||
def _plot_boundary_polyline(geometry: Dict[str, Any], arc_pts: int = 64) -> np.ndarray:
|
||||
"""Get the sandbox boundary as a clean polyline for plotting.
|
||||
|
||||
For v2 typed segments: densify arcs, keep lines exact.
|
||||
For v1: just use the raw outer_boundary vertices — no resampling needed
|
||||
since these are the true polygon corners from NX.
|
||||
"""
|
||||
typed = geometry.get("outer_boundary_typed")
|
||||
if typed:
|
||||
pts = typed_segments_to_polyline(typed, arc_pts=arc_pts)
|
||||
if len(pts) >= 3:
|
||||
return np.asarray(pts, dtype=float)
|
||||
|
||||
# v1 fallback: use raw boundary vertices directly — they ARE the geometry.
|
||||
outer = np.asarray(geometry["outer_boundary"], dtype=float)
|
||||
if len(outer) < 4:
|
||||
return outer
|
||||
# v1 fallback: dense polyline boundaries may encode fillets.
|
||||
# Resample uniformly for smoother plotting while preserving the polygon path.
|
||||
if np.allclose(outer[0], outer[-1]):
|
||||
ring = outer
|
||||
else:
|
||||
ring = np.vstack([outer, outer[0]])
|
||||
seg = np.diff(ring, axis=0)
|
||||
seg_len = np.hypot(seg[:, 0], seg[:, 1])
|
||||
nonzero = seg_len[seg_len > 1e-9]
|
||||
if len(nonzero) == 0:
|
||||
return outer
|
||||
step = max(float(np.median(nonzero) * 0.5), 0.5)
|
||||
cum = np.r_[0.0, np.cumsum(seg_len)]
|
||||
total = float(cum[-1])
|
||||
if total <= step:
|
||||
return outer
|
||||
samples = np.arange(0.0, total, step, dtype=float)
|
||||
if samples[-1] < total:
|
||||
samples = np.r_[samples, total]
|
||||
out = []
|
||||
j = 0
|
||||
for s in samples:
|
||||
while j < len(cum) - 2 and cum[j + 1] < s:
|
||||
j += 1
|
||||
den = max(cum[j + 1] - cum[j], 1e-12)
|
||||
t = (s - cum[j]) / den
|
||||
p = ring[j] + t * (ring[j + 1] - ring[j])
|
||||
out.append([float(p[0]), float(p[1])])
|
||||
return np.asarray(out, dtype=float)
|
||||
return outer
|
||||
|
||||
|
||||
def _plot_density(geometry: Dict[str, Any], params: Dict[str, Any], out_path: Path, resolution: float = 3.0) -> None:
|
||||
@@ -114,65 +91,83 @@ def _plot_density(geometry: Dict[str, Any], params: Dict[str, Any], out_path: Pa
|
||||
|
||||
def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any], out_path: Path, params: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Plot the Delaunay triangulation clipped to the inner frame (w_frame inset).
|
||||
Green boundary, blue rib edges, blue hole circles.
|
||||
Plot the triangulation with:
|
||||
- Green: original sandbox boundary
|
||||
- Red dashed: inset contour (w_frame offset) — this is what the mesh fills
|
||||
- Blue: triangle edges
|
||||
- Blue circles: hole boundaries
|
||||
- Orange dashed: hole keepout rings
|
||||
"""
|
||||
from shapely.geometry import Polygon as ShapelyPolygon, LineString, Point
|
||||
from shapely.geometry import Polygon as ShapelyPolygon, Point as ShapelyPoint
|
||||
|
||||
verts = triangulation["vertices"]
|
||||
tris = triangulation["triangles"]
|
||||
|
||||
outer = _plot_boundary_polyline(geometry)
|
||||
plate_poly = ShapelyPolygon(outer)
|
||||
w_frame = (params or {}).get("w_frame", 2.0)
|
||||
w_frame = (params or {}).get("w_frame", 8.0)
|
||||
d_keep = (params or {}).get("d_keep", 1.5)
|
||||
inner_plate = plate_poly.buffer(-w_frame)
|
||||
if inner_plate.is_empty or not inner_plate.is_valid:
|
||||
inner_plate = plate_poly
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 6), dpi=160)
|
||||
fig, ax = plt.subplots(figsize=(10, 8), dpi=160)
|
||||
|
||||
# Draw triangle edges clipped to inner plate
|
||||
# Draw all triangle edges
|
||||
drawn_edges = set()
|
||||
for tri in tris:
|
||||
centroid = Point(verts[tri].mean(axis=0))
|
||||
if not inner_plate.contains(centroid):
|
||||
continue
|
||||
for i in range(3):
|
||||
edge = tuple(sorted([tri[i], tri[(i + 1) % 3]]))
|
||||
if edge in drawn_edges:
|
||||
continue
|
||||
drawn_edges.add(edge)
|
||||
p1, p2 = verts[edge[0]], verts[edge[1]]
|
||||
line = LineString([p1, p2])
|
||||
clipped = inner_plate.intersection(line)
|
||||
if clipped.is_empty:
|
||||
continue
|
||||
if clipped.geom_type == "LineString":
|
||||
cx, cy = clipped.xy
|
||||
ax.plot(cx, cy, color="#1f77b4", lw=0.5, alpha=0.85)
|
||||
elif clipped.geom_type == "MultiLineString":
|
||||
for seg in clipped.geoms:
|
||||
cx, cy = seg.xy
|
||||
ax.plot(cx, cy, color="#1f77b4", lw=0.5, alpha=0.85)
|
||||
ax.plot([p1[0], p2[0]], [p1[1], p2[1]],
|
||||
color="#1f77b4", lw=0.5, alpha=0.85)
|
||||
|
||||
# Green sandbox boundary
|
||||
# Green: original sandbox boundary
|
||||
ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]],
|
||||
color="#228833", lw=1.8, zorder=5)
|
||||
color="#228833", lw=1.8, zorder=5, label="Sandbox boundary")
|
||||
|
||||
# Blue hole circles
|
||||
# Red dashed: inset contour (w_frame)
|
||||
if not inner_plate.is_empty:
|
||||
polys = [inner_plate] if inner_plate.geom_type == 'Polygon' else list(inner_plate.geoms)
|
||||
for poly in polys:
|
||||
ix, iy = poly.exterior.xy
|
||||
ax.plot(ix, iy, color="#cc3333", lw=1.2, ls="--", zorder=4,
|
||||
label=f"Inset contour (w_frame={w_frame}mm)")
|
||||
|
||||
# Blue hole circles + Orange keepout rings
|
||||
for hole in geometry.get("holes", []):
|
||||
if hole.get("is_circular", False) and "center" in hole and "diameter" in hole:
|
||||
cx, cy = hole["center"]
|
||||
r = float(hole["diameter"]) * 0.5
|
||||
a = np.linspace(0.0, 2.0 * np.pi, 90, endpoint=True)
|
||||
# Hole circle
|
||||
hb = np.column_stack([cx + r * np.cos(a), cy + r * np.sin(a)])
|
||||
ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]],
|
||||
"b-", lw=0.8, zorder=3)
|
||||
# Keepout ring
|
||||
kr = r * (1.0 + d_keep)
|
||||
kb = np.column_stack([cx + kr * np.cos(a), cy + kr * np.sin(a)])
|
||||
ax.plot(np.r_[kb[:, 0], kb[0, 0]], np.r_[kb[:, 1], kb[0, 1]],
|
||||
color="#ff8800", lw=0.8, ls="--", zorder=3)
|
||||
else:
|
||||
hb = np.asarray(hole["boundary"])
|
||||
ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]],
|
||||
"b-", lw=0.8, zorder=3)
|
||||
ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]],
|
||||
"b-", lw=0.8, zorder=3)
|
||||
|
||||
# De-duplicate legend entries
|
||||
handles, labels = ax.get_legend_handles_labels()
|
||||
seen = set()
|
||||
unique_handles, unique_labels = [], []
|
||||
for h, l in zip(handles, labels):
|
||||
if l not in seen:
|
||||
seen.add(l)
|
||||
unique_handles.append(h)
|
||||
unique_labels.append(l)
|
||||
ax.legend(unique_handles, unique_labels, loc="upper right", fontsize=8)
|
||||
|
||||
ax.set_aspect("equal", adjustable="box")
|
||||
ax.set_title("Constrained Delaunay Triangulation / Rib Pattern")
|
||||
ax.set_title(f"Isogrid Triangulation ({len(tris)} triangles, {len(drawn_edges)} edges)")
|
||||
ax.set_xlabel("x [mm]")
|
||||
ax.set_ylabel("y [mm]")
|
||||
fig.tight_layout()
|
||||
|
||||
Reference in New Issue
Block a user