Files
cdsl-cad/backend/engine/cdsl_engine/translator/runtime_lib.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

950 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Frozen runtime library embedded into generated build123d scripts.
These lines are appended after the per-model header (imports plus source
volume/area constants) in every generated script. The library is stable:
safe boolean wrappers, selector-based edge matching, and owned-face cutters.
Changes here affect every translator-generated rebuild.
"""
from __future__ import annotations
RUNTIME_LIB_LINES: list[str] = [
"",
"def _dist(a, b):",
" return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))",
"",
"def _owned_face_match_score(shape, expected_faces):",
" if not expected_faces:",
" return 0.0",
" try:",
" available = list(shape.faces())",
" except Exception:",
" return 1e99",
" total = 0.0",
" for expected in expected_faces:",
" bbox_m = expected.get('box_m')",
" if not bbox_m or len(bbox_m) < 6 or not available:",
" total += 1e6",
" continue",
" target_box = [float(v) * 1000 for v in bbox_m[:6]]",
" surface = expected.get('surface') or {}",
" target_type = next((name for name in ('plane', 'cylinder', 'cone', 'sphere', 'torus') if surface.get('is_' + name)), '')",
" target_area = float(expected.get('area_m2') or 0) * 1_000_000",
" ranked = []",
" for index, face in enumerate(available):",
" try:",
" fb = face.bounding_box()",
" face_box = [fb.min.X, fb.min.Y, fb.min.Z, fb.max.X, fb.max.Y, fb.max.Z]",
" geom = face.geom_type() if callable(face.geom_type) else face.geom_type",
" geom_name = getattr(geom, 'name', str(geom)).lower()",
" type_penalty = 0.0 if not target_type or target_type in geom_name else 1000.0",
" bbox_penalty = sum(abs(face_box[i] - target_box[i]) for i in range(6))",
" area_penalty = abs(float(face.area) - target_area) / max(math.sqrt(abs(target_area)), 1.0) if target_area else 0.0",
" ranked.append((type_penalty + bbox_penalty + area_penalty, index))",
" except Exception:",
" continue",
" if not ranked:",
" total += 1e6",
" continue",
" best, index = min(ranked, key=lambda item: item[0])",
" total += best",
" available.pop(index)",
" return total / max(len(expected_faces), 1)",
"",
"def _candidate_score(shape, expected_faces=None):",
" # Owned faces describe this exact SW history step. Final-part mass properties",
" # must not be used to choose an intermediate feature candidate.",
" if expected_faces:",
" return _owned_face_match_score(shape, expected_faces)",
" score = 0",
" if SOURCE_VOLUME_MM3 is not None:",
" try:",
" score += abs(float(shape.volume) - SOURCE_VOLUME_MM3)",
" except Exception:",
" score += 1e99",
" if SOURCE_AREA_MM2 is not None:",
" try:",
" score += abs(float(shape.area) - SOURCE_AREA_MM2) * 0.01",
" except Exception:",
" score += 1e99",
" score += _owned_face_match_score(shape, expected_faces)",
" return score",
"",
"def _edge_endpoints(edge):",
" vertices = [v.to_tuple() for v in edge.vertices()]",
" if len(vertices) != 2:",
" center = edge.center().to_tuple()",
" return center, center",
" return vertices[0], vertices[1]",
"",
"def _edge_match_score(edge, start, end):",
" a, b = _edge_endpoints(edge)",
" endpoint_score = min(_dist(a, start) + _dist(b, end), _dist(a, end) + _dist(b, start))",
" containment_score = edge.distance_to(start) + edge.distance_to(end)",
" return min(endpoint_score, containment_score)",
"",
"def select_edges_by_endpoints(part, selector_points, tolerance=0.5):",
" edges = list(part.edges())",
" selected = []",
" used = set()",
" for selector in selector_points:",
" start, end = selector",
" ranked = sorted(((_edge_match_score(edge, start, end), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" score, index, edge = ranked[0]",
" if score > tolerance:",
" raise ValueError(f\"No edge matched selector {selector}; best score={score:.4f} mm\")",
" if index not in used:",
" selected.append(edge)",
" used.add(index)",
" return selected",
"",
"def _bbox_match_score(edge, bbox_mm):",
" if not bbox_mm or len(bbox_mm) < 6:",
" return float('inf')",
" try:",
" a, b = _edge_endpoints(edge)",
" mid = tuple((a[i] + b[i]) / 2 for i in range(3))",
" mins = tuple(float(bbox_mm[i]) for i in range(3))",
" maxs = tuple(float(bbox_mm[i + 3]) for i in range(3))",
" diag = math.sqrt(sum((maxs[i] - mins[i]) ** 2 for i in range(3)))",
" pad = max(0.25, diag * 0.15)",
" def point_score(point):",
" total = 0.0",
" for axis in range(3):",
" if point[axis] < mins[axis] - pad:",
" total += mins[axis] - pad - point[axis]",
" elif point[axis] > maxs[axis] + pad:",
" total += point[axis] - maxs[axis] - pad",
" return total",
" return min(point_score(mid), (point_score(a) + point_score(b)) / 2)",
" except Exception:",
" return float('inf')",
"",
"def _circle_match_score(edge, circle_params):",
" if not circle_params or len(circle_params) < 7:",
" return float('inf')",
" try:",
" geom_type = edge.geom_type() if callable(edge.geom_type) else edge.geom_type",
" geom_name = getattr(geom_type, 'name', str(geom_type))",
" if 'CIRCLE' not in geom_name:",
" return float('inf')",
" target_center = tuple(float(v) * 1000 for v in circle_params[:3])",
" target_radius = float(circle_params[6]) * 1000",
" edge_center = edge.arc_center.to_tuple()",
" return _dist(edge_center, target_center) + abs(edge.radius - target_radius)",
" except Exception:",
" return float('inf')",
"",
"def _line_match_score(edge, line_params):",
" if not line_params or len(line_params) < 6:",
" return float('inf')",
" try:",
" geom_type = edge.geom_type() if callable(edge.geom_type) else edge.geom_type",
" geom_name = getattr(geom_type, 'name', str(geom_type))",
" if 'LINE' not in geom_name:",
" return float('inf')",
" target_point = tuple(float(v) * 1000 for v in line_params[:3])",
" target_dir = tuple(float(v) for v in line_params[3:6])",
" a, b = _edge_endpoints(edge)",
" edge_dir_raw = tuple(b[i] - a[i] for i in range(3))",
" length = math.sqrt(sum(v * v for v in edge_dir_raw))",
" if length <= 0:",
" return float('inf')",
" edge_dir = tuple(v / length for v in edge_dir_raw)",
" parallel = 1 - abs(sum(edge_dir[i] * target_dir[i] for i in range(3)))",
" distance = edge.distance_to(target_point)",
" return distance + parallel * 10",
" except Exception:",
" return float('inf')",
"",
"def select_edges_by_selectors(part, selectors, tolerance=0.5):",
" if part is None:",
" return []",
" edges = list(part.edges())",
" selected = []",
" used = set()",
" for selector in selectors or []:",
" geometry = selector.get('geometry', {})",
" start_vertex = geometry.get('start_vertex')",
" end_vertex = geometry.get('end_vertex')",
" start = start_vertex.get('point_m') if start_vertex else None",
" end = end_vertex.get('point_m') if end_vertex else None",
" bbox_mm = geometry.get('bbox_mm')",
" if start and end:",
" start_mm = tuple(float(v) * 1000 for v in start)",
" end_mm = tuple(float(v) * 1000 for v in end)",
" line_params = geometry.get('curve', {}).get('line_params')",
" if line_params:",
" ranked = sorted(((min(_edge_match_score(edge, start_mm, end_mm), _line_match_score(edge, line_params)) + (_bbox_match_score(edge, bbox_mm) if bbox_mm else 0), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" else:",
" ranked = sorted(((_edge_match_score(edge, start_mm, end_mm) + (_bbox_match_score(edge, bbox_mm) if bbox_mm else 0), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" else:",
" line_params = geometry.get('curve', {}).get('line_params')",
" circle_params = geometry.get('curve', {}).get('circle_params')",
" if line_params:",
" ranked = sorted(((_line_match_score(edge, line_params) + (_bbox_match_score(edge, bbox_mm) if bbox_mm else 0), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" elif bbox_mm:",
" ranked = sorted(((_bbox_match_score(edge, bbox_mm), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" else:",
" ranked = sorted(((_circle_match_score(edge, circle_params), i, edge) for i, edge in enumerate(edges)), key=lambda item: item[0])",
" score, index, edge = ranked[0]",
" selector_tolerance = float(selector.get('tolerance_mm') or tolerance)",
" if score > selector_tolerance:",
" # Skip edges that don't match well enough",
" continue",
" if index not in used:",
" selected.append(edge)",
" used.add(index)",
" return selected",
"",
"def _point_inside_bbox(point, bbox_mm, pad=0.25):",
" return all(float(bbox_mm[i]) - pad <= point[i] <= float(bbox_mm[i + 3]) + pad for i in range(3))",
"",
"def fillet_edges_from_owned_surface_bbox(part, selectors):",
" if part is None:",
" return []",
" boxes = []",
" seen_boxes = set()",
" for selector in selectors or []:",
" if selector.get('source') not in ('owned_cylindrical_face_axis', 'owned_face_bbox'):",
" continue",
" bbox = (selector.get('geometry') or {}).get('bbox_mm')",
" if bbox and len(bbox) >= 6:",
" normalized = [float(v) for v in bbox[:6]]",
" key = tuple(round(v, 6) for v in normalized)",
" if key not in seen_boxes:",
" seen_boxes.add(key)",
" boxes.append(normalized)",
" if len(boxes) < 2:",
" return []",
" selected = []",
" used_keys = set()",
" for box in boxes:",
" diag = math.sqrt(sum((box[i + 3] - box[i]) ** 2 for i in range(3)))",
" pad = max(0.25, diag * 0.08)",
" sizes = [abs(box[i + 3] - box[i]) for i in range(3)]",
" thin_axes = [i for i, size in enumerate(sizes) if size <= max(1.5, diag * 0.08)]",
" circle_candidates = []",
" if thin_axes:",
" thin_axis = thin_axes[0]",
" for edge in part.edges():",
" try:",
" geom_type = edge.geom_type() if callable(edge.geom_type) else edge.geom_type",
" geom_name = getattr(geom_type, 'name', str(geom_type))",
" if 'CIRCLE' not in geom_name:",
" continue",
" eb = edge.bounding_box()",
" edge_box = [eb.min.X, eb.min.Y, eb.min.Z, eb.max.X, eb.max.Y, eb.max.Z]",
" ok = True",
" score = 0.0",
" for axis in range(3):",
" if axis == thin_axis:",
" plane_delta = min(abs(edge_box[axis] - box[axis]), abs(edge_box[axis] - box[axis + 3]), abs(edge_box[axis + 3] - box[axis]), abs(edge_box[axis + 3] - box[axis + 3]))",
" if plane_delta > pad:",
" ok = False",
" break",
" score += plane_delta",
" else:",
" if edge_box[axis] < box[axis] - pad or edge_box[axis + 3] > box[axis + 3] + pad:",
" ok = False",
" break",
" score += abs(edge_box[axis] - box[axis]) + abs(edge_box[axis + 3] - box[axis + 3])",
" if not ok:",
" continue",
" key = tuple(round(v, 5) for v in edge_box)",
" circle_candidates.append((score, key, edge))",
" except Exception:",
" continue",
" if circle_candidates:",
" circle_candidates.sort(key=lambda item: item[0])",
" for _, key, edge in circle_candidates:",
" if key in used_keys:",
" continue",
" used_keys.add(key)",
" selected.append(edge)",
" break",
" continue",
" box_candidates = []",
" for edge in part.edges():",
" try:",
" geom_type = edge.geom_type() if callable(edge.geom_type) else edge.geom_type",
" geom_name = getattr(geom_type, 'name', str(geom_type))",
" if 'LINE' not in geom_name:",
" continue",
" a, b = _edge_endpoints(edge)",
" mid = tuple((a[i] + b[i]) / 2 for i in range(3))",
" if not (_point_inside_bbox(a, box, pad) and _point_inside_bbox(b, box, pad) and _point_inside_bbox(mid, box, pad)):",
" continue",
" key = tuple(round(v, 5) for point in (a, b) for v in point)",
" box_candidates.append((float(edge.length), key, edge))",
" except Exception:",
" continue",
" if not box_candidates:",
" continue",
" box_candidates.sort(key=lambda item: item[0], reverse=True)",
" for _, key, edge in box_candidates:",
" reverse_key = key[3:] + key[:3]",
" if key in used_keys or reverse_key in used_keys:",
" continue",
" used_keys.add(key)",
" selected.append(edge)",
" break",
" if selected:",
" return selected",
" union_bbox = [",
" min(box[i] for box in boxes) if i < 3 else max(box[i] for box in boxes)",
" for i in range(6)",
" ]",
" diag = math.sqrt(sum((union_bbox[i + 3] - union_bbox[i]) ** 2 for i in range(3)))",
" pad = max(0.25, diag * 0.05)",
" candidates = []",
" for edge in part.edges():",
" try:",
" geom_type = edge.geom_type() if callable(edge.geom_type) else edge.geom_type",
" geom_name = getattr(geom_type, 'name', str(geom_type))",
" if 'LINE' not in geom_name:",
" continue",
" a, b = _edge_endpoints(edge)",
" mid = tuple((a[i] + b[i]) / 2 for i in range(3))",
" if not (_point_inside_bbox(a, union_bbox, pad) and _point_inside_bbox(b, union_bbox, pad) and _point_inside_bbox(mid, union_bbox, pad)):",
" continue",
" candidates.append((float(edge.length), edge))",
" except Exception:",
" continue",
" if not candidates:",
" return []",
" candidates.sort(key=lambda item: item[0], reverse=True)",
" return [candidates[0][1]]",
"",
"def fillet_with_tolerance(edges, radius):",
" radii = [float(radius)]",
" shrink = max(0.001, abs(float(radius)) * 0.001)",
" if float(radius) > shrink:",
" radii.append(float(radius) - shrink)",
" radii.append(float(radius) * 0.99)",
" last_error = None",
" for candidate_radius in radii:",
" if candidate_radius <= 0:",
" continue",
" try:",
" return fillet(edges, radius=candidate_radius)",
" except Exception as exc:",
" last_error = exc",
" continue",
" if last_error:",
" raise last_error",
" return fillet(edges, radius=radius)",
"",
"def fillet_selected(part, radius, selectors, owned_faces=None):",
" if part is None:",
" return part",
" if not selectors:",
" # No edge selectors - skip fillet to avoid failing on all edges",
" return part",
" candidates = []",
" owned_edges = fillet_edges_from_owned_surface_bbox(part, selectors)",
" if owned_edges:",
" try:",
" candidates.append(fillet_with_tolerance(owned_edges, radius))",
" except Exception:",
" pass",
" try:",
" target_edges = select_edges_by_selectors(part, selectors)",
" if target_edges:",
" candidates.append(fillet_with_tolerance(target_edges, radius))",
" except Exception:",
" pass",
" result = part",
" applied_any = False",
" for selector in selectors:",
" edges = select_edges_by_selectors(result, [selector])",
" if not edges:",
" continue # Skip selectors that don't match any edge",
" try:",
" result = fillet_with_tolerance([edges[0]], radius)",
" applied_any = True",
" except Exception:",
" # OCC fillets are fragile: one invalid edge/radius should not abort the whole rebuild.",
" continue",
" if applied_any:",
" candidates.append(result)",
" variants = []",
" for selector in selectors:",
" edges = select_edges_by_selectors(part, [selector])",
" if not edges:",
" continue",
" try:",
" variants.append(fillet_with_tolerance([edges[0]], radius))",
" except Exception:",
" continue",
" if variants:",
" try:",
" union_result = part",
" for variant in variants:",
" union_result = union_result + variant",
" candidates.append(union_result)",
" except Exception:",
" pass",
" try:",
" intersection_result = part",
" for variant in variants:",
" intersection_result = intersection_result & variant",
" candidates.append(intersection_result)",
" except Exception:",
" pass",
" if candidates:",
" return sorted(candidates, key=lambda shape: _candidate_score(shape, owned_faces))[0]",
" return part",
"",
"def chamfer_selected(part, distance, selectors, owned_faces=None):",
" if part is None:",
" return part",
" if not selectors:",
" # No edge selectors available - chamfer would fail on all edges",
" return part",
" candidates = []",
" owned_edges = fillet_edges_from_owned_surface_bbox(part, selectors)",
" if owned_edges:",
" try:",
" candidates.append(chamfer(owned_edges, length=distance))",
" except Exception:",
" pass",
" target_edges = select_edges_by_selectors(part, selectors)",
" if target_edges:",
" try:",
" candidates.append(chamfer(target_edges, length=distance))",
" except Exception:",
" pass",
" result = part",
" applied_any = False",
" for selector in selectors:",
" edges = select_edges_by_selectors(result, [selector])",
" if not edges:",
" continue",
" try:",
" result = chamfer([edges[0]], length=distance)",
" applied_any = True",
" except Exception:",
" continue",
" if applied_any:",
" candidates.append(result)",
" if candidates:",
" return sorted(candidates, key=lambda shape: _candidate_score(shape, owned_faces))[0]",
" return part",
"",
"def is_internal_cone_face(face, part):",
" try:",
" bbox_m = face.get('box_m')",
" surface = face.get('surface') or {}",
" if not (bbox_m and len(bbox_m) >= 6 and surface.get('is_cone')):",
" return False",
" params = surface.get('cone_params')",
" if not params or len(params) < 6:",
" return False",
" direction = tuple(float(v) for v in params[3:6])",
" axis = max(range(3), key=lambda i: abs(direction[i]))",
" radial_axes = tuple(i for i in range(3) if i != axis)",
" part_bbox = part.bounding_box()",
" part_min = part_bbox.min.to_tuple()",
" part_max = part_bbox.max.to_tuple()",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" tol = 0.25",
" touches_outer = any(",
" abs(mins[i] - part_min[i]) <= tol or abs(maxs[i] - part_max[i]) <= tol",
" for i in radial_axes",
" )",
" return not touches_outer",
" except Exception:",
" return False",
"",
"def is_external_cone_face(face, part):",
" try:",
" bbox_m = face.get('box_m')",
" surface = face.get('surface') or {}",
" if not (bbox_m and len(bbox_m) >= 6 and surface.get('is_cone')):",
" return False",
" params = surface.get('cone_params')",
" if not params or len(params) < 6:",
" return False",
" direction = tuple(float(v) for v in params[3:6])",
" axis = max(range(3), key=lambda i: abs(direction[i]))",
" radial_axes = tuple(i for i in range(3) if i != axis)",
" part_bbox = part.bounding_box()",
" part_min = part_bbox.min.to_tuple()",
" part_max = part_bbox.max.to_tuple()",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" tol = 0.25",
" return any(",
" abs(mins[i] - part_min[i]) <= tol or abs(maxs[i] - part_max[i]) <= tol",
" for i in radial_axes",
" )",
" except Exception:",
" return False",
"",
"def make_owned_external_cone_chamfer_cutter(face):",
" surface = face.get('surface') or {}",
" params = surface.get('cone_params')",
" bbox_m = face.get('box_m')",
" if not params or len(params) < 8 or not bbox_m or len(bbox_m) < 6:",
" return None",
" origin = tuple(float(v) * 1000 for v in params[:3])",
" direction = tuple(float(v) for v in params[3:6])",
" norm = math.sqrt(sum(v * v for v in direction))",
" base_radius = abs(float(params[6]) * 1000)",
" half_angle = abs(float(params[7]))",
" if norm <= 1e-9 or base_radius <= 1e-9 or half_angle <= 1e-9:",
" return None",
" direction = tuple(v / norm for v in direction)",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" projections = []",
" for x in (mins[0], maxs[0]):",
" for y in (mins[1], maxs[1]):",
" for z in (mins[2], maxs[2]):",
" delta = (x - origin[0], y - origin[1], z - origin[2])",
" axial = sum(delta[i] * direction[i] for i in range(3))",
" projections.append(axial)",
" start = min(projections)",
" end = max(projections)",
" height = max(0.001, end - start)",
" r1 = max(0.0, base_radius - math.tan(half_angle) * start)",
" r2 = max(0.0, base_radius - math.tan(half_angle) * end)",
" outer_radius = max(r1, r2) + 0.001",
" center_offset = (start + end) / 2",
" center = tuple(origin[i] + direction[i] * center_offset for i in range(3))",
" if r1 <= 1e-9:",
" r1 = 1e-6",
" if r2 <= 1e-9:",
" r2 = 1e-6",
" with BuildPart(Plane(origin=center, z_dir=direction)) as cutter_part:",
" Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.CENTER))",
" Cone(r1, r2, height + 0.002, align=(Align.CENTER, Align.CENTER, Align.CENTER), mode=Mode.SUBTRACT)",
" return cutter_part.part",
"",
"def chamfer_owned_external_cones(part, faces):",
" if part is None:",
" return part, False",
" result = part",
" applied = False",
" for face in faces or []:",
" if not is_external_cone_face(face, result):",
" continue",
" cutter = make_owned_external_cone_chamfer_cutter(face)",
" new_result = safe_subtract(result, cutter)",
" if new_result is not result:",
" result = new_result",
" applied = True",
" return result, applied",
"",
"def chamfer_owned_internal_cones(part, faces):",
" if part is None:",
" return part, False",
" result = part",
" applied = False",
" for face in faces or []:",
" if not is_internal_cone_face(face, result):",
" continue",
" cutter = make_owned_cone_cutter(face)",
" new_result = safe_subtract(result, cutter)",
" if new_result is not result:",
" result = new_result",
" applied = True",
" return result, applied",
"",
"def chamfer_selected_with_owned_faces(part, distance, selectors, owned_faces):",
" cone_faces = [face for face in (owned_faces or []) if (face.get('surface') or {}).get('is_cone')]",
" if len(cone_faces) == 1 and is_internal_cone_face(cone_faces[0], part):",
" result, applied = chamfer_owned_internal_cones(part, cone_faces)",
" if applied:",
" return result",
" if len(cone_faces) == 1 and is_external_cone_face(cone_faces[0], part):",
" result, applied = chamfer_owned_external_cones(part, cone_faces)",
" if applied:",
" return result",
" return chamfer_selected(part, distance, selectors, owned_faces)",
"",
"def safe_subtract(part, cutter):",
" if part is None or cutter is None:",
" return part",
" try:",
" vol_before = float(part.volume)",
" except Exception:",
" vol_before = -1",
" try:",
" cut = part - cutter",
" if cut is None:",
" print(f' SUBTRACT: cutter resulted in None, keeping original (vol={vol_before:.0f})')",
" return part",
" # Accept the cut even when solids() reports 0 can happen",
" # for valid boolean results with non-standard structures.",
" try:",
" nb_solids = len(list(cut.solids()))",
" if nb_solids == 0:",
" print(f' SUBTRACT: cut produced 0 solids (still accepting) vol={vol_before:.0f}')",
" except Exception:",
" pass",
" return cut",
" except Exception as e:",
" print(f' SUBTRACT: exception {type(e).__name__}: {e}, keeping original (vol={vol_before:.0f})')",
" return part",
"",
"def _project_bbox_along_direction(bbox, origin, direction):",
" mins = tuple(float(bbox[i]) for i in range(3))",
" maxs = tuple(float(bbox[i + 3]) for i in range(3))",
" projections = []",
" for x in (mins[0], maxs[0]):",
" for y in (mins[1], maxs[1]):",
" for z in (mins[2], maxs[2]):",
" projections.append(sum(((x, y, z)[i] - origin[i]) * direction[i] for i in range(3)))",
" return min(projections), max(projections)",
"",
"def make_owned_cylinder_cutter(face, target_part=None):",
" surface = face.get('surface') or {}",
" params = surface.get('cylinder_params')",
" bbox_m = face.get('box_m')",
" if not params or len(params) < 7 or not bbox_m or len(bbox_m) < 6:",
" return None",
" origin = tuple(float(v) * 1000 for v in params[:3])",
" direction = tuple(float(v) for v in params[3:6])",
" norm = math.sqrt(sum(v * v for v in direction))",
" radius = abs(float(params[6]) * 1000)",
" if norm <= 1e-9 or radius <= 1e-9:",
" return None",
" direction = tuple(v / norm for v in direction)",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" start, end = _project_bbox_along_direction((*mins, *maxs), origin, direction)",
" if target_part is not None:",
" try:",
" part_bbox = target_part.bounding_box()",
" part_box = (*part_bbox.min.to_tuple(), *part_bbox.max.to_tuple())",
" part_start, part_end = _project_bbox_along_direction(part_box, origin, direction)",
" through_tolerance = max(1.0, radius * 0.12)",
" if abs(start - part_start) <= through_tolerance:",
" start = part_start",
" if abs(end - part_end) <= through_tolerance:",
" end = part_end",
" except Exception:",
" pass",
" height = max(0.001, end - start)",
" center_offset = (start + end) / 2",
" center = tuple(origin[i] + direction[i] * center_offset for i in range(3))",
" with BuildPart(Plane(origin=center, z_dir=direction)) as cutter_part:",
" Cylinder(radius, height, align=(Align.CENTER, Align.CENTER, Align.CENTER))",
" return cutter_part.part",
"",
"def make_owned_cone_cutter(face):",
" surface = face.get('surface') or {}",
" params = surface.get('cone_params')",
" bbox_m = face.get('box_m')",
" if not params or len(params) < 8 or not bbox_m or len(bbox_m) < 6:",
" return None",
" origin = tuple(float(v) * 1000 for v in params[:3])",
" direction = tuple(float(v) for v in params[3:6])",
" norm = math.sqrt(sum(v * v for v in direction))",
" base_radius = abs(float(params[6]) * 1000)",
" half_angle = abs(float(params[7]))",
" if norm <= 1e-9 or base_radius <= 1e-9 or half_angle <= 1e-9:",
" return None",
" direction = tuple(v / norm for v in direction)",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" projections = []",
" for x in (mins[0], maxs[0]):",
" for y in (mins[1], maxs[1]):",
" for z in (mins[2], maxs[2]):",
" projections.append(sum(((x, y, z)[i] - origin[i]) * direction[i] for i in range(3)))",
" start = min(projections)",
" end = max(projections)",
" # Keep a tiny overlap for the boolean while preserving blind-hole depth.",
" height = max(0.001, end - start) + 0.001",
" # SolidWorks ConeParams stores the radius at the cone origin; along the axis",
" # direction the radius tapers rather than expands for hole drill tips.",
" r1 = max(0.0, base_radius - math.tan(half_angle) * start)",
" r2 = max(0.0, base_radius - math.tan(half_angle) * end)",
" if max(r1, r2) <= 1e-9:",
" return None",
" if r1 <= 1e-9:",
" r1 = 1e-6",
" if r2 <= 1e-9:",
" r2 = 1e-6",
" center_offset = (start + end) / 2",
" center = tuple(origin[i] + direction[i] * center_offset for i in range(3))",
" with BuildPart(Plane(origin=center, z_dir=direction)) as cutter_part:",
" Cone(r1, r2, height, align=(Align.CENTER, Align.CENTER, Align.CENTER))",
" return cutter_part.part",
"",
"def make_owned_face_cutter(face, target_part=None):",
" surface = face.get('surface') or {}",
" if surface.get('is_cylinder'):",
" return make_owned_cylinder_cutter(face, target_part)",
" if surface.get('is_cone'):",
" return make_owned_cone_cutter(face)",
" return None",
"",
"def cut_owned_cylindrical_faces(part, faces):",
" result = part",
" for face in faces or []:",
" cutter = make_owned_face_cutter(face, result)",
" result = safe_subtract(result, cutter)",
" return result",
"",
"def make_owned_flip_side_ring_cutter(face, target_part):",
" surface = face.get('surface') or {}",
" params = surface.get('cylinder_params')",
" bbox_m = face.get('box_m')",
" if target_part is None or not params or len(params) < 7 or not bbox_m or len(bbox_m) < 6:",
" return None",
" origin = tuple(float(v) * 1000 for v in params[:3])",
" direction = tuple(float(v) for v in params[3:6])",
" norm = math.sqrt(sum(v * v for v in direction))",
" inner_radius = abs(float(params[6]) * 1000)",
" if norm <= 1e-9 or inner_radius <= 1e-9:",
" return None",
" direction = tuple(v / norm for v in direction)",
" mins = tuple(float(bbox_m[i]) * 1000 for i in range(3))",
" maxs = tuple(float(bbox_m[i + 3]) * 1000 for i in range(3))",
" start, end = _project_bbox_along_direction((*mins, *maxs), origin, direction)",
" height = max(0.001, end - start)",
" center_offset = (start + end) / 2",
" center = tuple(origin[i] + direction[i] * center_offset for i in range(3))",
" try:",
" part_bbox = target_part.bounding_box()",
" part_min = part_bbox.min.to_tuple()",
" part_max = part_bbox.max.to_tuple()",
" radial = []",
" for x in (part_min[0], part_max[0]):",
" for y in (part_min[1], part_max[1]):",
" for z in (part_min[2], part_max[2]):",
" delta = (x - origin[0], y - origin[1], z - origin[2])",
" axial = sum(delta[i] * direction[i] for i in range(3))",
" perp = tuple(delta[i] - axial * direction[i] for i in range(3))",
" radial.append(math.sqrt(sum(v * v for v in perp)))",
" outer_radius = max(radial) + max(1.0, inner_radius * 0.05)",
" except Exception:",
" outer_radius = inner_radius + 100.0",
" if outer_radius <= inner_radius + 1e-6:",
" return None",
" with BuildPart(Plane(origin=center, z_dir=direction)) as cutter_part:",
" Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.CENTER))",
" Cylinder(inner_radius, height + 0.002, align=(Align.CENTER, Align.CENTER, Align.CENTER), mode=Mode.SUBTRACT)",
" return cutter_part.part",
"",
"def cut_owned_flip_side_cylindrical_faces(part, faces):",
" result = part",
" for face in faces or []:",
" cutter = make_owned_flip_side_ring_cutter(face, result)",
" result = safe_subtract(result, cutter)",
" return result",
"",
"def cut_owned_bbox(part, bbox_mm):",
" if part is None or not bbox_mm or len(bbox_mm) < 6:",
" return part",
" mins = tuple(float(bbox_mm[i]) for i in range(3))",
" maxs = tuple(float(bbox_mm[i + 3]) for i in range(3))",
" size = tuple(max(0.001, maxs[i] - mins[i]) for i in range(3))",
" center = tuple((mins[i] + maxs[i]) / 2 for i in range(3))",
" cutter = Pos(center) * Box(size[0], size[1], size[2])",
" return safe_subtract(part, cutter)",
"",
"def shape_face_count(shape):",
" if shape is None:",
" return 0",
" try:",
" return len(list(shape.faces()))",
" except Exception:",
" return 0",
"",
"def safe_union(part, solid, preserve_visible=False):",
" if part is None:",
" return solid",
" if solid is None:",
" return part",
" try:",
" fused = part + solid",
" # OCCT fuse succeeded; always return the fused result.",
" # is_valid() can return False for edge cases where the geometry",
" # is actually correct (e.g. touching-at-faces). Accept it.",
" return fused",
" except Exception as e:",
" print(f' UNION: fuse threw {type(e).__name__}: {e}')",
" pass",
" try:",
" compound = Compound.make_composite([part, solid])",
" fused = compound.fuse()",
" try:",
" if len(list(fused.solids())) > 0:",
" print(f' UNION: compound.fuse() worked, {len(list(fused.solids()))} solids')",
" return fused",
" except Exception:",
" pass",
" except Exception as e:",
" print(f' UNION: compound.fuse() threw {type(e).__name__}: {e}')",
" pass",
" shapes = []",
" try:",
" shapes.extend(list(part.solids()))",
" except Exception:",
" shapes.append(part)",
" try:",
" shapes.extend(list(solid.solids()))",
" except Exception:",
" shapes.append(solid)",
" return Compound.make_composite(shapes)",
"",
"def sw_inverted_profile_cut(part, profile_solid, normal):",
" if part is None or profile_solid is None:",
" return part",
" try:",
" part_bbox = part.bounding_box()",
" profile_bbox = profile_solid.bounding_box()",
" n = tuple(float(v) for v in normal)",
" axis = max(range(3), key=lambda i: abs(n[i]))",
" part_min = part_bbox.min.to_tuple()",
" part_max = part_bbox.max.to_tuple()",
" prof_min = profile_bbox.min.to_tuple()",
" prof_max = profile_bbox.max.to_tuple()",
" margin = 5.0",
" mins = [part_min[i] - margin for i in range(3)]",
" maxs = [part_max[i] + margin for i in range(3)]",
" mins[axis] = prof_min[axis] - margin * 0.05",
" maxs[axis] = prof_max[axis] + margin * 0.05",
" center = tuple((mins[i] + maxs[i]) / 2 for i in range(3))",
" size = tuple(max(0.001, maxs[i] - mins[i]) for i in range(3))",
" envelope = Pos(center) * Box(size[0], size[1], size[2])",
" outside_profile = safe_subtract(envelope, profile_solid)",
" return safe_subtract(part, outside_profile)",
" except Exception:",
" return part",
"",
"def sw_flip_side_step_cut(part, profile_solid, normal, outer_radius_mm, inner_radius_mm):",
" part = sw_inverted_profile_cut(part, profile_solid, normal)",
" if part is None or profile_solid is None:",
" return part",
" try:",
" outer_radius = abs(float(outer_radius_mm))",
" inner_radius = abs(float(inner_radius_mm))",
" except Exception:",
" return part",
" if outer_radius <= inner_radius + 1e-6:",
" return part",
" try:",
" profile_bbox = profile_solid.bounding_box()",
" prof_min = profile_bbox.min.to_tuple()",
" prof_max = profile_bbox.max.to_tuple()",
" center = tuple((prof_min[i] + prof_max[i]) / 2 for i in range(3))",
" n = tuple(float(v) for v in normal)",
" axis = max(range(3), key=lambda i: abs(n[i]))",
" span_xy = max(prof_max[0] - prof_min[0], prof_max[1] - prof_min[1])",
" margin_xy = max(2.0, span_xy * 0.05)",
" margin_z = 0.1",
" size = tuple(",
" max(0.001, prof_max[i] - prof_min[i] + (margin_xy if i < 2 else margin_z))",
" for i in range(3)",
" )",
" plane = Plane(",
" origin=center,",
" x_dir=(1.0, 0.0, 0.0) if axis != 0 else (0.0, 1.0, 0.0),",
" z_dir=n,",
" )",
" cut_extent = prof_max[axis] - prof_min[axis]",
" cut_amount = -abs(cut_extent) if n[axis] < 0 else abs(cut_extent)",
" with BuildSketch(plane) as ring_sketch:",
" Circle(outer_radius)",
" Circle(inner_radius, mode=Mode.SUBTRACT)",
" ring = extrude(ring_sketch.sketch, amount=cut_amount)",
" return safe_union(part, ring)",
" except Exception:",
" return part",
"",
"def sw_cut_holes(part, positions, host_face, diameter, depth, drill_angle=0, include_drill_tip=False, countersink_diameter=0, countersink_angle=0, counterbore_diameter=0, counterbore_depth=0):",
" if part is None:",
" return part",
" if not positions or diameter <= 0 or depth <= 0:",
" return part",
" plane = host_face.get('surface', {}).get('plane_params') or [0, 0, 1, 0, 0, 0]",
" frame = host_face.get('frame') or {}",
" normal = tuple(float(v) for v in plane[:3])",
" plane_point = tuple(float(v) * 1000 for v in plane[3:6])",
" origin = tuple(float(v) for v in frame.get('origin_mm', plane_point))",
" x_dir = tuple(float(v) for v in frame.get('x_dir', (0, 0, 0)))",
" y_dir = tuple(float(v) for v in frame.get('y_dir', (0, 0, 0)))",
" has_frame = sum(abs(v) for v in x_dir) > 0 and sum(abs(v) for v in y_dir) > 0",
" bbox = part.bounding_box()",
" part_center = tuple((bbox.min.to_tuple()[i] + bbox.max.to_tuple()[i]) / 2 for i in range(3))",
" toward_center = tuple(part_center[i] - plane_point[i] for i in range(3))",
" dot = sum(toward_center[i] * normal[i] for i in range(3))",
" inward = normal if dot >= 0 else tuple(-v for v in normal)",
" axis = max(range(3), key=lambda i: abs(inward[i]))",
" rotation = (0, 0, 0)",
" if axis == 0:",
" rotation = (0, 90, 0) if inward[0] >= 0 else (0, -90, 0)",
" elif axis == 1:",
" rotation = (-90, 0, 0) if inward[1] >= 0 else (90, 0, 0)",
" elif inward[2] < 0:",
" rotation = (180, 0, 0)",
" tip_depth = 0",
" if include_drill_tip and drill_angle > 0:",
" tip_depth = (diameter / 2) / math.tan(drill_angle / 2)",
" countersink_depth = 0",
" if countersink_diameter > diameter and countersink_angle > 0:",
" countersink_depth = ((countersink_diameter - diameter) / 2) / math.tan(countersink_angle / 2)",
" result = part",
" for pos in positions:",
" x, y = float(pos[0]), float(pos[1])",
" if has_frame:",
" start = tuple(origin[i] + x_dir[i] * x + y_dir[i] * y for i in range(3))",
" elif axis == 0:",
" start = (plane_point[0], x, y)",
" elif axis == 1:",
" start = (x, plane_point[1], -y)",
" else:",
" start = (x, y, plane_point[2])",
" cut_depth = depth",
" if depth >= 199:",
" part_min = bbox.min.to_tuple()",
" part_max = bbox.max.to_tuple()",
" corners = []",
" for ci in range(2):",
" for cj in range(2):",
" for ck in range(2):",
" corners.append((",
" part_min[0] if ci else part_max[0],",
" part_min[1] if cj else part_max[1],",
" part_min[2] if ck else part_max[2],",
" ))",
" cut_depth = max(",
" sum((corner[i] - start[i]) * inward[i] for i in range(3))",
" for corner in corners",
" ) + 2.0",
" cutters = []",
" cb_depth = counterbore_depth if counterbore_diameter > diameter and counterbore_depth > 0 else 0",
" cs_depth = countersink_depth if countersink_depth > 0 else 0",
" hole_start = cs_depth",
" hole_depth = max(0.001, cut_depth - hole_start - cb_depth)",
" if hole_depth > 0:",
" hole_center = tuple(start[i] + inward[i] * (hole_start + cb_depth + hole_depth / 2) for i in range(3))",
" cutters.append(Pos(hole_center) * Cylinder(diameter / 2, hole_depth, rotation=rotation))",
" if cb_depth > 0:",
" cb_center = tuple(start[i] + inward[i] * (hole_start + cb_depth / 2) for i in range(3))",
" cutters.append(Pos(cb_center) * Cylinder(counterbore_diameter / 2, cb_depth, rotation=rotation))",
" if cs_depth > 0:",
" cs_center = tuple(start[i] + inward[i] * cs_depth / 2 for i in range(3))",
" cs = Pos(cs_center) * Cone(countersink_diameter / 2, diameter / 2, cs_depth, rotation=rotation)",
" cutters.append(cs)",
" if tip_depth > 0:",
" base = tuple(start[i] + inward[i] * cut_depth for i in range(3))",
" tip_center = tuple(base[i] + inward[i] * tip_depth / 2 for i in range(3))",
" tip = Pos(tip_center) * Cone(diameter / 2, 0, tip_depth, rotation=rotation)",
" cutters.append(tip)",
" if len(cutters) == 1:",
" cutter = cutters[0]",
" else:",
" cutter = Compound.make_composite(cutters)",
" result = safe_subtract(result, cutter)",
" return result",
"",
]