ad88d92ab9
Phase 5 of the decoupling refactor (behavior-preserving move): - translator/ir.py: SolidWorks plugin JSON to backend-IR conversion (70 syms) - translator/codegen.py: backend IR to build123d source generation (64 syms) - translator/runtime_lib.py: frozen generated-script runtime library, spliced into generate_build123d_code as *RUNTIME_LIB_LINES - translator/common.py: helpers shared by both sides - translator/__init__.py: full historical symbol surface re-exported Generated-code equivalence verified byte-for-byte against the pre-split output for a representative IR sample; py_compile clean.
1960 lines
85 KiB
Python
1960 lines
85 KiB
Python
"""SolidWorks plugin JSON to backend-IR conversion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
from copy import deepcopy
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .common import (
|
|
SW_END_CONDITIONS,
|
|
THROUGH_CUT_AMOUNT_MM,
|
|
_point_m_to_mm,
|
|
_scale_point,
|
|
_scale_length,
|
|
_to_degrees,
|
|
_unit3,
|
|
_points_bbox,
|
|
_rounded_point_key,
|
|
_dedupe_points,
|
|
_is_near_origin,
|
|
_similar_bbox_size,
|
|
_translated_bbox,
|
|
_bbox_overflow_score,
|
|
_bbox_center_distance_score,
|
|
_bbox_area_2d,
|
|
_loop_bbox,
|
|
)
|
|
|
|
|
|
def normalize_to_ir(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Normalize supported input formats to the backend internal IR."""
|
|
if "operations" in data and "sketches" in data:
|
|
return enrich_rebuild_parameters(data)
|
|
|
|
if "features" in data:
|
|
return enrich_rebuild_parameters(convert_sw_plugin_json_to_ir(data))
|
|
|
|
raise ValueError("Unsupported JSON format: expected internal IR or SW plugin features JSON")
|
|
|
|
def enrich_rebuild_parameters(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Add a generic editable-parameter index without changing feature history.
|
|
|
|
The returned rebuild JSON remains the source of truth for execution. The
|
|
`editable_parameters` section is an index of JSON paths that a UI or caller
|
|
can modify safely while preserving the original feature order and links.
|
|
"""
|
|
enriched = dict(data)
|
|
enriched["editable_parameters"] = extract_editable_parameters(enriched)
|
|
enriched["parameterization_status"] = analyze_parameterization_status(enriched)
|
|
return enriched
|
|
|
|
def analyze_parameterization_status(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
issues = []
|
|
|
|
for sketch in data.get("sketches", []):
|
|
host_reference = sketch.get("host_reference", {})
|
|
reference = host_reference.get("reference") or {}
|
|
if reference.get("kind") == "face" and not reference.get("owner_feature"):
|
|
issues.append({
|
|
"kind": "missing_stable_face_owner",
|
|
"sketch": {"id": sketch.get("id"), "name": sketch.get("name")},
|
|
"message": (
|
|
"Sketch is attached to a face geometry, but the JSON does not identify "
|
|
"the owning feature/face id. Parameter edits may require updating this "
|
|
"sketch workplane manually unless the plugin exports stable face ownership."
|
|
),
|
|
})
|
|
|
|
for op in data.get("operations", []):
|
|
if op.get("type") in ("unsupported", "unknown"):
|
|
sw_type = op.get("parameters", {}).get("sw_type") or op.get("type")
|
|
issues.append({
|
|
"kind": "unsupported_geometry_feature",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": sw_type},
|
|
"message": (
|
|
f"SolidWorks feature '{sw_type}' is present in the history, but the "
|
|
"core build123d translator has no generic implementation for it. "
|
|
"The feature is retained in IR and must not be treated as a complete rebuild."
|
|
),
|
|
})
|
|
|
|
if op.get("type") == "hole":
|
|
host_face = op.get("parameters", {}).get("host_face") or {}
|
|
if host_face and not host_face.get("frame"):
|
|
issues.append({
|
|
"kind": "missing_hole_host_frame",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"message": (
|
|
"Hole feature has a host face, but the JSON does not include the "
|
|
"face-local x/y axes. The translator can infer common axis-aligned "
|
|
"cases, but the plugin should export the sketch/face frame for exact "
|
|
"generic hole placement."
|
|
),
|
|
})
|
|
|
|
if op.get("type") == "extrude_cut":
|
|
end_code = op.get("parameters", {}).get("end_condition_code")
|
|
if end_code in (3, 4, 5, 7, 9):
|
|
params = op.get("parameters", {})
|
|
has_termination_reference = any(
|
|
params.get(key)
|
|
for key in (
|
|
"end_condition_reference",
|
|
"reverse_end_condition_reference",
|
|
"termination_reference",
|
|
)
|
|
)
|
|
kind = (
|
|
"sw_end_condition_requires_exact_translator"
|
|
if has_termination_reference
|
|
else "missing_extrude_termination_reference"
|
|
)
|
|
issues.append({
|
|
"kind": kind,
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"end_condition_code": end_code,
|
|
"end_condition": SW_END_CONDITIONS.get(end_code),
|
|
"message": (
|
|
"This SW cut uses a non-blind end condition. ThroughAll can be "
|
|
"replayed generically, but ThroughNext/UpTo-style rebuilds need the "
|
|
"selected terminating face/body/reference from the plugin for exact 1:1."
|
|
),
|
|
})
|
|
|
|
if op.get("type") in ("revolve_cut", "revolve_add"):
|
|
axis_reference = op.get("parameters", {}).get("axis_reference")
|
|
if not axis_reference or not (
|
|
isinstance(axis_reference, dict)
|
|
and axis_reference.get("origin_mm")
|
|
and axis_reference.get("direction")
|
|
):
|
|
axis_candidates = op.get("parameters", {}).get("axis_candidates") or []
|
|
if axis_candidates:
|
|
issues.append({
|
|
"kind": "revolve_axis_inferred_from_candidate",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"message": (
|
|
"Revolve feature lacks the original SolidWorks selected axis, but "
|
|
"the translator can use a construction-line candidate. For exact "
|
|
"auditability the plugin should still export the selected axis "
|
|
"reference and selection mark."
|
|
),
|
|
})
|
|
continue
|
|
issues.append({
|
|
"kind": "missing_revolve_axis_reference",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"message": (
|
|
"Revolve feature does not include the SolidWorks selected axis. "
|
|
"The translator can only infer an axis from the sketch workplane, "
|
|
"which is not reliable enough for exact 1:1 rebuild."
|
|
),
|
|
})
|
|
|
|
if op.get("type") in ("linear_pattern", "pattern_linear"):
|
|
params = op.get("parameters", {})
|
|
if not params.get("source_features") or not _linear_pattern_offsets(op):
|
|
issues.append({
|
|
"kind": "linear_pattern_missing_source_or_direction",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"message": (
|
|
"This SW linear pattern lacks source-feature selection or direction data. "
|
|
"The translator can replay patterns when source features and offsets are "
|
|
"available; otherwise the plugin should export the selected feature list "
|
|
"and pattern direction references."
|
|
),
|
|
})
|
|
|
|
if op.get("type") in ("fillet", "chamfer"):
|
|
selectors = op.get("selectors") or []
|
|
if selectors and not any(_selector_has_persistent_reference(selector) for selector in selectors):
|
|
issues.append({
|
|
"kind": "missing_original_feature_selection",
|
|
"feature": {"id": op.get("id"), "name": op.get("name"), "type": op.get("type")},
|
|
"message": (
|
|
"This feature only has final-geometry edge signatures. For exact replay "
|
|
"the plugin should export the original SolidWorks feature selections "
|
|
"including persistent references and selection marks."
|
|
),
|
|
})
|
|
|
|
return {
|
|
"safe_to_edit": not issues,
|
|
"issues": issues,
|
|
}
|
|
|
|
def _selector_has_persistent_reference(selector: Dict[str, Any]) -> bool:
|
|
stack = [selector]
|
|
while stack:
|
|
value = stack.pop()
|
|
if isinstance(value, dict):
|
|
if value.get("persistent_reference"):
|
|
return True
|
|
stack.extend(value.values())
|
|
elif isinstance(value, list):
|
|
stack.extend(value)
|
|
return False
|
|
|
|
def extract_editable_parameters(data: Dict[str, Any]) -> list[Dict[str, Any]]:
|
|
parameters: list[Dict[str, Any]] = []
|
|
sketches = {sketch.get("id"): sketch for sketch in data.get("sketches", [])}
|
|
|
|
for op_index, op in enumerate(data.get("operations", [])):
|
|
op_type = op.get("type", "")
|
|
op_name = op.get("name", op.get("id", f"operation_{op_index}"))
|
|
op_path = f"/operations/{op_index}"
|
|
params = op.get("parameters", {})
|
|
|
|
if op_type in ("extrude_add", "extrude_cut") and "distance_mm" in params:
|
|
semantic = "body_length" if op_type == "extrude_add" else "cut_depth"
|
|
parameters.append(_editable_param(
|
|
id=f"{op.get('id', op_index)}.distance_mm",
|
|
label=f"{op_name} distance",
|
|
semantic=semantic,
|
|
unit="mm",
|
|
value=params.get("distance_mm"),
|
|
path=f"{op_path}/parameters/distance_mm",
|
|
feature=op,
|
|
))
|
|
|
|
if "reverse_distance_mm" in params:
|
|
parameters.append(_editable_param(
|
|
id=f"{op.get('id', op_index)}.reverse_distance_mm",
|
|
label=f"{op_name} reverse distance",
|
|
semantic="reverse_depth",
|
|
unit="mm",
|
|
value=params.get("reverse_distance_mm"),
|
|
path=f"{op_path}/parameters/reverse_distance_mm",
|
|
feature=op,
|
|
))
|
|
|
|
if op_type in ("fillet", "chamfer"):
|
|
key = "radius_mm" if op_type == "fillet" else "distance_mm"
|
|
if key in params:
|
|
parameters.append(_editable_param(
|
|
id=f"{op.get('id', op_index)}.{key}",
|
|
label=f"{op_name} {key}",
|
|
semantic="fillet_radius" if op_type == "fillet" else "chamfer_distance",
|
|
unit="mm",
|
|
value=params.get(key),
|
|
path=f"{op_path}/parameters/{key}",
|
|
feature=op,
|
|
))
|
|
|
|
sketch_id = op.get("sketch")
|
|
sketch = sketches.get(sketch_id)
|
|
if sketch:
|
|
parameters.extend(_extract_sketch_parameters(sketch, sketch_id, op, op_index, data))
|
|
|
|
return parameters
|
|
|
|
def _extract_sketch_parameters(
|
|
sketch: Dict[str, Any],
|
|
sketch_id: str,
|
|
op: Dict[str, Any],
|
|
op_index: int,
|
|
data: Dict[str, Any],
|
|
) -> list[Dict[str, Any]]:
|
|
parameters: list[Dict[str, Any]] = []
|
|
sketch_index = next((i for i, item in enumerate(data.get("sketches", [])) if item.get("id") == sketch_id), None)
|
|
if sketch_index is None:
|
|
return parameters
|
|
|
|
op_type = op.get("type", "")
|
|
entities = sketch.get("entities", [])
|
|
drawable = [entity for entity in entities if not entity.get("construction", False)]
|
|
|
|
for entity_index, entity in enumerate(entities):
|
|
entity_type = entity.get("type")
|
|
entity_path = f"/sketches/{sketch_index}/entities/{entity_index}"
|
|
|
|
if entity_type in ("circle", "arc") and entity.get("is_circle", entity_type == "circle"):
|
|
center = entity.get("center", [0, 0, 0])
|
|
radius = entity.get("radius_mm")
|
|
semantic = "hole" if op_type == "extrude_cut" else "circle_profile"
|
|
if radius is not None:
|
|
parameters.append(_editable_param(
|
|
id=f"{sketch_id}.entity{entity_index}.radius_mm",
|
|
label=f"{sketch.get('name', sketch_id)} circle radius",
|
|
semantic=f"{semantic}_radius",
|
|
unit="mm",
|
|
value=radius,
|
|
path=f"{entity_path}/radius_mm",
|
|
feature=op,
|
|
))
|
|
for axis, value in zip(("x", "y"), center[:2]):
|
|
parameters.append(_editable_param(
|
|
id=f"{sketch_id}.entity{entity_index}.center_{axis}",
|
|
label=f"{sketch.get('name', sketch_id)} {semantic} center {axis}",
|
|
semantic=f"{semantic}_center_{axis}",
|
|
unit="mm",
|
|
value=value,
|
|
path=f"{entity_path}/center/{0 if axis == 'x' else 1}",
|
|
feature=op,
|
|
))
|
|
|
|
bounds = _sketch_bounds(drawable)
|
|
if bounds:
|
|
min_x, min_y, max_x, max_y = bounds
|
|
center_x = (min_x + max_x) / 2
|
|
center_y = (min_y + max_y) / 2
|
|
width = max_x - min_x
|
|
height = max_y - min_y
|
|
semantic_prefix = "slot" if op_type == "extrude_cut" else "profile"
|
|
for suffix, value, semantic in (
|
|
("center_x", center_x, f"{semantic_prefix}_center_x"),
|
|
("center_y", center_y, f"{semantic_prefix}_center_y"),
|
|
("width", width, f"{semantic_prefix}_width"),
|
|
("height", height, f"{semantic_prefix}_height"),
|
|
):
|
|
parameters.append(_editable_param(
|
|
id=f"{sketch_id}.{suffix}",
|
|
label=f"{sketch.get('name', sketch_id)} {suffix}",
|
|
semantic=semantic,
|
|
unit="mm",
|
|
value=value,
|
|
path=f"/sketches/{sketch_index}",
|
|
feature=op,
|
|
editable=False,
|
|
note="Derived from sketch entity bounds; edit underlying entities to change this safely.",
|
|
))
|
|
|
|
workplane = sketch.get("workplane", {})
|
|
origin = workplane.get("origin_mm")
|
|
if origin:
|
|
for axis, value in zip(("x", "y", "z"), origin[:3]):
|
|
parameters.append(_editable_param(
|
|
id=f"{sketch_id}.workplane_origin_{axis}",
|
|
label=f"{sketch.get('name', sketch_id)} workplane origin {axis}",
|
|
semantic=f"sketch_plane_origin_{axis}",
|
|
unit="mm",
|
|
value=value,
|
|
path=f"/sketches/{sketch_index}/workplane/origin_mm/{'xyz'.index(axis)}",
|
|
feature=op,
|
|
))
|
|
|
|
return parameters
|
|
|
|
def _sketch_bounds(entities: list[Dict[str, Any]]) -> Optional[tuple[float, float, float, float]]:
|
|
points: list[tuple[float, float]] = []
|
|
for entity in entities:
|
|
for key in ("start", "end", "center"):
|
|
point = entity.get(key)
|
|
if point and len(point) >= 2:
|
|
points.append((float(point[0]), float(point[1])))
|
|
radius = entity.get("radius_mm")
|
|
center = entity.get("center")
|
|
if radius is not None and center and len(center) >= 2:
|
|
cx, cy = float(center[0]), float(center[1])
|
|
r = float(radius)
|
|
points.extend([(cx - r, cy - r), (cx + r, cy + r)])
|
|
if not points:
|
|
return None
|
|
xs = [point[0] for point in points]
|
|
ys = [point[1] for point in points]
|
|
return min(xs), min(ys), max(xs), max(ys)
|
|
|
|
def _editable_param(
|
|
*,
|
|
id: str,
|
|
label: str,
|
|
semantic: str,
|
|
unit: str,
|
|
value: Any,
|
|
path: str,
|
|
feature: Dict[str, Any],
|
|
editable: bool = True,
|
|
note: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
result = {
|
|
"id": id,
|
|
"label": label,
|
|
"semantic": semantic,
|
|
"unit": unit,
|
|
"value": value,
|
|
"path": path,
|
|
"editable": editable,
|
|
"feature": {
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": feature.get("type"),
|
|
"source_index": feature.get("source_feature", {}).get("index"),
|
|
},
|
|
}
|
|
if note:
|
|
result["note"] = note
|
|
return result
|
|
|
|
def convert_sw_plugin_json_to_ir(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Convert the current SW plugin feature dump into the backend IR."""
|
|
features = data.get("features", [])
|
|
sketches = []
|
|
operations = []
|
|
last_sketch_id = None
|
|
last_build_op = None
|
|
references = []
|
|
source_bbox = _source_bbox_from_plugin_json(data)
|
|
|
|
if data.get("document_kind") == "assembly" and isinstance(data.get("assembly_data"), dict):
|
|
operations.append(_convert_sw_assembly(data))
|
|
part_name = data.get("part_name", "part")
|
|
return {
|
|
"version": "ir-0.1",
|
|
"metadata": {
|
|
"source": {
|
|
"format": "sw-plugin-json",
|
|
"file_name": f"{part_name}.sldasm",
|
|
"sw_version": data.get("sw_version"),
|
|
}
|
|
},
|
|
"sketches": sketches,
|
|
"operations": operations,
|
|
"references": references,
|
|
"validation_hints": data.get("validation_hints", {}),
|
|
"geometry_inventory": data.get("geometry_inventory", {}),
|
|
"rebuild_contract": data.get("rebuild_contract", {}),
|
|
}
|
|
|
|
for index, feature in enumerate(features):
|
|
if feature.get("is_suppressed"):
|
|
continue
|
|
|
|
feature_type = feature.get("type", "")
|
|
type_name = feature.get("type_name", "")
|
|
feature_id = feature.get("id") or f"feat_{index:03d}"
|
|
feature_name = feature.get("name", feature_id)
|
|
|
|
if feature_type in ("refplane", "refaxis"):
|
|
references.append(_convert_sw_reference(feature, index))
|
|
elif feature_type == "sketch":
|
|
sketch_id = f"sketch_{len(sketches):03d}"
|
|
sketches.append(_convert_sw_sketch(feature, sketch_id, index))
|
|
last_sketch_id = sketch_id
|
|
elif feature_type in ("extrude", "ice", "cut") and isinstance(feature.get("extrude_data"), dict):
|
|
sketch_ref = _append_feature_source_sketches(feature, sketches, index) or last_sketch_id
|
|
op = _convert_sw_extrude(feature, type_name, sketch_ref, index)
|
|
operations.append(op)
|
|
last_build_op = op
|
|
elif feature_type == "revolve":
|
|
sketch_ref = _append_feature_source_sketches(feature, sketches, index) or last_sketch_id
|
|
op = _convert_sw_revolve(feature, type_name, sketch_ref, index)
|
|
operations.append(op)
|
|
last_build_op = op
|
|
elif feature_type == "hole":
|
|
op = _convert_sw_hole(feature, index)
|
|
operations.append(op)
|
|
last_build_op = op
|
|
elif feature_type == "pattern_linear":
|
|
data_block = feature.get("linear_pattern_data", {})
|
|
source_op = _find_source_operation_for_pattern(operations, data_block.get("source_features") or [])
|
|
source_frame = _source_pattern_frame(source_op or last_build_op, sketches)
|
|
operations.append(_convert_sw_linear_pattern(feature, index, source_op or last_build_op, source_frame, sketches, source_bbox))
|
|
elif feature_type == "pattern_mirror":
|
|
data_block = feature.get("mirror_data") or {}
|
|
src_features = data_block.get("source_features") or []
|
|
mirror_origin = data_block.get("mirror_plane_origin")
|
|
mirror_normal = data_block.get("mirror_plane_normal")
|
|
operations.append({
|
|
"id": feature_id,
|
|
"name": feature_name,
|
|
"type": "pattern_mirror",
|
|
"parameters": {"source_features": src_features},
|
|
"raw_parameters": {
|
|
"mirror_plane_origin": mirror_origin,
|
|
"mirror_plane_normal": mirror_normal,
|
|
},
|
|
"source_feature": _source_feature(feature, index),
|
|
})
|
|
elif feature_type == "fillet":
|
|
data_block = feature.get("fillet_data", {})
|
|
radius_mm = data_block.get("radius") or _feature_length_dimension_mm(feature)
|
|
operations.append({
|
|
"id": feature_id,
|
|
"name": feature_name,
|
|
"type": "fillet",
|
|
"parameters": {"radius_mm": radius_mm},
|
|
"selectors": _feature_selection_selectors(feature, data_block),
|
|
"selection_source": _feature_selection_source(feature, data_block),
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
})
|
|
elif feature_type == "chamfer":
|
|
data_block = feature.get("chamfer_data", {})
|
|
distance_mm = data_block.get("distance") or _feature_length_dimension_mm(feature)
|
|
operations.append({
|
|
"id": feature_id,
|
|
"name": feature_name,
|
|
"type": "chamfer",
|
|
"parameters": {"distance_mm": distance_mm},
|
|
"selectors": _feature_selection_selectors(feature, data_block),
|
|
"selection_source": _feature_selection_source(feature, data_block),
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
})
|
|
elif _is_imported_body_feature(feature):
|
|
op = _convert_sw_imported_body(feature, index)
|
|
operations.append(op)
|
|
last_build_op = op
|
|
elif feature_type == "moveface":
|
|
data_block = feature.get("move_face_data") if isinstance(feature.get("move_face_data"), dict) else {}
|
|
op = {
|
|
"id": feature_id,
|
|
"name": feature_name,
|
|
"type": "move_face",
|
|
"parameters": {"sw_type": type_name or feature_type, "move_face_data": data_block},
|
|
"source_feature": _source_feature(feature, index),
|
|
}
|
|
operations.append(op)
|
|
last_build_op = op
|
|
elif feature_type not in _SW_METADATA_FEATURE_TYPES:
|
|
operations.append({
|
|
"id": feature_id,
|
|
"name": feature_name,
|
|
"type": "unsupported",
|
|
"parameters": {"sw_type": type_name or feature_type},
|
|
"source_feature": _source_feature(feature, index),
|
|
})
|
|
|
|
part_name = data.get("part_name", "part")
|
|
return {
|
|
"version": "ir-0.1",
|
|
"metadata": {
|
|
"source": {
|
|
"format": "sw-plugin-json",
|
|
"file_name": f"{part_name}.sldprt",
|
|
"sw_version": data.get("sw_version"),
|
|
}
|
|
},
|
|
"sketches": sketches,
|
|
"operations": operations,
|
|
"references": references,
|
|
"validation_hints": data.get("validation_hints", {}),
|
|
"geometry_inventory": data.get("geometry_inventory", {}),
|
|
"rebuild_contract": data.get("rebuild_contract", {}),
|
|
}
|
|
|
|
def _is_imported_body_feature(feature: Dict[str, Any]) -> bool:
|
|
feature_type = str(feature.get("type") or "").lower()
|
|
type_name = str(feature.get("type_name") or "").lower()
|
|
return bool(feature.get("imported_body_data")) or feature_type in {
|
|
"mbimport",
|
|
"savedextbody",
|
|
"importedbody",
|
|
"imported",
|
|
"stock",
|
|
} or type_name in {"mbimport", "savedextbody", "importedbody"}
|
|
|
|
def _convert_sw_imported_body(feature: Dict[str, Any], index: int) -> Dict[str, Any]:
|
|
data_block = feature.get("imported_body_data") if isinstance(feature.get("imported_body_data"), dict) else {}
|
|
solid_bodies = data_block.get("solid_bodies") or []
|
|
solid_body_stats = data_block.get("solid_body_stats") or []
|
|
source_name = feature.get("name")
|
|
parameters = {
|
|
"sw_type": feature.get("type_name") or feature.get("type"),
|
|
"source_name": source_name,
|
|
"history_status": data_block.get("history_status"),
|
|
"body_count": len(solid_bodies) if isinstance(solid_bodies, list) else len(solid_body_stats),
|
|
"solid_body_stats": solid_body_stats,
|
|
"solid_bodies": solid_bodies,
|
|
}
|
|
return {
|
|
"id": feature.get("id") or f"feat_{index:03d}",
|
|
"name": feature.get("name") or f"imported_body_{index:03d}",
|
|
"type": "imported_body",
|
|
"parameters": parameters,
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_imported_body": data_block,
|
|
}
|
|
|
|
def _convert_sw_assembly(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
assembly_data = data.get("assembly_data") or {}
|
|
components = []
|
|
for index, component in enumerate(assembly_data.get("components") or []):
|
|
if component.get("is_suppressed") or component.get("is_hidden"):
|
|
continue
|
|
path = component.get("path") or ""
|
|
component_name = component.get("name") or f"component_{index:03d}"
|
|
base_name = os.path.splitext(os.path.basename(str(path).replace("\\", "/")))[0] or component_name
|
|
components.append({
|
|
"index": index,
|
|
"name": component_name,
|
|
"component_id": base_name,
|
|
"source_path": path,
|
|
"component_json": f"{base_name}.solidworks_rebuild_extract.json",
|
|
"transform": component.get("transform") or {},
|
|
})
|
|
return {
|
|
"id": "assembly_000",
|
|
"name": data.get("part_name") or "assembly",
|
|
"type": "assembly_compose",
|
|
"parameters": {
|
|
"components": components,
|
|
},
|
|
"source_feature": {"index": 0, "name": data.get("part_name"), "type": "assembly"},
|
|
}
|
|
|
|
def _append_feature_source_sketches(feature: Dict[str, Any], sketches: list[Dict[str, Any]], index: int) -> Optional[str]:
|
|
"""Promote feature-owned SW sketches into the rebuild sketch table."""
|
|
source_sketches = []
|
|
for block_name in ("extrude_data", "revolve_data"):
|
|
block = feature.get(block_name)
|
|
if isinstance(block, dict):
|
|
source_sketches.extend(sketch for sketch in (block.get("source_sketches") or []) if isinstance(sketch, dict))
|
|
|
|
if not source_sketches:
|
|
return None
|
|
|
|
last_id = None
|
|
for sketch_data in source_sketches:
|
|
sketch_id = f"sketch_{len(sketches):03d}"
|
|
sketch_feature = dict(feature)
|
|
sketch_feature["sketch_data"] = sketch_data
|
|
if sketch_data.get("name"):
|
|
sketch_feature["name"] = sketch_data.get("name")
|
|
sketches.append(_convert_sw_sketch(sketch_feature, sketch_id, index))
|
|
last_id = sketch_id
|
|
return last_id
|
|
|
|
def _hole_dimension_value(data_block: Dict[str, Any], tokens: tuple[str, ...]) -> Optional[float]:
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
name = str(dim.get("name") or "").lower()
|
|
if all(token.lower() in name for token in tokens) and dim.get("value") not in (None, ""):
|
|
return float(dim.get("value"))
|
|
return None
|
|
|
|
def _feature_length_dimension_mm(feature: Dict[str, Any]) -> Optional[float]:
|
|
candidates: list[tuple[int, float]] = []
|
|
for dim in feature.get("dimensions") or []:
|
|
if not isinstance(dim, dict):
|
|
continue
|
|
name = str(dim.get("name") or "")
|
|
system_value = dim.get("system_value_m")
|
|
if system_value not in (None, ""):
|
|
length_mm = abs(float(system_value)) * 1000
|
|
elif dim.get("value") not in (None, ""):
|
|
length_mm = abs(float(dim.get("value")))
|
|
else:
|
|
continue
|
|
if length_mm <= 1e-9 or length_mm > 500:
|
|
continue
|
|
priority = 0 if name.startswith("D1@") else 1
|
|
candidates.append((priority, length_mm))
|
|
if not candidates:
|
|
return None
|
|
candidates.sort(key=lambda item: (item[0], item[1]))
|
|
return candidates[0][1]
|
|
|
|
def _feature_selection_selectors(
|
|
feature: Dict[str, Any],
|
|
data_block: Optional[Dict[str, Any]] = None,
|
|
) -> list[Dict[str, Any]]:
|
|
selectors: list[Dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
sources = []
|
|
if isinstance(data_block, dict):
|
|
sources.extend(data_block.get("selections") or [])
|
|
sources.extend(feature.get("selections") or [])
|
|
|
|
for selection in sources:
|
|
if not isinstance(selection, dict) or selection.get("kind") != "selection":
|
|
continue
|
|
geometry = selection.get("object")
|
|
if not isinstance(geometry, dict):
|
|
continue
|
|
kind = geometry.get("kind")
|
|
if kind not in ("edge", "face"):
|
|
continue
|
|
identity = geometry.get("identity") if isinstance(geometry.get("identity"), dict) else {}
|
|
stable_key = (
|
|
geometry.get("stable_id")
|
|
or geometry.get("persistent_reference")
|
|
or identity.get("stable_id")
|
|
or identity.get("persistent_reference")
|
|
or json.dumps(geometry, sort_keys=True, ensure_ascii=False, default=str)
|
|
)
|
|
if stable_key in seen:
|
|
continue
|
|
seen.add(str(stable_key))
|
|
selectors.append({
|
|
"kind": kind,
|
|
"geometry": geometry,
|
|
"mark": selection.get("mark"),
|
|
"source_feature": {
|
|
"name": selection.get("feature_name"),
|
|
"type_name": selection.get("feature_type_name"),
|
|
},
|
|
})
|
|
if selectors:
|
|
return selectors
|
|
|
|
for face in feature.get("owned_faces") or []:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
surface = face.get("surface") if isinstance(face.get("surface"), dict) else {}
|
|
cylinder_params = surface.get("cylinder_params")
|
|
if not (surface.get("is_cylinder") and isinstance(cylinder_params, list) and len(cylinder_params) >= 7):
|
|
continue
|
|
radius_mm = abs(float(cylinder_params[6]) * 1000)
|
|
line_params = [
|
|
float(cylinder_params[0]),
|
|
float(cylinder_params[1]),
|
|
float(cylinder_params[2]),
|
|
float(cylinder_params[3]),
|
|
float(cylinder_params[4]),
|
|
float(cylinder_params[5]),
|
|
]
|
|
stable_key = f"owned_cylinder:{','.join(f'{value:.9g}' for value in line_params)}:{radius_mm:.6g}"
|
|
if stable_key in seen:
|
|
continue
|
|
seen.add(stable_key)
|
|
selectors.append({
|
|
"kind": "edge",
|
|
"geometry": {
|
|
"kind": "edge",
|
|
"curve": {
|
|
"kind": "curve",
|
|
"is_line": True,
|
|
"line_params": line_params,
|
|
},
|
|
"bbox_mm": [float(value) * 1000 for value in face.get("box_m", [])[:6]]
|
|
if isinstance(face.get("box_m"), list) and len(face.get("box_m")) >= 6
|
|
else None,
|
|
},
|
|
"tolerance_mm": max(0.5, radius_mm * 2.5),
|
|
"source": "owned_cylindrical_face_axis",
|
|
})
|
|
for face in feature.get("owned_faces") or []:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
box_m = face.get("box_m")
|
|
if not (isinstance(box_m, list) and len(box_m) >= 6):
|
|
continue
|
|
bbox_mm = [float(value) * 1000 for value in box_m[:6]]
|
|
if any(not math.isfinite(value) for value in bbox_mm):
|
|
continue
|
|
sizes = [abs(bbox_mm[i + 3] - bbox_mm[i]) for i in range(3)]
|
|
stable_key = f"owned_face_bbox:{','.join(f'{value:.9g}' for value in bbox_mm)}"
|
|
if stable_key in seen:
|
|
continue
|
|
seen.add(stable_key)
|
|
selectors.append({
|
|
"kind": "edge",
|
|
"geometry": {
|
|
"kind": "edge",
|
|
"bbox_mm": bbox_mm,
|
|
},
|
|
"tolerance_mm": max(0.5, min(max(sizes), 10.0) * 0.35),
|
|
"source": "owned_face_bbox",
|
|
})
|
|
return selectors
|
|
|
|
def _feature_selection_source(
|
|
feature: Dict[str, Any],
|
|
data_block: Optional[Dict[str, Any]] = None,
|
|
) -> str:
|
|
sources = []
|
|
if isinstance(data_block, dict):
|
|
sources.extend(data_block.get("selections") or [])
|
|
sources.extend(feature.get("selections") or [])
|
|
if any(isinstance(item, dict) and item.get("kind") == "selection" for item in sources):
|
|
return "solidworks_original_selection"
|
|
if feature.get("owned_faces"):
|
|
return "post_feature_owned_face_inference"
|
|
return "missing"
|
|
|
|
def _hole_dimension_value_excluding(
|
|
data_block: Dict[str, Any],
|
|
tokens: tuple[str, ...],
|
|
excluded: tuple[str, ...] = (),
|
|
) -> Optional[float]:
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
name = str(dim.get("name") or "").lower()
|
|
if excluded and any(token.lower() in name for token in excluded):
|
|
continue
|
|
if all(token.lower() in name for token in tokens) and dim.get("value") not in (None, ""):
|
|
return float(dim.get("value"))
|
|
return None
|
|
|
|
def _hole_primary_dimension_fallback(data_block: Dict[str, Any], prefer_small: bool) -> Optional[float]:
|
|
values = []
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
name = str(dim.get("name") or "").lower()
|
|
if not any(token in name for token in ("孔", "hole", "螺", "thread")):
|
|
continue
|
|
if any(token in name for token in ("沉头", "锥", "counter", "csk", "导头", "angle", "角度")):
|
|
continue
|
|
value = dim.get("value")
|
|
if value in (None, ""):
|
|
continue
|
|
number = abs(float(value))
|
|
if 0 < number < 200:
|
|
values.append(number)
|
|
if not values:
|
|
return None
|
|
return min(values) if prefer_small else max(values)
|
|
|
|
def _hole_primary_diameter_mm(data_block: Dict[str, Any]) -> float:
|
|
diameter = (
|
|
_hole_dimension_value_excluding(data_block, ("tap", "drill", "dia"), ("depth", "angle"))
|
|
or _hole_dimension_value_excluding(data_block, ("tap", "drill", "diameter"), ("depth", "angle"))
|
|
or _hole_dimension_value_excluding(data_block, ("螺纹孔钻头", "直径"), ("深度", "角度"))
|
|
or _hole_dimension_value_excluding(data_block, ("钻头", "直径"), ("深度", "角度"))
|
|
or _hole_dimension_value_excluding(data_block, ("通孔", "孔直径"), ("沉头", "锥", "counter", "csk", "角度", "深度"))
|
|
or _hole_dimension_value_excluding(data_block, ("孔直径",), ("沉头", "锥", "counter", "csk", "角度", "深度"))
|
|
or _hole_dimension_value_excluding(data_block, ("hole", "diameter"), ("counter", "csk", "angle", "depth"))
|
|
or _hole_dimension_value_excluding(data_block, ("thread", "diameter"), ("counter", "csk", "angle", "depth"))
|
|
or _hole_dimension_value_excluding(data_block, ("螺纹",), ("深度", "depth", "角度", "angle"))
|
|
or _hole_primary_dimension_fallback(data_block, prefer_small=True)
|
|
)
|
|
return abs(float(diameter)) if diameter else 0
|
|
|
|
def _hole_primary_depth_mm(data_block: Dict[str, Any]) -> float:
|
|
depth = (
|
|
_hole_dimension_value_excluding(data_block, ("通孔", "孔深度"), ("沉头", "锥", "counter", "csk", "角度", "直径"))
|
|
or _hole_dimension_value_excluding(data_block, ("孔深度",), ("沉头", "锥", "counter", "csk", "角度", "直径"))
|
|
or _hole_dimension_value_excluding(data_block, ("螺纹孔钻头", "深度"), ("直径", "角度"))
|
|
or _hole_dimension_value_excluding(data_block, ("通孔", "螺纹孔钻头", "深度"), ("直径", "角度"))
|
|
or _hole_dimension_value_excluding(data_block, ("tap", "drill", "depth"), ("diameter", "angle"))
|
|
or _hole_dimension_value_excluding(data_block, ("hole", "depth"), ("counter", "csk", "angle", "diameter"))
|
|
or _hole_dimension_value_excluding(data_block, ("thread", "depth"), ("counter", "csk", "angle", "diameter"))
|
|
)
|
|
if depth:
|
|
return abs(float(depth))
|
|
return THROUGH_CUT_AMOUNT_MM
|
|
|
|
def _hole_counterbore_dimension_mm(data_block: Dict[str, Any]) -> Optional[float]:
|
|
return (
|
|
_hole_dimension_value(data_block, ("柱形沉头", "直径"))
|
|
or _hole_dimension_value(data_block, ("柱形沉头孔", "直径"))
|
|
or _hole_dimension_value(data_block, ("沉头孔", "直径"))
|
|
or _hole_dimension_value(data_block, ("counterbore", "diameter"))
|
|
or _hole_dimension_value(data_block, ("counter", "bore", "diameter"))
|
|
)
|
|
|
|
def _hole_counterbore_depth_dimension_mm(data_block: Dict[str, Any]) -> Optional[float]:
|
|
return (
|
|
_hole_dimension_value(data_block, ("柱形沉头", "深度"))
|
|
or _hole_dimension_value(data_block, ("柱形沉头孔", "深度"))
|
|
or _hole_dimension_value(data_block, ("沉头孔", "深度"))
|
|
or _hole_dimension_value(data_block, ("counterbore", "depth"))
|
|
or _hole_dimension_value(data_block, ("counter", "bore", "depth"))
|
|
)
|
|
|
|
def _hole_angle_dimension_rad(data_block: Dict[str, Any], tokens: tuple[str, ...]) -> Optional[float]:
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
name = str(dim.get("name") or "").lower()
|
|
if all(token.lower() in name for token in tokens):
|
|
if dim.get("system_value_m") not in (None, ""):
|
|
return float(dim.get("system_value_m"))
|
|
if dim.get("value") not in (None, ""):
|
|
value = float(dim.get("value"))
|
|
return value / 1000 if value > math.tau else value
|
|
return None
|
|
|
|
def _extract_edge_selector_points(op: Dict[str, Any]) -> list[list[tuple[float, float, float]]]:
|
|
selector_points = []
|
|
for selector in op.get("selectors", []):
|
|
geometry = selector.get("geometry") or {}
|
|
start = geometry.get("start_vertex") or {}
|
|
start_point = start.get("point_m") if isinstance(start, dict) else None
|
|
end = geometry.get("end_vertex") or {}
|
|
end_point = end.get("point_m") if isinstance(end, dict) else None
|
|
if start_point and end_point:
|
|
selector_points.append([_point_m_to_mm(start_point), _point_m_to_mm(end_point)])
|
|
return selector_points
|
|
|
|
_SW_METADATA_FEATURE_TYPES = {
|
|
"commentsfolder",
|
|
"favoritefolder",
|
|
"historyfolder",
|
|
"selectionsetfolder",
|
|
"sensorfolder",
|
|
"docsfolder",
|
|
"detailcabinet",
|
|
"surfacebodyfolder",
|
|
"solidbodyfolder",
|
|
"envfolder",
|
|
"inkmarkupfolder",
|
|
"eqnfolder",
|
|
"materialfolder",
|
|
"configtablefolder",
|
|
"ftrfolder",
|
|
}
|
|
|
|
def _source_feature(feature: Dict[str, Any], index: int) -> Dict[str, Any]:
|
|
source = feature.get("source_feature") if isinstance(feature.get("source_feature"), dict) else {}
|
|
identity = source.get("identity") if isinstance(source.get("identity"), dict) else {}
|
|
return {
|
|
"index": source.get("index", index),
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": feature.get("type"),
|
|
"type_name": feature.get("type_name"),
|
|
"stable_id": source.get("stable_id") or identity.get("stable_id"),
|
|
"persistent_reference": source.get("persistent_reference") or identity.get("persistent_reference"),
|
|
"identity": identity or None,
|
|
}
|
|
|
|
def _source_owned_faces(feature: Dict[str, Any]) -> list[Dict[str, Any]]:
|
|
faces = feature.get("owned_faces")
|
|
if not isinstance(faces, list):
|
|
return []
|
|
summarized = []
|
|
for face in faces:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
surface = face.get("surface") if isinstance(face.get("surface"), dict) else {}
|
|
summarized.append(
|
|
{
|
|
"box_m": face.get("box_m"),
|
|
"area_m2": face.get("area_m2"),
|
|
"surface": {
|
|
"is_plane": bool(surface.get("is_plane")),
|
|
"is_cylinder": bool(surface.get("is_cylinder")),
|
|
"is_cone": bool(surface.get("is_cone")),
|
|
"is_sphere": bool(surface.get("is_sphere")),
|
|
"is_torus": bool(surface.get("is_torus")),
|
|
"cylinder_params": surface.get("cylinder_params"),
|
|
"cone_params": surface.get("cone_params"),
|
|
"plane_params": surface.get("plane_params"),
|
|
},
|
|
}
|
|
)
|
|
return summarized
|
|
|
|
def _convert_sw_reference(feature: Dict[str, Any], index: int) -> Dict[str, Any]:
|
|
snapshot = feature.get("definition_snapshot", {})
|
|
return {
|
|
"id": feature.get("id") or f"reference_{index:03d}",
|
|
"name": feature.get("name"),
|
|
"type": feature.get("type"),
|
|
"sw_type": feature.get("type_name"),
|
|
"definition": snapshot.get("values", {}),
|
|
"source_feature": _source_feature(feature, index),
|
|
}
|
|
|
|
def _convert_sw_sketch(feature: Dict[str, Any], sketch_id: str, index: int) -> Dict[str, Any]:
|
|
sketch_data = feature.get("sketch_data", {})
|
|
raw_entities = sketch_data.get("entities", [])
|
|
raw_converted_entities = [_convert_sw_sketch_entity(entity) for entity in raw_entities]
|
|
converted_entities = []
|
|
raw_to_converted_index: dict[int, int] = {}
|
|
stable_id_to_raw_index: dict[str, int] = {}
|
|
for raw_index, (raw_entity, converted_entity) in enumerate(zip(raw_entities, raw_converted_entities)):
|
|
for stable_id in _selectable_stable_ids(raw_entity):
|
|
stable_id_to_raw_index.setdefault(stable_id, raw_index)
|
|
if converted_entity is None:
|
|
continue
|
|
raw_to_converted_index[raw_index] = len(converted_entities)
|
|
converted_entities.append(converted_entity)
|
|
loops = []
|
|
for contour in sketch_data.get("sketch_contours", []) or sketch_data.get("contours", []) or []:
|
|
if not isinstance(contour, dict):
|
|
continue
|
|
entity_indices = contour.get("entity_indices") or contour.get("segment_indices") or []
|
|
if not entity_indices:
|
|
entity_indices = _contour_entity_indices_from_segments(contour, stable_id_to_raw_index)
|
|
if not entity_indices:
|
|
continue
|
|
normalized_indices = [
|
|
raw_to_converted_index[int(idx)]
|
|
for idx in entity_indices
|
|
if isinstance(idx, (int, float)) and int(idx) in raw_to_converted_index
|
|
]
|
|
if not normalized_indices:
|
|
continue
|
|
bbox = _loop_bbox([
|
|
converted_entities[idx]
|
|
for idx in normalized_indices
|
|
if 0 <= idx < len(converted_entities)
|
|
])
|
|
loops.append({
|
|
"id": contour.get("contour_id"),
|
|
"entity_indices": normalized_indices,
|
|
"is_closed": contour.get("is_closed"),
|
|
"bbox_mm": bbox or contour.get("bbox_mm"),
|
|
"bbox_area_mm2": _bbox_area_2d(bbox) if bbox else contour.get("bbox_area_mm2"),
|
|
"source": "solidworks_sketch_contour",
|
|
})
|
|
workplane = sketch_data.get("workplane") or {}
|
|
if not workplane:
|
|
workplane = {"name": sketch_data.get("plane"), "origin_mm": [0, 0, 0], "normal": [0, 0, 1], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}
|
|
return {
|
|
"id": sketch_id,
|
|
"name": feature.get("name", sketch_id),
|
|
"workplane": workplane,
|
|
"host_reference": sketch_data.get("host_reference"),
|
|
"entities": converted_entities,
|
|
"loops": loops,
|
|
"sketch_regions": sketch_data.get("sketch_regions", []),
|
|
"constraints": sketch_data.get("constraints", []),
|
|
"inferred_constraints": sketch_data.get("inferred_constraints", []),
|
|
"dimensions": sketch_data.get("dimensions", []),
|
|
"feature_dimensions": sketch_data.get("feature_dimensions", []),
|
|
"source_feature": _source_feature(feature, index),
|
|
}
|
|
|
|
def _selectable_stable_ids(value: Any) -> list[str]:
|
|
if not isinstance(value, dict):
|
|
return []
|
|
candidates = [value.get("stable_id")]
|
|
identity = value.get("identity")
|
|
if isinstance(identity, dict):
|
|
candidates.append(identity.get("stable_id"))
|
|
return [str(candidate) for candidate in candidates if candidate]
|
|
|
|
def _contour_entity_indices_from_segments(contour: Dict[str, Any], stable_id_to_raw_index: dict[str, int]) -> list[int]:
|
|
indices: list[int] = []
|
|
seen: set[int] = set()
|
|
for segment in contour.get("sketch_segments") or []:
|
|
for stable_id in _selectable_stable_ids(segment):
|
|
raw_index = stable_id_to_raw_index.get(stable_id)
|
|
if raw_index is None or raw_index in seen:
|
|
continue
|
|
seen.add(raw_index)
|
|
indices.append(raw_index)
|
|
break
|
|
return indices
|
|
|
|
def _convert_sw_sketch_entity(entity: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
entity_type = str(entity.get("canonical_entity_type") or entity.get("entity_type", "")).lower()
|
|
curve = entity.get("curve") if isinstance(entity.get("curve"), dict) else {}
|
|
if (
|
|
entity_type == "circle_or_arc"
|
|
or curve.get("is_circle") is True
|
|
or entity.get("curve_entity_type") == "circle_or_arc"
|
|
):
|
|
center = entity.get("curve_center_mm") or entity.get("center_mm")
|
|
radius_mm_value = entity.get("curve_radius_mm") or entity.get("radius_mm")
|
|
radius_raw_value = entity.get("radius")
|
|
start = entity.get("start_mm")
|
|
end = entity.get("end_mm")
|
|
start_2d = [float(start[0]), float(start[1])] if isinstance(start, list) and len(start) >= 2 else None
|
|
end_2d = [float(end[0]), float(end[1])] if isinstance(end, list) and len(end) >= 2 else None
|
|
center_2d = [float(center[0]), float(center[1])] if isinstance(center, list) and len(center) >= 2 else [0.0, 0.0]
|
|
radius_mm = float(radius_mm_value) if radius_mm_value is not None else _scale_length(radius_raw_value or 0)
|
|
if start_2d and end_2d and math.hypot(start_2d[0] - end_2d[0], start_2d[1] - end_2d[1]) > 1e-6:
|
|
# 计算 sweep 方向
|
|
import math as _math
|
|
sa = _math.degrees(_math.atan2(start_2d[1] - center_2d[1], start_2d[0] - center_2d[0]))
|
|
ea = _math.degrees(_math.atan2(end_2d[1] - center_2d[1], end_2d[0] - center_2d[0]))
|
|
sweep = round(ea - sa, 10)
|
|
while sweep <= -180:
|
|
sweep += 360
|
|
while sweep > 180:
|
|
sweep -= 360
|
|
result = {
|
|
"type": "arc",
|
|
"center": center_2d,
|
|
"start": start_2d,
|
|
"end": end_2d,
|
|
"radius_mm": radius_mm,
|
|
"start_angle_deg": round(sa, 10),
|
|
"end_angle_deg": round(ea, 10),
|
|
"arc_sweep_deg": round(sweep, 10),
|
|
"construction": bool(entity.get("construction")),
|
|
"raw": entity,
|
|
}
|
|
curve_axis = entity.get("curve_axis")
|
|
if isinstance(curve_axis, list) and len(curve_axis) >= 3:
|
|
result["curve_axis"] = [float(v) for v in curve_axis[:3]]
|
|
return result
|
|
return {
|
|
"type": "circle",
|
|
"center": center_2d,
|
|
"radius_mm": radius_mm,
|
|
"construction": bool(entity.get("construction")),
|
|
"raw": entity,
|
|
}
|
|
if "line" in entity_type:
|
|
return {
|
|
"type": "line",
|
|
"start": _sketch_point_mm(entity, "start"),
|
|
"end": _sketch_point_mm(entity, "end"),
|
|
"construction": bool(entity.get("construction")),
|
|
"raw": entity,
|
|
}
|
|
if "circle" in entity_type:
|
|
return {
|
|
"type": "circle",
|
|
"center": _sketch_point_mm(entity, "center"),
|
|
"radius_mm": _sketch_radius_mm(entity),
|
|
"construction": bool(entity.get("construction")),
|
|
"raw": entity,
|
|
}
|
|
if "arc" in entity_type:
|
|
return {
|
|
"type": "arc",
|
|
"center": _sketch_point_mm(entity, "center"),
|
|
"start": _sketch_point_mm(entity, "start"),
|
|
"end": _sketch_point_mm(entity, "end"),
|
|
"radius_mm": _sketch_radius_mm(entity),
|
|
"start_angle_deg": _to_degrees(entity.get("start_angle", 0)),
|
|
"end_angle_deg": _to_degrees(entity.get("end_angle", 360)),
|
|
"construction": bool(entity.get("construction")),
|
|
"raw": entity,
|
|
}
|
|
if entity_type == "point":
|
|
point = entity.get("point_mm") or [float(entity.get("x", 0)) * 1000, float(entity.get("y", 0)) * 1000, 0]
|
|
return {"type": "point", "point": point[:2], "point_mm": point, "construction": bool(entity.get("construction")), "raw": entity}
|
|
return None
|
|
|
|
def _sketch_point_mm(entity: Dict[str, Any], key: str) -> list[float]:
|
|
point = entity.get(f"{key}_mm")
|
|
if isinstance(point, list) and len(point) >= 2:
|
|
return [float(point[0]), float(point[1])]
|
|
return _scale_point(entity.get(key, [0, 0]))
|
|
|
|
def _sketch_radius_mm(entity: Dict[str, Any]) -> float:
|
|
for key in ("radius_mm", "major_radius_mm", "major_radius"):
|
|
if entity.get(key) is not None:
|
|
return _scale_length(entity.get(key))
|
|
if entity.get("radius") is not None:
|
|
return _scale_length(entity.get("radius"))
|
|
start = _sketch_point_mm(entity, "start")
|
|
center = _sketch_point_mm(entity, "center")
|
|
if start and center:
|
|
return math.hypot(float(start[0]) - float(center[0]), float(start[1]) - float(center[1]))
|
|
return 1.0
|
|
|
|
def _convert_sw_extrude(feature: Dict[str, Any], type_name: str, sketch_id: Optional[str], index: int) -> Dict[str, Any]:
|
|
data_block = feature.get("extrude_data", {})
|
|
op_type = "extrude_cut" if _is_cut_feature(feature, type_name) else "extrude_add"
|
|
distance = _best_extrude_depth_mm(feature, data_block)
|
|
reverse_end_condition_code = data_block.get("reverse_end_condition_code")
|
|
reverse_distance = abs(data_block.get("reverse_depth") or 0)
|
|
both_directions = bool(data_block.get("both_directions", False))
|
|
reverse_direction = data_block.get("is_reverse")
|
|
if reverse_direction is None:
|
|
reverse_direction = data_block.get("definition_snapshot", {}).get("ReverseDirection")
|
|
if reverse_direction is None:
|
|
reverse_direction = feature.get("definition_snapshot", {}).get("values", {}).get("ReverseDirection", False)
|
|
if reverse_end_condition_code in (None, 0) and data_block.get("effective_depth_source") == "feature_dimension":
|
|
spans_both_sides = _extrude_owned_faces_span_sketch_plane(feature, data_block)
|
|
if spans_both_sides and bool(reverse_direction):
|
|
both_directions = True
|
|
reverse_distance = reverse_distance or distance
|
|
else:
|
|
both_directions = False
|
|
reverse_distance = 0
|
|
raw_depth = abs(data_block.get("depth") or data_block.get("blind_depth") or 0)
|
|
uses_reverse_depth_only = (
|
|
op_type == "extrude_cut"
|
|
and
|
|
feature.get("type") == "ice"
|
|
and raw_depth <= 1e-9
|
|
and reverse_distance > 0
|
|
)
|
|
if uses_reverse_depth_only:
|
|
reverse_direction = not bool(reverse_direction) if False else bool(reverse_direction)
|
|
return {
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": op_type,
|
|
"sketch": sketch_id,
|
|
"parameters": {
|
|
"distance_mm": distance,
|
|
"reverse": bool(reverse_direction),
|
|
"reverse_direction": bool(reverse_direction),
|
|
"reverse_distance_mm": reverse_distance,
|
|
"both_directions": False if uses_reverse_depth_only else both_directions,
|
|
"end_condition": data_block.get("end_condition"),
|
|
"end_condition_code": data_block.get("end_condition_code"),
|
|
"reverse_end_condition_code": reverse_end_condition_code,
|
|
"flip_side_to_cut": bool(data_block.get("flip_side_to_cut", False)),
|
|
"start_condition_reference": _clean_null_reference(data_block.get("start_condition_reference")),
|
|
"end_condition_reference": _clean_null_reference(data_block.get("end_condition_reference")),
|
|
"reverse_end_condition_reference": _clean_null_reference(data_block.get("reverse_end_condition_reference")),
|
|
"draft_angle_rad": data_block.get("draft_angle_rad"),
|
|
"reverse_draft_angle_rad": data_block.get("reverse_draft_angle_rad"),
|
|
},
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
}
|
|
|
|
def _extrude_owned_faces_span_sketch_plane(feature: Dict[str, Any], data_block: Dict[str, Any]) -> bool:
|
|
sketches = data_block.get("source_sketches") or []
|
|
workplane = sketches[0].get("workplane") if sketches and isinstance(sketches[0], dict) else None
|
|
if not isinstance(workplane, dict):
|
|
return bool(data_block.get("both_directions")) and (data_block.get("reverse_depth") not in (None, 0))
|
|
|
|
origin = workplane.get("origin_mm") or [0, 0, 0]
|
|
normal = workplane.get("normal") or [0, 0, 1]
|
|
if not isinstance(origin, list) or not isinstance(normal, list) or len(origin) < 3 or len(normal) < 3:
|
|
return False
|
|
|
|
nx, ny, nz = (float(normal[0]), float(normal[1]), float(normal[2]))
|
|
length = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0
|
|
nx, ny, nz = nx / length, ny / length, nz / length
|
|
ox, oy, oz = float(origin[0]), float(origin[1]), float(origin[2])
|
|
|
|
min_distance = math.inf
|
|
max_distance = -math.inf
|
|
for face in feature.get("owned_faces") or []:
|
|
box = face.get("box_m") if isinstance(face, dict) else None
|
|
if not isinstance(box, list) or len(box) < 6:
|
|
continue
|
|
xs = [float(box[0]) * 1000, float(box[3]) * 1000]
|
|
ys = [float(box[1]) * 1000, float(box[4]) * 1000]
|
|
zs = [float(box[2]) * 1000, float(box[5]) * 1000]
|
|
for x in xs:
|
|
for y in ys:
|
|
for z in zs:
|
|
distance_to_plane = (x - ox) * nx + (y - oy) * ny + (z - oz) * nz
|
|
min_distance = min(min_distance, distance_to_plane)
|
|
max_distance = max(max_distance, distance_to_plane)
|
|
|
|
if math.isinf(min_distance) or math.isinf(max_distance):
|
|
return False
|
|
tolerance = 1e-4
|
|
return min_distance < -tolerance and max_distance > tolerance
|
|
|
|
def _convert_sw_revolve(feature: Dict[str, Any], type_name: str, sketch_id: Optional[str], index: int) -> Dict[str, Any]:
|
|
data_block = feature.get("revolve_data", {})
|
|
op_type = "revolve_cut" if _is_cut_feature(feature, type_name) else "revolve_add"
|
|
selected_axis = _axis_reference_from_feature_selections(data_block.get("selections"))
|
|
owned_face_axis = _axis_reference_from_owned_faces(feature)
|
|
extracted_axis = _extract_axis_reference(data_block.get("axis_reference"))
|
|
axis_reference = selected_axis or owned_face_axis
|
|
if not axis_reference and not _is_weak_inferred_axis(extracted_axis):
|
|
axis_reference = extracted_axis
|
|
return {
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": op_type,
|
|
"sketch": sketch_id,
|
|
"parameters": {
|
|
"angle_deg": abs(data_block.get("angle") or 360),
|
|
"angle_rad": data_block.get("angle_rad"),
|
|
"reverse": data_block.get("is_reverse", False),
|
|
"end_condition": data_block.get("end_condition"),
|
|
"end_condition_code": data_block.get("end_condition_code"),
|
|
"axis_reference": axis_reference,
|
|
"axis_candidates": data_block.get("axis_candidates", []),
|
|
},
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
}
|
|
|
|
def _axis_reference_from_owned_faces(feature: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
candidates: list[tuple[float, Dict[str, Any]]] = []
|
|
for face in feature.get("owned_faces") or []:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
surface = face.get("surface") if isinstance(face.get("surface"), dict) else {}
|
|
params = None
|
|
if surface.get("is_cylinder") and isinstance(surface.get("cylinder_params"), list):
|
|
params = surface.get("cylinder_params")
|
|
elif surface.get("is_cone") and isinstance(surface.get("cone_params"), list):
|
|
params = surface.get("cone_params")
|
|
if not isinstance(params, list) or len(params) < 6:
|
|
continue
|
|
direction = [float(value) for value in params[3:6]]
|
|
norm = math.sqrt(sum(value * value for value in direction))
|
|
if norm <= 1e-9:
|
|
continue
|
|
candidates.append((
|
|
float(face.get("area_m2") or 0.0),
|
|
{
|
|
"origin_mm": [float(value) * 1000 for value in params[:3]],
|
|
"direction": [value / norm for value in direction],
|
|
"source": "owned_face_axis",
|
|
},
|
|
))
|
|
if not candidates:
|
|
return None
|
|
candidates.sort(key=lambda item: item[0], reverse=True)
|
|
return candidates[0][1]
|
|
|
|
def _is_weak_inferred_axis(axis_reference: Optional[Dict[str, Any]]) -> bool:
|
|
if not isinstance(axis_reference, dict):
|
|
return False
|
|
return str(axis_reference.get("source") or "") in {"construction_line_candidate", "construction_line"}
|
|
|
|
def _convert_sw_hole(feature: Dict[str, Any], index: int) -> Dict[str, Any]:
|
|
data_block = feature.get("hole_data", {})
|
|
positions = []
|
|
host_face = _host_face_from_feature_selections(data_block.get("selections")) or {}
|
|
position_sketches = _hole_position_sketches(data_block.get("position_sketches", []) or [])
|
|
for sketch in position_sketches:
|
|
workplane = sketch.get("workplane") or {}
|
|
if not host_face and workplane:
|
|
host_face = _host_face_from_workplane(workplane)
|
|
for point in _hole_position_points(sketch):
|
|
positions.append({"mm": [float(point[0]), float(point[1]), float(point[2] if len(point) > 2 else 0)]})
|
|
diameter_mm = abs(data_block.get("diameter") or 0) or _hole_primary_diameter_mm(data_block)
|
|
depth_mm = abs(data_block.get("depth") or 0) or _hole_primary_depth_mm(data_block)
|
|
return {
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": "hole",
|
|
"parameters": {
|
|
"diameter_mm": diameter_mm,
|
|
"depth_mm": depth_mm,
|
|
"counterbore_diameter_mm": _hole_counterbore_dimension_mm(data_block),
|
|
"counterbore_depth_mm": _hole_counterbore_depth_dimension_mm(data_block),
|
|
"countersink_diameter_mm": _hole_dimension_value(data_block, ("锥形沉头", "直径"))
|
|
or _hole_dimension_value(data_block, ("近端锥形沉头", "直径"))
|
|
or _hole_dimension_value(data_block, ("锥坑", "直径"))
|
|
or _hole_dimension_value(data_block, ("countersink", "diameter"))
|
|
or _hole_dimension_value(data_block, ("csk", "diameter")),
|
|
"angles_rad": {
|
|
"countersink_angle": _hole_angle_dimension_rad(data_block, ("锥形沉头", "角度"))
|
|
or _hole_angle_dimension_rad(data_block, ("近端锥形沉头", "角度"))
|
|
or _hole_angle_dimension_rad(data_block, ("锥坑", "角度"))
|
|
or _hole_angle_dimension_rad(data_block, ("countersink", "angle"))
|
|
or _hole_angle_dimension_rad(data_block, ("csk", "angle")),
|
|
"drill_angle": _hole_angle_dimension_rad(data_block, ("导头", "角度"))
|
|
or _hole_angle_dimension_rad(data_block, ("drill", "angle"))
|
|
or _hole_angle_dimension_rad(data_block, ("tip", "angle")),
|
|
},
|
|
"positions": positions,
|
|
"host_face": host_face,
|
|
"hole_type": data_block.get("hole_type"),
|
|
"standard": data_block.get("standard"),
|
|
"size": data_block.get("size"),
|
|
"dimension_names": [
|
|
str(dim.get("name") or "")
|
|
for dim in data_block.get("dimensions", []) or []
|
|
if isinstance(dim, dict)
|
|
],
|
|
},
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
}
|
|
|
|
def _hole_position_sketches(sketches: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
|
|
point_only = []
|
|
for sketch in sketches:
|
|
entities = sketch.get("entities") or []
|
|
if not entities:
|
|
continue
|
|
if _is_hole_profile_sketch(sketch):
|
|
continue
|
|
point_count = sum(1 for entity in entities if _is_sketch_point_entity(entity))
|
|
drawable_segment_count = sum(
|
|
1
|
|
for entity in entities
|
|
if not _is_sketch_point_entity(entity) and not entity.get("construction")
|
|
)
|
|
if point_count > 0 and drawable_segment_count == 0:
|
|
point_only.append(sketch)
|
|
return point_only or sketches[:1]
|
|
|
|
def _is_hole_profile_sketch(sketch: Dict[str, Any]) -> bool:
|
|
tokens = (
|
|
"孔直径",
|
|
"孔深度",
|
|
"沉头",
|
|
"导头",
|
|
"螺纹孔钻头",
|
|
"tap drill",
|
|
"drill",
|
|
"counterbore",
|
|
"countersink",
|
|
"hole diameter",
|
|
"hole depth",
|
|
)
|
|
dimension_sources = []
|
|
dimension_sources.extend(sketch.get("dimensions") or [])
|
|
dimension_sources.extend(sketch.get("feature_dimensions") or [])
|
|
for dim in dimension_sources:
|
|
if not isinstance(dim, dict):
|
|
continue
|
|
name = str(dim.get("name") or "").lower()
|
|
if any(token in name for token in tokens):
|
|
return True
|
|
return False
|
|
|
|
def _is_sketch_point_entity(entity: Dict[str, Any]) -> bool:
|
|
entity_type = str(entity.get("entity_type") or entity.get("type") or "").lower()
|
|
return entity_type == "point"
|
|
|
|
def _hole_position_entity_flags(entity: Dict[str, Any]) -> tuple[Optional[bool], bool]:
|
|
raw = entity.get("raw") if isinstance(entity.get("raw"), dict) else entity
|
|
candidate = raw.get("hole_position_candidate")
|
|
if candidate is None:
|
|
candidate = entity.get("hole_position_candidate")
|
|
if isinstance(candidate, bool):
|
|
candidate_flag: Optional[bool] = candidate
|
|
else:
|
|
candidate_flag = None
|
|
construction_reference = bool(
|
|
raw.get("construction_endpoint_reference") or entity.get("construction_endpoint_reference")
|
|
)
|
|
return candidate_flag, construction_reference
|
|
|
|
def _construction_endpoint_degrees(sketch: Dict[str, Any]) -> dict[tuple[float, float, float], int]:
|
|
degrees: dict[tuple[float, float, float], int] = {}
|
|
for entity in sketch.get("entities") or []:
|
|
if not entity.get("construction"):
|
|
continue
|
|
entity_type = str(entity.get("entity_type") or entity.get("type") or "").lower()
|
|
if "line" not in entity_type:
|
|
continue
|
|
for key in ("start_mm", "end_mm"):
|
|
endpoint = entity.get(key)
|
|
if isinstance(endpoint, list) and len(endpoint) >= 2:
|
|
point_key = _rounded_point_key(endpoint)
|
|
degrees[point_key] = degrees.get(point_key, 0) + 1
|
|
return degrees
|
|
|
|
def _hole_position_points(sketch: Dict[str, Any]) -> list[list[float]]:
|
|
"""Return only real Hole Wizard placement points from a position sketch.
|
|
|
|
SolidWorks Hole Wizard position sketches often include construction
|
|
segments whose endpoints are reference geometry, not hole centers. Older
|
|
parser JSON exposes those endpoints as ordinary sketch points, so we filter
|
|
them generically here instead of letting every point become a hole.
|
|
"""
|
|
entities = sketch.get("entities") or []
|
|
point_entities: list[tuple[list[float], Optional[bool], bool]] = []
|
|
construction_endpoints: set[tuple[float, float, float]] = set()
|
|
|
|
for entity in entities:
|
|
point = entity.get("point_mm")
|
|
if _is_sketch_point_entity(entity) and isinstance(point, list) and len(point) >= 2:
|
|
candidate_flag, construction_reference = _hole_position_entity_flags(entity)
|
|
point_entities.append(
|
|
(
|
|
[float(point[0]), float(point[1]), float(point[2] if len(point) > 2 else 0)],
|
|
candidate_flag,
|
|
construction_reference,
|
|
)
|
|
)
|
|
continue
|
|
if not entity.get("construction"):
|
|
continue
|
|
entity_type = str(entity.get("entity_type") or entity.get("type") or "").lower()
|
|
if "line" not in entity_type:
|
|
continue
|
|
for key in ("start_mm", "end_mm"):
|
|
endpoint = entity.get(key)
|
|
if isinstance(endpoint, list) and len(endpoint) >= 2:
|
|
construction_endpoints.add(_rounded_point_key(endpoint))
|
|
|
|
if not point_entities:
|
|
return []
|
|
|
|
explicit_candidates = [
|
|
point for point, candidate_flag, _ in point_entities if candidate_flag is True
|
|
]
|
|
if explicit_candidates:
|
|
return _dedupe_points(explicit_candidates)
|
|
|
|
endpoint_degrees = _construction_endpoint_degrees(sketch)
|
|
if endpoint_degrees:
|
|
filtered = []
|
|
for point, candidate_flag, construction_reference in point_entities:
|
|
point_key = _rounded_point_key(point)
|
|
degree = endpoint_degrees.get(point_key, 0)
|
|
if candidate_flag is False and construction_reference and degree <= 1:
|
|
continue
|
|
if degree >= 2 or not construction_reference:
|
|
filtered.append(point)
|
|
filtered = _dedupe_points(filtered)
|
|
non_origin_filtered = [point for point in filtered if not _is_near_origin(point)]
|
|
if non_origin_filtered:
|
|
return _dedupe_points(non_origin_filtered)
|
|
if filtered:
|
|
return filtered
|
|
|
|
raw_points = _dedupe_points([point for point, _, _ in point_entities])
|
|
if not raw_points or not construction_endpoints:
|
|
return raw_points
|
|
|
|
legacy_filtered = [point for point in raw_points if _rounded_point_key(point) not in construction_endpoints]
|
|
non_origin_raw = [point for point in raw_points if not _is_near_origin(point)]
|
|
non_origin_filtered = [point for point in legacy_filtered if not _is_near_origin(point)]
|
|
if non_origin_filtered:
|
|
return _dedupe_points(non_origin_filtered)
|
|
if non_origin_raw:
|
|
return _dedupe_points(non_origin_raw)
|
|
return _dedupe_points(legacy_filtered or raw_points)
|
|
|
|
def _convert_sw_linear_pattern(
|
|
feature: Dict[str, Any],
|
|
index: int,
|
|
previous_build_op: Optional[Dict[str, Any]],
|
|
source_frame: Optional[Dict[str, Any]] = None,
|
|
sketches: Optional[list[Dict[str, Any]]] = None,
|
|
source_bbox: Optional[list[float]] = None,
|
|
) -> Dict[str, Any]:
|
|
data_block = feature.get("linear_pattern_data", {})
|
|
source_features = data_block.get("source_features") or []
|
|
if not source_features and previous_build_op:
|
|
source_features = [previous_build_op.get("source_feature", {})]
|
|
spacing_1 = data_block.get("spacing_1")
|
|
spacing_2 = data_block.get("spacing_2")
|
|
direction_1 = _pattern_direction_from_plugin(data_block.get("direction_1"), axis="x", source_frame=source_frame)
|
|
direction_2 = _pattern_direction_from_plugin(data_block.get("direction_2"), axis="y", source_frame=source_frame)
|
|
direction_1 = _pattern_direction_from_reference(direction_1, data_block.get("direction_1_reference"), source_frame)
|
|
direction_2 = _pattern_direction_from_reference(direction_2, data_block.get("direction_2_reference"), source_frame)
|
|
if data_block.get("direction_1_reverse") is True:
|
|
direction_1 = _reverse_pattern_direction(direction_1)
|
|
if data_block.get("direction_2_reverse") is True:
|
|
direction_2 = _reverse_pattern_direction(direction_2)
|
|
source_op_bbox = _operation_profile_bbox(previous_build_op, sketches or [])
|
|
if data_block.get("direction_1") is None:
|
|
direction_1 = _choose_pattern_direction_sign(
|
|
direction_1,
|
|
spacing_1 or 0,
|
|
int(data_block.get("pattern_count_1") or 1),
|
|
source_op_bbox,
|
|
source_bbox,
|
|
)
|
|
if data_block.get("direction_2") is None:
|
|
direction_2 = _choose_pattern_direction_sign(
|
|
direction_2,
|
|
spacing_2 or 0,
|
|
int(data_block.get("pattern_count_2") or 1),
|
|
source_op_bbox,
|
|
source_bbox,
|
|
)
|
|
explicit_offsets = _owned_face_pattern_offsets(previous_build_op, feature)
|
|
return {
|
|
"id": feature.get("id"),
|
|
"name": feature.get("name"),
|
|
"type": "linear_pattern",
|
|
"parameters": {
|
|
"source_features": source_features,
|
|
"total_instances": data_block.get("pattern_count_1") or 1,
|
|
"spacing_mm": spacing_1 or 0,
|
|
"direction1": direction_1,
|
|
"direction2": direction_2,
|
|
},
|
|
"raw_parameters": {
|
|
"d1_total_instances": data_block.get("pattern_count_1") or 1,
|
|
"d2_total_instances": data_block.get("pattern_count_2") or 1,
|
|
"d1_spacing_mm": spacing_1 or 0,
|
|
"d2_spacing_mm": spacing_2 or 0,
|
|
"direction1": direction_1,
|
|
"direction2": direction_2,
|
|
"explicit_offsets_mm": explicit_offsets,
|
|
},
|
|
"source_feature": _source_feature(feature, index),
|
|
"source_owned_faces": _source_owned_faces(feature),
|
|
}
|
|
|
|
def _owned_face_pattern_offsets(
|
|
source_op: Optional[Dict[str, Any]],
|
|
pattern_feature: Dict[str, Any],
|
|
) -> list[list[float]]:
|
|
if not source_op:
|
|
return []
|
|
source_faces = _owned_face_signatures(source_op.get("source_owned_faces") or [])
|
|
pattern_faces = _owned_face_signatures(_source_owned_faces(pattern_feature))
|
|
if not source_faces or not pattern_faces:
|
|
return []
|
|
|
|
votes: Dict[tuple[float, float, float], int] = {}
|
|
for pattern_face in pattern_faces:
|
|
for source_face in source_faces:
|
|
if pattern_face["kind"] != source_face["kind"]:
|
|
continue
|
|
if not _similar_bbox_size(pattern_face["size"], source_face["size"]):
|
|
continue
|
|
offset = tuple(
|
|
round(pattern_face["center"][axis] - source_face["center"][axis], 3)
|
|
for axis in range(3)
|
|
)
|
|
if math.sqrt(sum(component * component for component in offset)) < 1e-6:
|
|
continue
|
|
votes[offset] = votes.get(offset, 0) + 1
|
|
|
|
if not votes:
|
|
return []
|
|
threshold = max(1, min(2, len(source_faces)))
|
|
offsets = [offset for offset, count in votes.items() if count >= threshold]
|
|
offsets.sort(key=lambda offset: (offset[0] * offset[0] + offset[1] * offset[1] + offset[2] * offset[2], offset))
|
|
return [[float(value) for value in offset] for offset in offsets]
|
|
|
|
def _owned_face_signatures(faces: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
|
|
signatures = []
|
|
for face in faces:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
box = face.get("box_m")
|
|
if not isinstance(box, list) or len(box) < 6:
|
|
continue
|
|
box_mm = [float(value) * 1000 for value in box[:6]]
|
|
surface = face.get("surface") if isinstance(face.get("surface"), dict) else {}
|
|
kind = "other"
|
|
if surface.get("is_cylinder"):
|
|
kind = "cylinder"
|
|
elif surface.get("is_cone"):
|
|
kind = "cone"
|
|
elif surface.get("is_plane"):
|
|
kind = "plane"
|
|
signatures.append(
|
|
{
|
|
"kind": kind,
|
|
"center": [(box_mm[i] + box_mm[i + 3]) / 2 for i in range(3)],
|
|
"size": [abs(box_mm[i + 3] - box_mm[i]) for i in range(3)],
|
|
}
|
|
)
|
|
return signatures
|
|
|
|
def _source_pattern_frame(
|
|
previous_build_op: Optional[Dict[str, Any]],
|
|
sketches: list[Dict[str, Any]],
|
|
) -> Optional[Dict[str, Any]]:
|
|
if not previous_build_op:
|
|
return None
|
|
params = previous_build_op.get("parameters") or {}
|
|
host_frame = ((params.get("host_face") or {}).get("frame") or {})
|
|
if host_frame.get("x_dir") and host_frame.get("y_dir"):
|
|
return host_frame
|
|
sketch_id = previous_build_op.get("sketch")
|
|
for sketch in sketches:
|
|
if sketch.get("id") == sketch_id:
|
|
workplane = sketch.get("workplane") or {}
|
|
if workplane.get("x_dir") and workplane.get("y_dir"):
|
|
return workplane
|
|
return None
|
|
|
|
def _source_bbox_from_plugin_json(data: Dict[str, Any]) -> Optional[list[float]]:
|
|
bbox = (data.get("validation_hints") or {}).get("part_box_m")
|
|
if isinstance(bbox, list) and len(bbox) >= 6:
|
|
return [float(v) * 1000 for v in bbox[:6]]
|
|
return None
|
|
|
|
def _operation_profile_bbox(
|
|
op: Optional[Dict[str, Any]],
|
|
sketches: list[Dict[str, Any]],
|
|
) -> Optional[list[float]]:
|
|
if not op:
|
|
return None
|
|
if op.get("type") == "hole":
|
|
host_face = (op.get("parameters") or {}).get("host_face") or {}
|
|
positions = [
|
|
_hole_position_to_model(pos.get("mm"), host_face)
|
|
for pos in (op.get("parameters") or {}).get("positions", [])
|
|
if isinstance(pos.get("mm"), list) and len(pos.get("mm")) >= 3
|
|
]
|
|
if positions:
|
|
return _points_bbox(positions)
|
|
sketch_id = op.get("sketch")
|
|
sketch = next((item for item in sketches if item.get("id") == sketch_id), None)
|
|
if not sketch:
|
|
return None
|
|
points = []
|
|
workplane = sketch.get("workplane") or {}
|
|
origin = workplane.get("origin_mm") or [0, 0, 0]
|
|
x_dir = workplane.get("x_dir") or [1, 0, 0]
|
|
y_dir = workplane.get("y_dir") or [0, 1, 0]
|
|
for entity in sketch.get("entities", []) or []:
|
|
if entity.get("type") == "circle":
|
|
center = entity.get("center") or [0, 0]
|
|
radius = float(entity.get("radius_mm") or 0)
|
|
for dx, dy in ((-radius, -radius), (-radius, radius), (radius, -radius), (radius, radius)):
|
|
points.append(_sketch_point_to_model_bbox(origin, x_dir, y_dir, [float(center[0]) + dx, float(center[1]) + dy]))
|
|
for key in ("start", "end", "center", "point"):
|
|
point = entity.get(key)
|
|
if isinstance(point, list) and len(point) >= 2:
|
|
points.append(_sketch_point_to_model_bbox(origin, x_dir, y_dir, point))
|
|
return _points_bbox(points)
|
|
|
|
def _sketch_point_to_model_bbox(origin: list[Any], x_dir: list[Any], y_dir: list[Any], point: list[Any]) -> list[float]:
|
|
return [
|
|
float(origin[i]) + float(x_dir[i]) * float(point[0]) + float(y_dir[i]) * float(point[1])
|
|
for i in range(3)
|
|
]
|
|
|
|
def _hole_position_to_model(point: list[Any], host_face: Dict[str, Any]) -> list[float]:
|
|
frame = host_face.get("frame") if isinstance(host_face, dict) else {}
|
|
if not isinstance(frame, dict):
|
|
return [float(v) for v in (point + [0, 0, 0])[:3]]
|
|
origin = frame.get("origin_mm") or [0, 0, 0]
|
|
x_dir = frame.get("x_dir") or [1, 0, 0]
|
|
y_dir = frame.get("y_dir") or [0, 1, 0]
|
|
values = [float(v) for v in (point + [0, 0, 0])[:3]]
|
|
return [
|
|
float(origin[i]) + float(x_dir[i]) * values[0] + float(y_dir[i]) * values[1]
|
|
for i in range(3)
|
|
]
|
|
|
|
def _choose_pattern_direction_sign(
|
|
direction: Dict[str, Any],
|
|
spacing: float,
|
|
count: int,
|
|
source_op_bbox: Optional[list[float]],
|
|
source_bbox: Optional[list[float]],
|
|
) -> Dict[str, Any]:
|
|
vector = direction.get("vector")
|
|
if (
|
|
not isinstance(vector, list)
|
|
or len(vector) < 3
|
|
or not spacing
|
|
or count <= 1
|
|
or not source_op_bbox
|
|
or not source_bbox
|
|
):
|
|
return direction
|
|
unit = _unit3(vector)
|
|
distance = float(spacing) * (count - 1)
|
|
positive = [component * distance for component in unit]
|
|
negative = [-component * distance for component in unit]
|
|
positive_score = _bbox_overflow_score(_translated_bbox(source_op_bbox, positive), source_bbox)
|
|
negative_score = _bbox_overflow_score(_translated_bbox(source_op_bbox, negative), source_bbox)
|
|
if abs(positive_score - negative_score) <= 1e-9:
|
|
positive_score += _bbox_center_distance_score(_translated_bbox(source_op_bbox, positive), source_bbox)
|
|
negative_score += _bbox_center_distance_score(_translated_bbox(source_op_bbox, negative), source_bbox)
|
|
copied = dict(direction)
|
|
if negative_score + 1e-9 < positive_score:
|
|
copied["vector"] = [-component for component in unit]
|
|
copied["source"] = f"{direction.get('source', 'missing_direction')}_sign_from_source_bbox"
|
|
return copied
|
|
copied["vector"] = unit
|
|
if positive_score + 1e-9 < negative_score:
|
|
copied["source"] = f"{direction.get('source', 'missing_direction')}_sign_from_source_bbox"
|
|
return copied
|
|
|
|
def _is_cut_feature(feature: Dict[str, Any], type_name: str) -> bool:
|
|
text = f"{type_name} {feature.get('name', '')}".lower()
|
|
return "cut" in text or "切除" in text or "revcut" in text
|
|
|
|
def _best_extrude_depth_mm(feature: Dict[str, Any], data_block: Dict[str, Any]) -> float:
|
|
for key in ("depth", "blind_depth"):
|
|
value = data_block.get(key)
|
|
if value:
|
|
return abs(float(value))
|
|
owned_face_depth = _extrude_depth_from_owned_faces(feature, data_block)
|
|
effective_depth = abs(float(data_block.get("effective_depth") or 0))
|
|
if (
|
|
owned_face_depth
|
|
and _is_cut_feature(feature, str(feature.get("type_name") or feature.get("type") or ""))
|
|
and data_block.get("effective_depth_source") == "feature_dimension"
|
|
and not data_block.get("depth")
|
|
and not data_block.get("blind_depth")
|
|
and not data_block.get("reverse_depth")
|
|
and effective_depth > owned_face_depth * 2
|
|
):
|
|
return owned_face_depth
|
|
owner_name = feature.get("name")
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
name = dim.get("name") or ""
|
|
if owner_name and f"@{owner_name}@" in name and dim.get("value") not in (None, 0):
|
|
return abs(float(dim.get("value")))
|
|
for dim in data_block.get("dimensions", []) or []:
|
|
if dim.get("owner") == owner_name and dim.get("value") not in (None, 0):
|
|
return abs(float(dim.get("value")))
|
|
if data_block.get("reverse_depth") not in (None, 0):
|
|
return abs(float(data_block.get("reverse_depth")))
|
|
if data_block.get("effective_depth") not in (None, 0):
|
|
return abs(float(data_block.get("effective_depth")))
|
|
return 0.0
|
|
|
|
def _extrude_depth_from_owned_faces(feature: Dict[str, Any], data_block: Dict[str, Any]) -> Optional[float]:
|
|
sketches = data_block.get("source_sketches") or []
|
|
workplane = sketches[0].get("workplane") if sketches and isinstance(sketches[0], dict) else None
|
|
if not isinstance(workplane, dict):
|
|
return None
|
|
normal = workplane.get("normal") or [0, 0, 1]
|
|
if not isinstance(normal, list) or len(normal) < 3:
|
|
return None
|
|
axis = max(range(3), key=lambda idx: abs(float(normal[idx])))
|
|
values: list[float] = []
|
|
for face in feature.get("owned_faces") or []:
|
|
if not isinstance(face, dict):
|
|
continue
|
|
box = face.get("box_m")
|
|
if isinstance(box, list) and len(box) >= 6:
|
|
values.extend([float(box[axis]) * 1000, float(box[axis + 3]) * 1000])
|
|
if not values:
|
|
return None
|
|
extent = max(values) - min(values)
|
|
return abs(extent) if extent > 1e-6 else None
|
|
|
|
def _host_face_from_workplane(workplane: Dict[str, Any]) -> Dict[str, Any]:
|
|
origin = workplane.get("origin_mm") or [0, 0, 0]
|
|
normal = workplane.get("normal") or [0, 0, 1]
|
|
x_dir = workplane.get("x_dir") or [1, 0, 0]
|
|
y_dir = workplane.get("y_dir") or [0, 1, 0]
|
|
return {
|
|
"surface": {"plane_params": [*normal[:3], *(float(v) / 1000 for v in origin[:3])]},
|
|
"frame": {"origin_mm": origin[:3], "x_dir": x_dir[:3], "y_dir": y_dir[:3], "normal": normal[:3]},
|
|
}
|
|
|
|
def _pattern_direction_from_plugin(
|
|
direction: Any,
|
|
axis: str,
|
|
source_frame: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
if isinstance(direction, dict):
|
|
return direction
|
|
if source_frame:
|
|
key = "y_dir" if axis == "y" else "x_dir"
|
|
vector = source_frame.get(key)
|
|
if isinstance(vector, list) and len(vector) >= 3:
|
|
return {"vector": vector[:3], "source": f"source_feature_frame_{key}"}
|
|
if axis == "y":
|
|
return {"vector": [0, 1, 0], "source": "default_y_when_plugin_direction_missing"}
|
|
return {"vector": [1, 0, 0], "source": "default_x_when_plugin_direction_missing"}
|
|
|
|
def _pattern_direction_from_reference(
|
|
fallback: Dict[str, Any],
|
|
reference: Any,
|
|
source_frame: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
axis = _extract_axis_reference(reference)
|
|
if not axis:
|
|
return fallback
|
|
vector = axis.get("direction")
|
|
if not isinstance(vector, list) or len(vector) < 3:
|
|
return fallback
|
|
model_vector = _sketch_vector_to_model(vector[:3], source_frame) or vector[:3]
|
|
model_origin = _sketch_point_to_model(axis.get("origin_mm"), source_frame) or axis.get("origin_mm")
|
|
return {
|
|
"vector": _unit3(model_vector),
|
|
"origin_mm": model_origin,
|
|
"source": axis.get("source") or "direction_reference",
|
|
}
|
|
|
|
def _sketch_vector_to_model(
|
|
vector: list[Any],
|
|
source_frame: Optional[Dict[str, Any]],
|
|
) -> Optional[list[float]]:
|
|
if not source_frame:
|
|
return None
|
|
x_dir = source_frame.get("x_dir")
|
|
y_dir = source_frame.get("y_dir")
|
|
normal = source_frame.get("normal")
|
|
if not (
|
|
isinstance(x_dir, list)
|
|
and len(x_dir) >= 3
|
|
and isinstance(y_dir, list)
|
|
and len(y_dir) >= 3
|
|
):
|
|
return None
|
|
if not (isinstance(normal, list) and len(normal) >= 3):
|
|
normal = [
|
|
float(x_dir[1]) * float(y_dir[2]) - float(x_dir[2]) * float(y_dir[1]),
|
|
float(x_dir[2]) * float(y_dir[0]) - float(x_dir[0]) * float(y_dir[2]),
|
|
float(x_dir[0]) * float(y_dir[1]) - float(x_dir[1]) * float(y_dir[0]),
|
|
]
|
|
values = [float(v) for v in (vector + [0, 0, 0])[:3]]
|
|
return [
|
|
values[0] * float(x_dir[i]) + values[1] * float(y_dir[i]) + values[2] * float(normal[i])
|
|
for i in range(3)
|
|
]
|
|
|
|
def _sketch_point_to_model(
|
|
point: Any,
|
|
source_frame: Optional[Dict[str, Any]],
|
|
) -> Optional[list[float]]:
|
|
if not isinstance(point, list) or len(point) < 3 or not source_frame:
|
|
return None
|
|
origin = source_frame.get("origin_mm")
|
|
vector = _sketch_vector_to_model(point[:3], source_frame)
|
|
if not (isinstance(origin, list) and len(origin) >= 3 and vector):
|
|
return None
|
|
return [float(origin[i]) + vector[i] for i in range(3)]
|
|
|
|
def _reverse_pattern_direction(direction: Dict[str, Any]) -> Dict[str, Any]:
|
|
vector = direction.get("vector")
|
|
if not isinstance(vector, list) or len(vector) < 3:
|
|
return direction
|
|
copied = dict(direction)
|
|
copied["vector"] = [-float(vector[0]), -float(vector[1]), -float(vector[2])]
|
|
copied["source"] = f"{direction.get('source', 'direction')}_reversed"
|
|
return copied
|
|
|
|
def _clean_null_reference(reference: Any) -> Optional[Dict[str, Any]]:
|
|
if not isinstance(reference, dict):
|
|
return None
|
|
if reference.get("kind") == "null":
|
|
return None
|
|
obj = reference.get("object")
|
|
if isinstance(obj, dict) and obj.get("kind") == "null":
|
|
return None
|
|
return reference
|
|
|
|
def _extract_axis_reference(reference: Any) -> Optional[Dict[str, Any]]:
|
|
if not isinstance(reference, dict):
|
|
return None
|
|
if reference.get("origin_mm") and reference.get("direction"):
|
|
return {
|
|
"origin_mm": [float(v) for v in reference.get("origin_mm", [])[:3]],
|
|
"direction": [float(v) for v in reference.get("direction", [])[:3]],
|
|
"source": reference.get("source") or "axis_reference",
|
|
}
|
|
obj = reference.get("object") if isinstance(reference.get("object"), dict) else reference
|
|
if obj.get("kind") == "null":
|
|
return None
|
|
|
|
line_params = obj.get("line_params")
|
|
if isinstance(line_params, list) and len(line_params) >= 6:
|
|
return {
|
|
"origin_mm": [float(v) * 1000 for v in line_params[:3]],
|
|
"direction": [float(v) for v in line_params[3:6]],
|
|
"source": reference.get("source") or "selection_line_params",
|
|
}
|
|
|
|
curve = obj.get("curve") if isinstance(obj.get("curve"), dict) else {}
|
|
curve_line_params = curve.get("line_params")
|
|
if isinstance(curve_line_params, list) and len(curve_line_params) >= 6:
|
|
return {
|
|
"origin_mm": [float(v) * 1000 for v in curve_line_params[:3]],
|
|
"direction": [float(v) for v in curve_line_params[3:6]],
|
|
"source": reference.get("source") or "selection_curve_line_params",
|
|
}
|
|
return None
|
|
|
|
def _selection_objects(selections: Any) -> list[Dict[str, Any]]:
|
|
objects: list[Dict[str, Any]] = []
|
|
if not isinstance(selections, list):
|
|
return objects
|
|
for selection in selections:
|
|
if not isinstance(selection, dict):
|
|
continue
|
|
obj = selection.get("object")
|
|
if isinstance(obj, dict) and obj.get("kind") != "null":
|
|
objects.append(obj)
|
|
return objects
|
|
|
|
def _axis_reference_from_feature_selections(selections: Any) -> Optional[Dict[str, Any]]:
|
|
for obj in _selection_objects(selections):
|
|
axis = _extract_axis_reference(obj)
|
|
if axis:
|
|
axis["source"] = "feature_selection_axis"
|
|
return axis
|
|
return None
|
|
|
|
def _host_face_from_feature_selections(selections: Any) -> Optional[Dict[str, Any]]:
|
|
for obj in _selection_objects(selections):
|
|
if obj.get("kind") != "face":
|
|
continue
|
|
surface = obj.get("surface") if isinstance(obj.get("surface"), dict) else {}
|
|
frame = obj.get("frame") if isinstance(obj.get("frame"), dict) else {}
|
|
if not frame:
|
|
continue
|
|
normal = frame.get("normal") or (surface.get("plane_params") or [0, 0, 1])[:3]
|
|
origin = frame.get("origin_mm")
|
|
if not origin:
|
|
plane_params = surface.get("plane_params")
|
|
if isinstance(plane_params, list) and len(plane_params) >= 6:
|
|
origin = [float(v) * 1000 for v in plane_params[3:6]]
|
|
if not origin:
|
|
origin = [0, 0, 0]
|
|
x_dir = frame.get("x_dir") or [1, 0, 0]
|
|
y_dir = frame.get("y_dir") or [0, 1, 0]
|
|
origin_values = list(origin)
|
|
x_values = list(x_dir)
|
|
y_values = list(y_dir)
|
|
normal_values = list(normal)
|
|
return {
|
|
"surface": surface,
|
|
"frame": {
|
|
"origin_mm": [float(v) for v in (origin_values + [0, 0, 0])[:3]],
|
|
"x_dir": [float(v) for v in (x_values + [0, 0, 0])[:3]],
|
|
"y_dir": [float(v) for v in (y_values + [0, 0, 0])[:3]],
|
|
"normal": [float(v) for v in (normal_values + [0, 0, 1])[:3]],
|
|
},
|
|
"source": "feature_selection_face",
|
|
}
|
|
return None
|