ad88d92ab9
Phase 5 of the decoupling refactor (behavior-preserving move): - translator/ir.py: SolidWorks plugin JSON to backend-IR conversion (70 syms) - translator/codegen.py: backend IR to build123d source generation (64 syms) - translator/runtime_lib.py: frozen generated-script runtime library, spliced into generate_build123d_code as *RUNTIME_LIB_LINES - translator/common.py: helpers shared by both sides - translator/__init__.py: full historical symbol surface re-exported Generated-code equivalence verified byte-for-byte against the pre-split output for a representative IR sample; py_compile clean.
195 lines
6.5 KiB
Python
195 lines
6.5 KiB
Python
"""Shared helpers for the SW-IR and code-generation sides of the translator.
|
|
|
|
These small utilities are used by both ``ir`` (SolidWorks plugin JSON to
|
|
backend IR) and ``codegen`` (backend IR to build123d source). Anything used
|
|
by exactly one side lives in that side's module instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from typing import Any, Optional
|
|
|
|
#: SolidWorks numeric end-condition codes, shared by IR conversion and codegen.
|
|
SW_END_CONDITIONS = {
|
|
0: "Blind",
|
|
1: "ThroughAll",
|
|
2: "ThroughAllBoth",
|
|
3: "UpToVertex",
|
|
4: "UpToSurface",
|
|
5: "OffsetFromSurface",
|
|
6: "ThroughAllAndBlind",
|
|
7: "UpToBody",
|
|
8: "MidPlane",
|
|
9: "ThroughNext",
|
|
}
|
|
|
|
#: Generous through-cut length used when a termination reference is missing.
|
|
THROUGH_CUT_AMOUNT_MM = 200
|
|
|
|
|
|
def _tuple3(values: Any) -> tuple[float, float, float]:
|
|
values = list(values or [0, 0, 0])
|
|
values = (values + [0, 0, 0])[:3]
|
|
return tuple(values)
|
|
|
|
|
|
def _point_m_to_mm(point: Any) -> tuple[float, float, float]:
|
|
values = list(point or [0, 0, 0])
|
|
values = (values + [0, 0, 0])[:3]
|
|
return tuple(float(value) * 1000 for value in values)
|
|
|
|
|
|
def _scale_point(point: Any) -> list[float]:
|
|
values = [0 if value is None else float(value) for value in (point or [0, 0])]
|
|
return [_scale_length(value) for value in values[:2]]
|
|
|
|
|
|
def _scale_length(value: Any) -> float:
|
|
value = 0 if value is None else float(value)
|
|
return value * 1000 if abs(value) <= 10 else value
|
|
|
|
|
|
def _to_degrees(value: Any) -> float:
|
|
value = 0 if value is None else float(value)
|
|
return value * 180 / 3.141592653589793 if abs(value) <= 6.283185307179586 else value
|
|
|
|
|
|
def _unit3(vector: list[Any]) -> list[float]:
|
|
raw = [float(vector[i]) for i in range(3)]
|
|
length = math.sqrt(sum(v * v for v in raw))
|
|
if length <= 0:
|
|
return [0.0, 0.0, 0.0]
|
|
return [v / length for v in raw]
|
|
|
|
|
|
def _points_bbox(points: list[list[float]]) -> Optional[list[float]]:
|
|
if not points:
|
|
return None
|
|
return [
|
|
min(point[0] for point in points),
|
|
min(point[1] for point in points),
|
|
min(point[2] for point in points),
|
|
max(point[0] for point in points),
|
|
max(point[1] for point in points),
|
|
max(point[2] for point in points),
|
|
]
|
|
|
|
|
|
def _point_key(point: Any, places: int = 5) -> tuple[float, float] | None:
|
|
if not isinstance(point, list) or len(point) < 2:
|
|
return None
|
|
return (round(float(point[0]), places), round(float(point[1]), places))
|
|
|
|
|
|
def _rounded_point_key(point: list[Any], digits: int = 5) -> tuple[float, float, float]:
|
|
z = point[2] if len(point) > 2 else 0
|
|
return (round(float(point[0]), digits), round(float(point[1]), digits), round(float(z), digits))
|
|
|
|
|
|
def _dedupe_points(points: list[list[float]]) -> list[list[float]]:
|
|
result = []
|
|
seen = set()
|
|
for point in points:
|
|
key = _rounded_point_key(point)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
result.append(point)
|
|
return result
|
|
|
|
|
|
def _is_near_origin(point: list[float], tolerance: float = 1e-6) -> bool:
|
|
return math.sqrt(sum(float(component) * float(component) for component in point[:3])) <= tolerance
|
|
|
|
|
|
def _similar_bbox_size(a: list[float], b: list[float], tolerance: float = 0.05) -> bool:
|
|
return all(abs(float(a[i]) - float(b[i])) <= tolerance for i in range(3))
|
|
|
|
|
|
def _translated_bbox(bbox: list[float], offset: list[float]) -> list[float]:
|
|
return [
|
|
bbox[0] + offset[0],
|
|
bbox[1] + offset[1],
|
|
bbox[2] + offset[2],
|
|
bbox[3] + offset[0],
|
|
bbox[4] + offset[1],
|
|
bbox[5] + offset[2],
|
|
]
|
|
|
|
|
|
def _bbox_overflow_score(candidate: list[float], source: list[float]) -> float:
|
|
score = 0.0
|
|
for axis in range(3):
|
|
score += max(source[axis] - candidate[axis], 0)
|
|
score += max(candidate[axis + 3] - source[axis + 3], 0)
|
|
return score
|
|
|
|
|
|
def _bbox_center_distance_score(candidate: list[float], source: list[float]) -> float:
|
|
score = 0.0
|
|
for axis in range(3):
|
|
source_center = (source[axis] + source[axis + 3]) / 2
|
|
candidate_center = (candidate[axis] + candidate[axis + 3]) / 2
|
|
axis_size = max(source[axis + 3] - source[axis], 1.0)
|
|
score += abs(candidate_center - source_center) / axis_size
|
|
return score
|
|
|
|
|
|
def _bbox_area_2d(bbox: Optional[list[float]]) -> float:
|
|
if not isinstance(bbox, list) or len(bbox) < 4:
|
|
return 0.0
|
|
return max(0.0, float(bbox[2]) - float(bbox[0])) * max(0.0, float(bbox[3]) - float(bbox[1]))
|
|
|
|
|
|
def _bbox_contains_2d(outer: Optional[list[float]], inner: Optional[list[float]], tolerance: float = 1e-6) -> bool:
|
|
if not isinstance(outer, list) or not isinstance(inner, list) or len(outer) < 4 or len(inner) < 4:
|
|
return False
|
|
return (
|
|
float(outer[0]) <= float(inner[0]) + tolerance
|
|
and float(outer[1]) <= float(inner[1]) + tolerance
|
|
and float(outer[2]) >= float(inner[2]) - tolerance
|
|
and float(outer[3]) >= float(inner[3]) - tolerance
|
|
)
|
|
|
|
|
|
def _bbox_overlap_ratio_2d(a: Optional[list[float]], b: Optional[list[float]]) -> float:
|
|
if not isinstance(a, list) or not isinstance(b, list) or len(a) < 4 or len(b) < 4:
|
|
return 0.0
|
|
ix0 = max(float(a[0]), float(b[0]))
|
|
iy0 = max(float(a[1]), float(b[1]))
|
|
ix1 = min(float(a[2]), float(b[2]))
|
|
iy1 = min(float(a[3]), float(b[3]))
|
|
intersection = max(0.0, ix1 - ix0) * max(0.0, iy1 - iy0)
|
|
smaller = min(_bbox_area_2d(a), _bbox_area_2d(b))
|
|
if smaller <= 1e-9:
|
|
return 0.0
|
|
return intersection / smaller
|
|
|
|
|
|
def _loop_bbox(entities: list[dict[str, Any]]) -> Optional[list[float]]:
|
|
points = []
|
|
for ent in entities:
|
|
if not isinstance(ent, dict):
|
|
continue
|
|
if ent.get("type") == "circle":
|
|
center = ent.get("center")
|
|
radius = ent.get("radius_mm")
|
|
if isinstance(center, list) and len(center) >= 2 and radius is not None:
|
|
radius_value = abs(float(radius))
|
|
points.append([float(center[0]) - radius_value, float(center[1]) - radius_value])
|
|
points.append([float(center[0]) + radius_value, float(center[1]) + radius_value])
|
|
continue
|
|
for key in ("start", "end", "center"):
|
|
point = ent.get(key)
|
|
if isinstance(point, list) and len(point) >= 2:
|
|
points.append(point)
|
|
if not points:
|
|
return None
|
|
return [
|
|
min(float(point[0]) for point in points),
|
|
min(float(point[1]) for point in points),
|
|
max(float(point[0]) for point in points),
|
|
max(float(point[1]) for point in points),
|
|
]
|