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.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
"""Compatibility package for the historical ``cdsl_engine.translator`` module.
|
||||
|
||||
The former single-file translator now lives in ``ir`` (SolidWorks plugin JSON
|
||||
to backend IR), ``codegen`` (backend IR to build123d source), ``runtime_lib``
|
||||
(frozen generated-script library), and ``common`` (shared helpers). Every
|
||||
historical import path and symbol keeps working.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .common import SW_END_CONDITIONS, THROUGH_CUT_AMOUNT_MM
|
||||
from .ir import (
|
||||
_SW_METADATA_FEATURE_TYPES,
|
||||
_append_feature_source_sketches,
|
||||
_axis_reference_from_feature_selections,
|
||||
_axis_reference_from_owned_faces,
|
||||
_best_extrude_depth_mm,
|
||||
_choose_pattern_direction_sign,
|
||||
_clean_null_reference,
|
||||
_construction_endpoint_degrees,
|
||||
_contour_entity_indices_from_segments,
|
||||
_convert_sw_assembly,
|
||||
_convert_sw_extrude,
|
||||
_convert_sw_hole,
|
||||
_convert_sw_imported_body,
|
||||
_convert_sw_linear_pattern,
|
||||
_convert_sw_reference,
|
||||
_convert_sw_revolve,
|
||||
_convert_sw_sketch,
|
||||
_convert_sw_sketch_entity,
|
||||
_editable_param,
|
||||
_extract_axis_reference,
|
||||
_extract_edge_selector_points,
|
||||
_extract_sketch_parameters,
|
||||
_extrude_depth_from_owned_faces,
|
||||
_extrude_owned_faces_span_sketch_plane,
|
||||
_feature_length_dimension_mm,
|
||||
_feature_selection_selectors,
|
||||
_feature_selection_source,
|
||||
_hole_angle_dimension_rad,
|
||||
_hole_counterbore_depth_dimension_mm,
|
||||
_hole_counterbore_dimension_mm,
|
||||
_hole_dimension_value,
|
||||
_hole_dimension_value_excluding,
|
||||
_hole_position_entity_flags,
|
||||
_hole_position_points,
|
||||
_hole_position_sketches,
|
||||
_hole_position_to_model,
|
||||
_hole_primary_depth_mm,
|
||||
_hole_primary_diameter_mm,
|
||||
_hole_primary_dimension_fallback,
|
||||
_host_face_from_feature_selections,
|
||||
_host_face_from_workplane,
|
||||
_is_cut_feature,
|
||||
_is_hole_profile_sketch,
|
||||
_is_imported_body_feature,
|
||||
_is_sketch_point_entity,
|
||||
_is_weak_inferred_axis,
|
||||
_operation_profile_bbox,
|
||||
_owned_face_pattern_offsets,
|
||||
_owned_face_signatures,
|
||||
_pattern_direction_from_plugin,
|
||||
_pattern_direction_from_reference,
|
||||
_reverse_pattern_direction,
|
||||
_selectable_stable_ids,
|
||||
_selection_objects,
|
||||
_selector_has_persistent_reference,
|
||||
_sketch_bounds,
|
||||
_sketch_point_mm,
|
||||
_sketch_point_to_model,
|
||||
_sketch_point_to_model_bbox,
|
||||
_sketch_radius_mm,
|
||||
_sketch_vector_to_model,
|
||||
_source_bbox_from_plugin_json,
|
||||
_source_feature,
|
||||
_source_owned_faces,
|
||||
_source_pattern_frame,
|
||||
analyze_parameterization_status,
|
||||
convert_sw_plugin_json_to_ir,
|
||||
enrich_rebuild_parameters,
|
||||
extract_editable_parameters,
|
||||
normalize_to_ir,
|
||||
)
|
||||
from .codegen import (
|
||||
_active_profile_loops,
|
||||
_aligned_workplane_for_owned_midplane,
|
||||
_blind_extrude_face_offset,
|
||||
_effective_extrude_cut_depth_mm,
|
||||
_effective_hole_cut_depth_mm,
|
||||
_extract_mirror_plane_normal,
|
||||
_extract_mirror_plane_origin,
|
||||
_find_operation_for_source_feature,
|
||||
_find_source_operation_for_pattern,
|
||||
_flip_side_step_inner_radius_mm,
|
||||
_flip_side_uses_step_ring,
|
||||
_generate_assembly_compose,
|
||||
_generate_chamfer,
|
||||
_generate_extrude,
|
||||
_generate_fillet,
|
||||
_generate_hole,
|
||||
_generate_imported_body_pending,
|
||||
_generate_linear_pattern,
|
||||
_generate_mirror_pattern,
|
||||
_generate_move_face,
|
||||
_generate_revolve,
|
||||
_generate_sketch,
|
||||
_hole_counterbore_depth_mm,
|
||||
_hole_counterbore_diameter_mm,
|
||||
_hole_countersink_angle_rad,
|
||||
_hole_countersink_diameter_mm,
|
||||
_hole_depth_mm,
|
||||
_hole_diameter_mm,
|
||||
_hole_drill_angle_rad,
|
||||
_hole_has_drill_tip,
|
||||
_hole_has_through_dimension,
|
||||
_hole_owned_cut_faces,
|
||||
_hole_should_use_sw_cut_holes,
|
||||
_infer_closed_wire_loops,
|
||||
_linear_pattern_offsets,
|
||||
_looks_like_reverse_history,
|
||||
_loop_area_from_radii,
|
||||
_loop_radius_candidates,
|
||||
_loops_matching_owned_radii,
|
||||
_model_offset_to_host_local,
|
||||
_operation_priority,
|
||||
_ordered_wire_entities,
|
||||
_owned_bbox_cut,
|
||||
_owned_cylindrical_cut_faces,
|
||||
_owned_extrude_terminal_offsets_mm,
|
||||
_owned_profile_radii_mm,
|
||||
_pattern_direction_vector,
|
||||
_prefer_blind_sketch_extrude,
|
||||
_project_owned_faces_to_sketch_bbox,
|
||||
_resolve_extrude_owned_termination,
|
||||
_reverse_curve_entity,
|
||||
_revolve_axis_expr,
|
||||
_sketch_circle_radii_mm,
|
||||
_sketch_construction_axis,
|
||||
_sketch_has_buildable_profile,
|
||||
_sketch_point_to_model_from_basis,
|
||||
_sw_math_transform_matrix,
|
||||
_translate_owned_faces,
|
||||
_translate_sketch_entities,
|
||||
_translated_operation,
|
||||
_translated_sketch,
|
||||
generate_build123d_code,
|
||||
get_part_name,
|
||||
sort_operations_for_history,
|
||||
)
|
||||
from .runtime_lib import RUNTIME_LIB_LINES
|
||||
|
||||
__all__ = [
|
||||
"RUNTIME_LIB_LINES",
|
||||
"SW_END_CONDITIONS",
|
||||
"THROUGH_CUT_AMOUNT_MM",
|
||||
"analyze_parameterization_status",
|
||||
"convert_sw_plugin_json_to_ir",
|
||||
"enrich_rebuild_parameters",
|
||||
"extract_editable_parameters",
|
||||
"generate_build123d_code",
|
||||
"get_part_name",
|
||||
"normalize_to_ir",
|
||||
"sort_operations_for_history",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,194 @@
|
||||
"""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),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,949 @@
|
||||
"""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",
|
||||
"",
|
||||
]
|
||||
Reference in New Issue
Block a user