diff --git a/tools/adaptive-isogrid/src/brain/__main__.py b/tools/adaptive-isogrid/src/brain/__main__.py index eea836c9..413d6322 100644 --- a/tools/adaptive-isogrid/src/brain/__main__.py +++ b/tools/adaptive-isogrid/src/brain/__main__.py @@ -64,17 +64,46 @@ 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) -> None: + from shapely.geometry import Polygon as ShapelyPolygon, LineString + verts = triangulation["vertices"] tris = triangulation["triangles"] fig, ax = plt.subplots(figsize=(8, 6), dpi=160) - ax.triplot(verts[:, 0], verts[:, 1], tris, color="#1f77b4", lw=0.4, alpha=0.85) outer = np.asarray(geometry["outer_boundary"]) + plate_poly = ShapelyPolygon(outer) + + # Draw only triangle edges that are inside the boundary (clipped) + drawn_edges = set() + for tri in tris: + centroid = verts[tri].mean(axis=0) + from shapely.geometry import Point + if not plate_poly.contains(Point(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 = plate_poly.intersection(line) + if clipped.is_empty: + continue + if clipped.geom_type == "LineString": + cx, cy = clipped.xy + ax.plot(cx, cy, color="#1f77b4", lw=0.4, 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.4, alpha=0.85) + ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], "k-", lw=1.6) for hole in geometry.get("holes", []): hb = np.asarray(hole["boundary"]) - ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]], "r-", lw=1.0) + ax.fill(hb[:, 0], hb[:, 1], color="white", zorder=2) + ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]], "r-", lw=1.0, zorder=3) ax.set_aspect("equal", adjustable="box") ax.set_title("Constrained Delaunay Triangulation / Rib Pattern") @@ -86,16 +115,42 @@ def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any], def _plot_final_profile(geometry, pockets, ribbed_plate, out_path: Path) -> None: + from shapely.geometry import Polygon as ShapelyPolygon + from matplotlib.patches import PathPatch + from matplotlib.path import Path as MplPath + fig, ax = plt.subplots(figsize=(8, 6), dpi=160) outer = np.asarray(geometry["outer_boundary"]) + plate_poly = ShapelyPolygon(outer) ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], "k-", lw=1.8, label="Outer boundary") + # Draw pockets clipped to the plate boundary — no crossovers for pocket in pockets: 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) + pocket_poly = ShapelyPolygon(pv) + if not pocket_poly.is_valid: + pocket_poly = pocket_poly.buffer(0) + if pocket_poly.is_empty: + continue + clipped = plate_poly.intersection(pocket_poly) + if clipped.is_empty: + continue + # Draw clipped geometry + clip_geoms = [clipped] if clipped.geom_type == "Polygon" else list(clipped.geoms) + for cg in clip_geoms: + if cg.geom_type != "Polygon" or cg.is_empty: + continue + cx, cy = cg.exterior.xy + ax.fill(cx, cy, color="#88ccee", alpha=0.35, lw=0.0) + + # Draw holes from geometry + for hole in geometry.get("holes", []): + hb = np.asarray(hole["boundary"]) + ax.fill(hb[:, 0], hb[:, 1], color="white", lw=0.0) + ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]], "k-", lw=0.7) if ribbed_plate.geom_type == "Polygon": geoms = [ribbed_plate] diff --git a/tools/adaptive-isogrid/src/brain/triangulation.py b/tools/adaptive-isogrid/src/brain/triangulation.py index 674d49c8..3aacb773 100644 --- a/tools/adaptive-isogrid/src/brain/triangulation.py +++ b/tools/adaptive-isogrid/src/brain/triangulation.py @@ -224,8 +224,20 @@ def generate_triangulation(geometry, params, max_refinement_passes=3): cx = np.mean(all_pts[t, 0]) cy = np.mean(all_pts[t, 1]) centroid = Point(cx, cy) - # Keep if centroid is inside plate frame and outside keepouts - if inner_plate.contains(centroid) and not keepout_union.contains(centroid): + # Keep if centroid is inside plate frame and outside keepouts, + # AND all 3 vertices are inside the plate boundary (no crossovers) + if not inner_plate.contains(centroid): + continue + if keepout_union.contains(centroid): + continue + all_inside = True + for vi in t: + if not plate_poly.contains(Point(all_pts[vi])): + # Allow small tolerance (vertex on boundary is OK) + if not plate_poly.buffer(0.5).contains(Point(all_pts[vi])): + all_inside = False + break + if all_inside: keep.append(i) triangles = triangles[keep] if keep else np.empty((0, 3), dtype=int)