Files
cdsl-cad/backend/engine/cdsl_engine/translator/common.py
T
ganjihong ad88d92ab9 refactor(cdsl_engine): split translator.py into the translator package
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.
2026-09-09 13:56:08 +08:00

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),
]