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