From 886390843013708ba39e725e8ad3d59789b94b71 Mon Sep 17 00:00:00 2001 From: ganjihong Date: Wed, 26 Aug 2026 14:39:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(json=5Fto=5Fcdsl):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8F=82=E8=80=83=E5=B9=B3=E9=9D=A2=E4=B8=8A=E6=97=8B=E8=BD=AC?= =?UTF-8?q?=E8=BD=AE=E5=BB=93=E7=9A=84=E5=B7=A5=E4=BD=9C=E5=B9=B3=E9=9D=A2?= =?UTF-8?q?=E9=87=8D=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- json_to_cdsl/evidence_v2_to_cdsl.py | 127 +++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/json_to_cdsl/evidence_v2_to_cdsl.py b/json_to_cdsl/evidence_v2_to_cdsl.py index 251d48a5..1012ad55 100644 --- a/json_to_cdsl/evidence_v2_to_cdsl.py +++ b/json_to_cdsl/evidence_v2_to_cdsl.py @@ -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 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") 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. rotation = [matrix[0:3], matrix[4:7], matrix[8: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)]) y_dir = _normalize([inverse_rotation[row][1] for row in range(3)]) 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} +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: """Build a workplane from an exporter-captured planar face signature.""" 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_workplanes_by_parent_source: 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 = [] for feature in sketch_features: 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) 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) + 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): parent_source_id = _identity_key(parent) if parent_source_id: 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: 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]] = [] previous_id: str | None = None 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: 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) 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] + # 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": atomic_id = "hole_wizard" 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"]: 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"] + 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", []): if reference not in selectors: selectors.append(reference)