fix(json_to_cdsl): 修复参考平面上旋转轮廓的工作平面重建
This commit is contained in:
@@ -184,9 +184,22 @@ def _workplane(sketch: dict[str, Any]) -> dict[str, Any]:
|
|||||||
A conservative XY fallback keeps the record valid when the exporter did
|
A conservative XY fallback keeps the record valid when the exporter did
|
||||||
not provide a usable matrix.
|
not provide a usable matrix.
|
||||||
"""
|
"""
|
||||||
|
workplane = _workplane_from_matrix(sketch)
|
||||||
|
if workplane is not None:
|
||||||
|
return workplane
|
||||||
|
return {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "y_dir": [0.0, 1.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_from_matrix(sketch: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
"""Parse the exporter's model-to-sketch matrix into a workplane.
|
||||||
|
|
||||||
|
Returns None when no usable rigid rotation can be extracted (missing
|
||||||
|
matrix or a non-orthonormal frame). The caller then decides between a
|
||||||
|
rebuilt workplane and the conservative XY fallback.
|
||||||
|
"""
|
||||||
matrix = sketch.get("model_to_sketch_transform")
|
matrix = sketch.get("model_to_sketch_transform")
|
||||||
if not isinstance(matrix, list) or len(matrix) != 16 or not all(isinstance(item, (int, float)) for item in matrix):
|
if not isinstance(matrix, list) or len(matrix) != 16 or not all(isinstance(item, (int, float)) for item in matrix):
|
||||||
return {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "y_dir": [0.0, 1.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
return None
|
||||||
# The exporter serializes MathTransform in row-major form.
|
# The exporter serializes MathTransform in row-major form.
|
||||||
rotation = [matrix[0:3], matrix[4:7], matrix[8:11]]
|
rotation = [matrix[0:3], matrix[4:7], matrix[8:11]]
|
||||||
translation = [matrix[3], matrix[7], matrix[11]]
|
translation = [matrix[3], matrix[7], matrix[11]]
|
||||||
@@ -195,9 +208,69 @@ def _workplane(sketch: dict[str, Any]) -> dict[str, Any]:
|
|||||||
x_dir = _normalize([inverse_rotation[row][0] for row in range(3)])
|
x_dir = _normalize([inverse_rotation[row][0] for row in range(3)])
|
||||||
y_dir = _normalize([inverse_rotation[row][1] for row in range(3)])
|
y_dir = _normalize([inverse_rotation[row][1] for row in range(3)])
|
||||||
normal = _normalize(_cross(x_dir, y_dir))
|
normal = _normalize(_cross(x_dir, y_dir))
|
||||||
|
# A rigid model-to-sketch rotation keeps the sketch axes orthogonal. The
|
||||||
|
# exporter sometimes emits a non-orthogonal matrix for profiles placed on
|
||||||
|
# a copied reference plane; only trust a frame that is truly orthonormal.
|
||||||
|
if abs(x_dir[0] * y_dir[0] + x_dir[1] * y_dir[1] + x_dir[2] * y_dir[2]) > 1e-9:
|
||||||
|
return None
|
||||||
return {"origin_mm": [_mm(value) for value in origin_m], "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
return {"origin_mm": [_mm(value) for value in origin_m], "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_is_orthonormal(workplane: dict[str, Any]) -> bool:
|
||||||
|
"""True when a workplane's sketch axes describe an orthonormal frame."""
|
||||||
|
x_dir = workplane.get("x_dir")
|
||||||
|
y_dir = workplane.get("y_dir")
|
||||||
|
if not x_dir or not y_dir:
|
||||||
|
return False
|
||||||
|
return abs(x_dir[0] * y_dir[0] + x_dir[1] * y_dir[1] + x_dir[2] * y_dir[2]) <= 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_rebuilt(parent_plane: dict[str, Any] | None, axis_direction: list[float] | None) -> dict[str, Any] | None:
|
||||||
|
"""Rebuild a workplane from the parent reference plane and the revolve axis.
|
||||||
|
|
||||||
|
When the exporter's matrix is unusable, the profile still lies on its
|
||||||
|
parent reference plane and the revolve axis runs along the sketch V axis.
|
||||||
|
The captured axis direction is world space, so V maps directly onto it and
|
||||||
|
U is completed by the cross product to keep a right-handed frame.
|
||||||
|
"""
|
||||||
|
if not parent_plane or not axis_direction:
|
||||||
|
return None
|
||||||
|
normal = _normalize(list(parent_plane.get("normal") or []))
|
||||||
|
y_dir = _normalize(list(axis_direction))
|
||||||
|
if abs(normal[0] * y_dir[0] + normal[1] * y_dir[1] + normal[2] * y_dir[2]) > 0.9:
|
||||||
|
# The axis would lie parallel to the plane normal and the revolve
|
||||||
|
# would degenerate to zero volume; do not trust this rebuild.
|
||||||
|
return None
|
||||||
|
x_dir = _normalize(_cross(y_dir, normal))
|
||||||
|
origin_mm = list(parent_plane.get("origin_mm") or [0.0, 0.0, 0.0])
|
||||||
|
return {"origin_mm": origin_mm, "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
||||||
|
|
||||||
|
|
||||||
|
def _axis_in_world(axis: dict[str, Any], workplane: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Transform a revolve axis captured in sketch-local coordinates to world space."""
|
||||||
|
origin = axis.get("origin_mm")
|
||||||
|
direction = axis.get("direction")
|
||||||
|
if not origin or not direction or not workplane.get("x_dir"):
|
||||||
|
return axis
|
||||||
|
origin_mm = workplane.get("origin_mm") or [0.0, 0.0, 0.0]
|
||||||
|
x_dir = workplane["x_dir"]
|
||||||
|
y_dir = workplane.get("y_dir") or [0.0, 1.0, 0.0]
|
||||||
|
normal = workplane.get("normal") or [0.0, 0.0, 1.0]
|
||||||
|
world_origin = [
|
||||||
|
origin_mm[index] + origin[0] * x_dir[index] + origin[1] * y_dir[index] + origin[2] * normal[index]
|
||||||
|
for index in range(3)
|
||||||
|
]
|
||||||
|
world_direction = _normalize([
|
||||||
|
direction[0] * x_dir[index] + direction[1] * y_dir[index] + direction[2] * normal[index]
|
||||||
|
for index in range(3)
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
**axis,
|
||||||
|
"origin_mm": [round(value, 9) for value in world_origin],
|
||||||
|
"direction": world_direction if math.sqrt(sum(c * c for c in world_direction)) > 1e-12 else list(direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _workplane_from_plane_reference(reference: Any) -> dict[str, Any] | None:
|
def _workplane_from_plane_reference(reference: Any) -> dict[str, Any] | None:
|
||||||
"""Build a workplane from an exporter-captured planar face signature."""
|
"""Build a workplane from an exporter-captured planar face signature."""
|
||||||
if not isinstance(reference, dict):
|
if not isinstance(reference, dict):
|
||||||
@@ -892,6 +965,25 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
sketch_id_by_name = {str(feature.get("name")): sketch_id_by_source[source_id] for feature in sketch_features if (source_id := _identity_key(feature)) and feature.get("name") is not None}
|
sketch_id_by_name = {str(feature.get("name")): sketch_id_by_source[source_id] for feature in sketch_features if (source_id := _identity_key(feature)) and feature.get("name") is not None}
|
||||||
sketch_workplanes_by_parent_source: dict[str, tuple[str, dict[str, Any]]] = {}
|
sketch_workplanes_by_parent_source: dict[str, tuple[str, dict[str, Any]]] = {}
|
||||||
sketch_workplanes_by_parent_name: dict[str, tuple[str, dict[str, Any]]] = {}
|
sketch_workplanes_by_parent_name: dict[str, tuple[str, dict[str, Any]]] = {}
|
||||||
|
# sketch source id -> the parent reference-plane source id it is drawn on
|
||||||
|
sketch_parent_plane_by_source: dict[str, str] = {}
|
||||||
|
# sketch source id -> the world-space direction of its revolve axis
|
||||||
|
sketch_axis_direction_by_source: dict[str, list[float]] = {}
|
||||||
|
# sketch source ids whose exporter matrix did not form an orthonormal frame
|
||||||
|
sketch_matrix_unreliable: set[str] = set()
|
||||||
|
sketch_record_by_id: dict[str, dict[str, Any]] = {}
|
||||||
|
# Prescan the revolve features so each profile's workplane can be rebuilt
|
||||||
|
# from the axis direction captured on its own revolve feature.
|
||||||
|
for feature in features:
|
||||||
|
if _feature_family(feature) not in {"Revolution", "RevCut"}:
|
||||||
|
continue
|
||||||
|
props, _ = _record_values(feature)
|
||||||
|
axis, _ = _axis_from_reference(props.get("Axis"))
|
||||||
|
if not axis or not axis.get("direction"):
|
||||||
|
continue
|
||||||
|
for parent in _record_parents(feature):
|
||||||
|
if str(parent.get("effective_type") or "") in SKETCH_TYPES and (sketch_source_id := _identity_key(parent)):
|
||||||
|
sketch_axis_direction_by_source.setdefault(sketch_source_id, list(axis["direction"]))
|
||||||
sketches = []
|
sketches = []
|
||||||
for feature in sketch_features:
|
for feature in sketch_features:
|
||||||
source_id = _identity_key(feature)
|
source_id = _identity_key(feature)
|
||||||
@@ -901,10 +993,15 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
workplane = _workplane(sketch)
|
workplane = _workplane(sketch)
|
||||||
record = {"id": sketch_id_by_source[source_id], "name": str(feature.get("name") or sketch_id_by_source[source_id]), "role": "profile", "workplane": workplane, "profile": _analytic_profile(sketch)}
|
record = {"id": sketch_id_by_source[source_id], "name": str(feature.get("name") or sketch_id_by_source[source_id]), "role": "profile", "workplane": workplane, "profile": _analytic_profile(sketch)}
|
||||||
sketches.append(record)
|
sketches.append(record)
|
||||||
|
sketch_record_by_id[record["id"]] = record
|
||||||
|
if not _workplane_is_orthonormal(workplane):
|
||||||
|
sketch_matrix_unreliable.add(source_id)
|
||||||
for parent in _record_parents(feature):
|
for parent in _record_parents(feature):
|
||||||
parent_source_id = _identity_key(parent)
|
parent_source_id = _identity_key(parent)
|
||||||
if parent_source_id:
|
if parent_source_id:
|
||||||
sketch_workplanes_by_parent_source.setdefault(parent_source_id, (record["id"], workplane))
|
sketch_workplanes_by_parent_source.setdefault(parent_source_id, (record["id"], workplane))
|
||||||
|
if _feature_family(parent) == "RefPlane":
|
||||||
|
sketch_parent_plane_by_source[source_id] = parent_source_id
|
||||||
if parent.get("name") is not None:
|
if parent.get("name") is not None:
|
||||||
sketch_workplanes_by_parent_name.setdefault(str(parent["name"]), (record["id"], workplane))
|
sketch_workplanes_by_parent_name.setdefault(str(parent["name"]), (record["id"], workplane))
|
||||||
|
|
||||||
@@ -944,6 +1041,11 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
cdsl_features: list[dict[str, Any]] = []
|
cdsl_features: list[dict[str, Any]] = []
|
||||||
previous_id: str | None = None
|
previous_id: str | None = None
|
||||||
diagnostics: list[dict[str, Any]] = []
|
diagnostics: list[dict[str, Any]] = []
|
||||||
|
# Sketch source ids whose workplane was successfully rebuilt once their
|
||||||
|
# parent reference plane was resolved. Their revolve axis is expressed in
|
||||||
|
# sketch-local coordinates and must be transformed to world space too.
|
||||||
|
sketch_fix_applied: set[str] = set()
|
||||||
|
sketch_source_by_id = {sketch_id: source_id for source_id, sketch_id in sketch_id_by_source.items()}
|
||||||
|
|
||||||
for feature in model_features:
|
for feature in model_features:
|
||||||
source_id = _identity_key(feature)
|
source_id = _identity_key(feature)
|
||||||
@@ -985,6 +1087,14 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
selectors.append(axis_selector)
|
selectors.append(axis_selector)
|
||||||
if props.get("Axis") and any("no captured semantic selections" in item for item in unresolved):
|
if props.get("Axis") and any("no captured semantic selections" in item for item in unresolved):
|
||||||
unresolved = [item for item in unresolved if "no captured semantic selections" not in item]
|
unresolved = [item for item in unresolved if "no captured semantic selections" not in item]
|
||||||
|
# When the profile workplane was rebuilt, the captured revolve axis
|
||||||
|
# is still in sketch-local coordinates; map it to world space so
|
||||||
|
# the cut lands on the resolved parent reference plane.
|
||||||
|
if sketch_id and (axis_source_id := sketch_source_by_id.get(sketch_id)) in sketch_fix_applied:
|
||||||
|
sketch_workplane = sketch_record_by_id.get(sketch_id)
|
||||||
|
axis = params.get("axis")
|
||||||
|
if isinstance(axis, dict) and sketch_workplane is not None:
|
||||||
|
params["axis"] = _axis_in_world(axis, sketch_workplane["workplane"])
|
||||||
elif family == "HoleWzd":
|
elif family == "HoleWzd":
|
||||||
atomic_id = "hole_wizard"
|
atomic_id = "hole_wizard"
|
||||||
params, positions = _hole_params(props, methods)
|
params, positions = _hole_params(props, methods)
|
||||||
@@ -1065,6 +1175,21 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
if "unresolved" not in params["plane"]:
|
if "unresolved" not in params["plane"]:
|
||||||
more_unresolved = [item for item in more_unresolved if item != "reference plane orientation was not captured"]
|
more_unresolved = [item for item in more_unresolved if item != "reference plane orientation was not captured"]
|
||||||
unresolved = [item for item in unresolved if item != "reference plane orientation was not captured"]
|
unresolved = [item for item in unresolved if item != "reference plane orientation was not captured"]
|
||||||
|
plane = params.get("plane")
|
||||||
|
if isinstance(plane, dict) and "origin_mm" in plane and "unresolved" not in plane:
|
||||||
|
# Profiles whose exporter matrix was unusable are rebuilt from
|
||||||
|
# this resolved reference plane (normal + origin) and the axis
|
||||||
|
# direction of the profile's own revolve feature.
|
||||||
|
for sketch_source_id, ref_source_id in sketch_parent_plane_by_source.items():
|
||||||
|
if ref_source_id != source_id or sketch_source_id not in sketch_matrix_unreliable:
|
||||||
|
continue
|
||||||
|
rebuilt = _workplane_rebuilt(plane, sketch_axis_direction_by_source.get(sketch_source_id))
|
||||||
|
if rebuilt is None:
|
||||||
|
continue
|
||||||
|
sketch_record = sketch_record_by_id.get(sketch_id_by_source.get(sketch_source_id) or "")
|
||||||
|
if sketch_record is not None:
|
||||||
|
sketch_record["workplane"] = rebuilt
|
||||||
|
sketch_fix_applied.add(sketch_source_id)
|
||||||
for reference in params.get("references", []):
|
for reference in params.get("references", []):
|
||||||
if reference not in selectors:
|
if reference not in selectors:
|
||||||
selectors.append(reference)
|
selectors.append(reference)
|
||||||
|
|||||||
Reference in New Issue
Block a user