Add v2 geometry normalization and boundary-layer seed points
This commit is contained in:
@@ -16,6 +16,7 @@ from shapely.geometry import Polygon
|
|||||||
from src.atomizer_study import DEFAULT_PARAMS
|
from src.atomizer_study import DEFAULT_PARAMS
|
||||||
from src.shared.arc_utils import typed_segments_to_polyline
|
from src.shared.arc_utils import typed_segments_to_polyline
|
||||||
from .density_field import evaluate_density_grid
|
from .density_field import evaluate_density_grid
|
||||||
|
from .geometry_schema import normalize_geometry_schema
|
||||||
from .triangulation import generate_triangulation
|
from .triangulation import generate_triangulation
|
||||||
from .pocket_profiles import generate_pockets
|
from .pocket_profiles import generate_pockets
|
||||||
from .profile_assembly import assemble_profile, profile_to_json
|
from .profile_assembly import assemble_profile, profile_to_json
|
||||||
@@ -212,7 +213,8 @@ def _plot_final_profile(geometry, pockets, ribbed_plate, out_path: Path, params:
|
|||||||
|
|
||||||
|
|
||||||
def run_pipeline(geometry_path: Path, params_path: Path | None, output_dir: Path, output_json_name: str = "rib_profile.json") -> Dict[str, Any]:
|
def run_pipeline(geometry_path: Path, params_path: Path | None, output_dir: Path, output_json_name: str = "rib_profile.json") -> Dict[str, Any]:
|
||||||
geometry = _load_json(geometry_path)
|
raw_geometry = _load_json(geometry_path)
|
||||||
|
geometry = normalize_geometry_schema(raw_geometry)
|
||||||
params = _merge_params(geometry, params_path)
|
params = _merge_params(geometry, params_path)
|
||||||
|
|
||||||
triangulation = generate_triangulation(geometry, params)
|
triangulation = generate_triangulation(geometry, params)
|
||||||
|
|||||||
132
tools/adaptive-isogrid/src/brain/geometry_schema.py
Normal file
132
tools/adaptive-isogrid/src/brain/geometry_schema.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""Geometry schema normalization (v1.0 and v2.0 typed-segment support)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from src.shared.arc_utils import arc_to_polyline
|
||||||
|
|
||||||
|
|
||||||
|
def _as_xy(pt: Any) -> List[float]:
|
||||||
|
return [float(pt[0]), float(pt[1])]
|
||||||
|
|
||||||
|
|
||||||
|
def _arc_span(seg: Dict[str, Any]) -> float:
|
||||||
|
cx, cy = _as_xy(seg["center"])
|
||||||
|
sx, sy = _as_xy(seg["start"])
|
||||||
|
ex, ey = _as_xy(seg["end"])
|
||||||
|
|
||||||
|
a0 = math.atan2(sy - cy, sx - cx)
|
||||||
|
a1 = math.atan2(ey - cy, ex - cx)
|
||||||
|
cw = bool(seg.get("clockwise", False))
|
||||||
|
|
||||||
|
if abs(sx - ex) < 1e-9 and abs(sy - ey) < 1e-9:
|
||||||
|
return 2.0 * math.pi
|
||||||
|
|
||||||
|
if cw:
|
||||||
|
if a1 > a0:
|
||||||
|
a1 -= 2.0 * math.pi
|
||||||
|
else:
|
||||||
|
if a1 < a0:
|
||||||
|
a1 += 2.0 * math.pi
|
||||||
|
return abs(a1 - a0)
|
||||||
|
|
||||||
|
|
||||||
|
def _typed_segments_to_polyline_v2(segments: List[Dict[str, Any]], full_circle_segments: int = 32) -> List[List[float]]:
|
||||||
|
out: List[List[float]] = []
|
||||||
|
|
||||||
|
for seg in segments or []:
|
||||||
|
stype = seg.get("type", "line")
|
||||||
|
if stype == "arc":
|
||||||
|
span = _arc_span(seg)
|
||||||
|
n_seg = max(2, int(round(full_circle_segments * span / (2.0 * math.pi))))
|
||||||
|
pts = arc_to_polyline(seg, n_pts=n_seg + 1)
|
||||||
|
else:
|
||||||
|
pts = [_as_xy(seg["start"]), _as_xy(seg["end"])]
|
||||||
|
|
||||||
|
if out and pts:
|
||||||
|
if abs(out[-1][0] - pts[0][0]) < 1e-9 and abs(out[-1][1] - pts[0][1]) < 1e-9:
|
||||||
|
out.extend(pts[1:])
|
||||||
|
else:
|
||||||
|
out.extend(pts)
|
||||||
|
else:
|
||||||
|
out.extend(pts)
|
||||||
|
|
||||||
|
if len(out) >= 2 and abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9:
|
||||||
|
out = out[:-1]
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _inner_boundary_to_hole(inner: Dict[str, Any], default_weight: float = 0.5) -> Dict[str, Any]:
|
||||||
|
segments = inner.get("segments", [])
|
||||||
|
boundary = _typed_segments_to_polyline_v2(segments)
|
||||||
|
|
||||||
|
# Circular hole detection: single full-circle arc
|
||||||
|
is_circular = False
|
||||||
|
center = None
|
||||||
|
diameter = None
|
||||||
|
if len(segments) == 1 and segments[0].get("type") == "arc":
|
||||||
|
seg = segments[0]
|
||||||
|
s = _as_xy(seg["start"])
|
||||||
|
e = _as_xy(seg["end"])
|
||||||
|
if abs(s[0] - e[0]) < 1e-8 and abs(s[1] - e[1]) < 1e-8:
|
||||||
|
is_circular = True
|
||||||
|
center = _as_xy(seg["center"])
|
||||||
|
diameter = 2.0 * float(seg["radius"])
|
||||||
|
|
||||||
|
if center is None or diameter is None:
|
||||||
|
if len(boundary) >= 3:
|
||||||
|
poly = Polygon(boundary)
|
||||||
|
if poly.is_valid and not poly.is_empty:
|
||||||
|
c = poly.centroid
|
||||||
|
center = [float(c.x), float(c.y)]
|
||||||
|
minx, miny, maxx, maxy = poly.bounds
|
||||||
|
diameter = float(max(maxx - minx, maxy - miny))
|
||||||
|
else:
|
||||||
|
arr = np.asarray(boundary, dtype=float)
|
||||||
|
center = [float(np.mean(arr[:, 0])), float(np.mean(arr[:, 1]))]
|
||||||
|
diameter = float(max(np.ptp(arr[:, 0]), np.ptp(arr[:, 1])))
|
||||||
|
else:
|
||||||
|
center = [0.0, 0.0]
|
||||||
|
diameter = 1.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"index": int(inner.get("index", 0)),
|
||||||
|
"center": center,
|
||||||
|
"diameter": float(diameter),
|
||||||
|
"boundary": boundary,
|
||||||
|
"is_circular": bool(is_circular),
|
||||||
|
"weight": float(inner.get("weight", default_weight)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_geometry_schema(geometry: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Return geometry in legacy Brain format (outer_boundary + holes), preserving typed data."""
|
||||||
|
schema_version = str(geometry.get("schema_version", "1.0"))
|
||||||
|
|
||||||
|
if schema_version.startswith("1"):
|
||||||
|
out = deepcopy(geometry)
|
||||||
|
out.setdefault("holes", [])
|
||||||
|
return out
|
||||||
|
|
||||||
|
if not schema_version.startswith("2"):
|
||||||
|
# Unknown schema: best effort fallback (assume legacy fields are present)
|
||||||
|
out = deepcopy(geometry)
|
||||||
|
out.setdefault("holes", [])
|
||||||
|
return out
|
||||||
|
|
||||||
|
out = deepcopy(geometry)
|
||||||
|
typed_outer = out.get("outer_boundary_typed", [])
|
||||||
|
if typed_outer:
|
||||||
|
out["outer_boundary"] = _typed_segments_to_polyline_v2(typed_outer)
|
||||||
|
|
||||||
|
inner_boundaries = out.get("inner_boundaries", [])
|
||||||
|
out["holes"] = [_inner_boundary_to_hole(inner) for inner in inner_boundaries]
|
||||||
|
|
||||||
|
return out
|
||||||
@@ -21,6 +21,71 @@ from src.shared.arc_utils import inset_arc, typed_segments_to_polyline, typed_se
|
|||||||
from .density_field import evaluate_density, density_to_spacing
|
from .density_field import evaluate_density, density_to_spacing
|
||||||
|
|
||||||
|
|
||||||
|
def _boundary_layer_offset_for_segment(mid_pt, geometry, params):
|
||||||
|
"""Choose inward offset for boundary seed row."""
|
||||||
|
explicit = params.get('boundary_layer_offset', None)
|
||||||
|
if explicit is not None:
|
||||||
|
return max(float(explicit), 0.0)
|
||||||
|
eta = evaluate_density(mid_pt[0], mid_pt[1], geometry, params)
|
||||||
|
return max(float(density_to_spacing(eta, params)), 1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_boundary_layer_seed_points(points, geometry, params, plate_poly, keepout_union):
|
||||||
|
"""Add a structured point row offset inward from each straight outer edge."""
|
||||||
|
boundary_pts = []
|
||||||
|
ring = LinearRing(geometry['outer_boundary'])
|
||||||
|
is_ccw = bool(ring.is_ccw)
|
||||||
|
|
||||||
|
# Prefer typed segments to avoid treating discretized arcs as straight edges
|
||||||
|
typed = geometry.get('outer_boundary_typed')
|
||||||
|
if typed:
|
||||||
|
segments = [seg for seg in typed if seg.get('type', 'line') == 'line']
|
||||||
|
edge_pairs = [(_np(seg['start']), _np(seg['end'])) for seg in segments]
|
||||||
|
else:
|
||||||
|
coords = np.asarray(geometry['outer_boundary'], dtype=float)
|
||||||
|
if len(coords) >= 2 and np.allclose(coords[0], coords[-1]):
|
||||||
|
coords = coords[:-1]
|
||||||
|
edge_pairs = []
|
||||||
|
for i in range(len(coords)):
|
||||||
|
edge_pairs.append((coords[i], coords[(i + 1) % len(coords)]))
|
||||||
|
|
||||||
|
for a, b in edge_pairs:
|
||||||
|
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||||
|
edge_len = float(np.hypot(dx, dy))
|
||||||
|
if edge_len < 1e-9:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mid = np.array([(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5], dtype=float)
|
||||||
|
spacing = float(density_to_spacing(evaluate_density(mid[0], mid[1], geometry, params), params))
|
||||||
|
spacing = max(spacing, 1e-3)
|
||||||
|
offset = _boundary_layer_offset_for_segment(mid, geometry, params)
|
||||||
|
|
||||||
|
nx_l, ny_l = (-dy / edge_len), (dx / edge_len)
|
||||||
|
nx, ny = (nx_l, ny_l) if is_ccw else (-nx_l, -ny_l)
|
||||||
|
|
||||||
|
n_pts = max(int(np.floor(edge_len / spacing)), 1)
|
||||||
|
for k in range(1, n_pts + 1):
|
||||||
|
t = k / (n_pts + 1)
|
||||||
|
bx = a[0] + t * dx
|
||||||
|
by = a[1] + t * dy
|
||||||
|
px = bx + offset * nx
|
||||||
|
py = by + offset * ny
|
||||||
|
p = Point(px, py)
|
||||||
|
if not plate_poly.buffer(1e-6).contains(p):
|
||||||
|
continue
|
||||||
|
if not keepout_union.is_empty and keepout_union.contains(p):
|
||||||
|
continue
|
||||||
|
boundary_pts.append([px, py])
|
||||||
|
|
||||||
|
if boundary_pts:
|
||||||
|
return np.vstack([points, np.asarray(boundary_pts, dtype=np.float64)])
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def _np(pt):
|
||||||
|
return np.asarray([float(pt[0]), float(pt[1])], dtype=float)
|
||||||
|
|
||||||
|
|
||||||
def _generate_hex_grid(bbox, base_spacing):
|
def _generate_hex_grid(bbox, base_spacing):
|
||||||
"""
|
"""
|
||||||
Generate a regular hexagonal-packed point grid.
|
Generate a regular hexagonal-packed point grid.
|
||||||
@@ -267,6 +332,10 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
|
|||||||
# Add boundary-conforming vertices and get inset plate polygon for clipping
|
# Add boundary-conforming vertices and get inset plate polygon for clipping
|
||||||
all_pts, inner_plate = _add_boundary_vertices(grid_pts, geometry, params, keepout_union)
|
all_pts, inner_plate = _add_boundary_vertices(grid_pts, geometry, params, keepout_union)
|
||||||
|
|
||||||
|
# Add structured boundary-layer seed row along straight edges
|
||||||
|
plate_poly = Polygon(geometry['outer_boundary'])
|
||||||
|
all_pts = _add_boundary_layer_seed_points(all_pts, geometry, params, plate_poly, keepout_union)
|
||||||
|
|
||||||
# Deduplicate close points
|
# Deduplicate close points
|
||||||
all_pts = np.unique(np.round(all_pts, 4), axis=0)
|
all_pts = np.unique(np.round(all_pts, 4), axis=0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user