Add v2 geometry normalization and boundary-layer seed points
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user