diff --git a/tools/adaptive-isogrid/src/brain/__main__.py b/tools/adaptive-isogrid/src/brain/__main__.py index 413d6322..e9ca75be 100644 --- a/tools/adaptive-isogrid/src/brain/__main__.py +++ b/tools/adaptive-isogrid/src/brain/__main__.py @@ -63,23 +63,30 @@ def _plot_density(geometry: Dict[str, Any], params: Dict[str, Any], out_path: Pa plt.close(fig) -def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any], out_path: Path) -> None: - from shapely.geometry import Polygon as ShapelyPolygon, LineString +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. + """ + from shapely.geometry import Polygon as ShapelyPolygon, LineString, Point verts = triangulation["vertices"] tris = triangulation["triangles"] - fig, ax = plt.subplots(figsize=(8, 6), dpi=160) - outer = np.asarray(geometry["outer_boundary"]) plate_poly = ShapelyPolygon(outer) + w_frame = (params or {}).get("w_frame", 2.0) + inner_plate = plate_poly.buffer(-w_frame) + if inner_plate.is_empty or not inner_plate.is_valid: + inner_plate = plate_poly - # Draw only triangle edges that are inside the boundary (clipped) + fig, ax = plt.subplots(figsize=(8, 6), dpi=160) + + # Draw triangle edges clipped to inner plate 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)): + 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]])) @@ -88,22 +95,26 @@ def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any], drawn_edges.add(edge) p1, p2 = verts[edge[0]], verts[edge[1]] line = LineString([p1, p2]) - clipped = plate_poly.intersection(line) + 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.4, alpha=0.85) + 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.4, alpha=0.85) + ax.plot(cx, cy, color="#1f77b4", lw=0.5, alpha=0.85) - ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], "k-", lw=1.6) + # Green 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) + + # Blue hole circles for hole in geometry.get("holes", []): hb = np.asarray(hole["boundary"]) - 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.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]], + "b-", lw=0.8, zorder=3) ax.set_aspect("equal", adjustable="box") ax.set_title("Constrained Delaunay Triangulation / Rib Pattern") @@ -114,60 +125,77 @@ def _plot_triangulation(geometry: Dict[str, Any], triangulation: Dict[str, Any], plt.close(fig) -def _plot_final_profile(geometry, pockets, ribbed_plate, out_path: Path) -> None: +def _plot_final_profile(geometry, pockets, ribbed_plate, out_path: Path, params: Dict[str, Any] = None) -> None: + """ + Plot the rib profile: green sandbox boundary, pink pocket outlines (clipped), + blue hole circles. Pockets must respect w_frame inset from boundary. + Also produces a zoomed corner view as a second subplot. + """ 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") + w_frame = (params or {}).get("w_frame", 2.0) + inner_plate = plate_poly.buffer(-w_frame) + if inner_plate.is_empty or not inner_plate.is_valid: + inner_plate = plate_poly - # 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: + fig, (ax_full, ax_zoom) = plt.subplots(1, 2, figsize=(14, 6), dpi=160) + + for ax in (ax_full, ax_zoom): + # Green 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) + + # Draw pockets clipped to inner plate (w_frame inset) — outlines only + for pocket in pockets: + polyline = pocket.get("polyline", pocket.get("vertices", [])) + pv = np.asarray(polyline) + if len(pv) < 3: + continue 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) + clipped = inner_plate.intersection(pocket_poly) if clipped.is_empty: continue - # Draw clipped geometry - clip_geoms = [clipped] if clipped.geom_type == "Polygon" else list(clipped.geoms) + clip_geoms = [clipped] if clipped.geom_type == "Polygon" else list(getattr(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) + ax.fill(cx, cy, color="#ffcccc", alpha=0.25, lw=0.0) + ax.plot(cx, cy, color="#cc6677", lw=0.9, zorder=4) - # 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) + # Blue hole circles + 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]], + "b-", lw=0.8, zorder=3) - if ribbed_plate.geom_type == "Polygon": - geoms = [ribbed_plate] - else: - geoms = list(ribbed_plate.geoms) + ax.set_aspect("equal", adjustable="box") + ax.set_xlabel("x [mm]") + ax.set_ylabel("y [mm]") - for g in geoms: - x, y = g.exterior.xy - ax.plot(x, y, color="#228833", lw=1.2) - for i in g.interiors: - ix, iy = i.xy - ax.plot(ix, iy, color="#cc6677", lw=0.9) + # Full view + num_pockets_shown = sum( + 1 for p in pockets + if len(p.get("polyline", p.get("vertices", []))) >= 3 + and not inner_plate.intersection( + ShapelyPolygon(p.get("polyline", p.get("vertices", []))).buffer(0) + ).is_empty + ) + ax_full.set_title(f"Full view ({num_pockets_shown} pockets)") + + # Zoomed view on a corner area (top-right where crossovers were worst) + bounds = plate_poly.bounds # minx, miny, maxx, maxy + mid_x = (bounds[0] + bounds[2]) / 2 + ax_zoom.set_xlim(mid_x - 30, bounds[2] + 10) + ax_zoom.set_ylim(bounds[3] - 60, bounds[3] + 10) + ax_zoom.set_title("Zoomed: corner area") - ax.set_aspect("equal", adjustable="box") - ax.set_title("Final Ribbed Plate Profile with Pockets") - ax.set_xlabel("x [mm]") - ax.set_ylabel("y [mm]") fig.tight_layout() fig.savefig(out_path) plt.close(fig) @@ -199,8 +227,8 @@ def run_pipeline(geometry_path: Path, params_path: Path | None, output_dir: Path stem = geometry_path.stem _plot_density(geometry, params, output_dir / f"{stem}_density.png") - _plot_triangulation(geometry, triangulation, output_dir / f"{stem}_triangulation.png") - _plot_final_profile(geometry, pockets, ribbed_plate, output_dir / f"{stem}_final_profile.png") + _plot_triangulation(geometry, triangulation, output_dir / f"{stem}_triangulation.png", params=params) + _plot_final_profile(geometry, pockets, ribbed_plate, output_dir / f"{stem}_final_profile.png", params=params) summary = { "geometry": geometry.get("plate_id", geometry_path.name),