diff --git a/backend/engine/cdsl_engine/runtime.py b/backend/engine/cdsl_engine/runtime.py index c370ad6f..80c1d1ae 100644 --- a/backend/engine/cdsl_engine/runtime.py +++ b/backend/engine/cdsl_engine/runtime.py @@ -702,9 +702,20 @@ def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec) -> host_frame.get(key) is not None for key in ("origin_mm", "x_dir", "normal") ) if positions_are_local: - for key in ("origin_mm", "x_dir", "y_dir", "normal"): + for key in ("origin_mm", "x_dir", "normal"): if host_frame.get(key): host_frame[key] = _reflect_point(host_frame[key], plane, vector=key != "origin_mm") + # #1 y_dir 保留:PlaneSpec 现在会尊重显式正交 y_dir。镜像后 frame 的 + # canonical y 轴必须是 n×x(x 已反射 → y 反转),否则反射后的 frame + # 会保留反射前的 y_dir,与下方"局部坐标 v 取反"双重翻转。 + x_reflected = host_frame.get("x_dir") + n_reflected = host_frame.get("normal") + if x_reflected is not None and n_reflected is not None: + host_frame["y_dir"] = [ + n_reflected[1] * x_reflected[2] - n_reflected[2] * x_reflected[1], + n_reflected[2] * x_reflected[0] - n_reflected[0] * x_reflected[2], + n_reflected[0] * x_reflected[1] - n_reflected[1] * x_reflected[0], + ] # See _mirrored_sketch: the canonical reflected plane reverses local # y, so local hole coordinates must do the same. for position in params.get("positions") or []: diff --git a/backend/engine/cdsl_engine/runtime_types.py b/backend/engine/cdsl_engine/runtime_types.py index 242c1f39..def85b0a 100644 --- a/backend/engine/cdsl_engine/runtime_types.py +++ b/backend/engine/cdsl_engine/runtime_types.py @@ -7,6 +7,7 @@ any geometry adapter without importing OCC objects. from __future__ import annotations +import warnings from dataclasses import dataclass, field from math import sqrt from typing import Any, Iterable @@ -14,6 +15,10 @@ from typing import Any, Iterable Vector3 = tuple[float, float, float] +# y_dir 与 x_dir / normal 点积的绝对值不超过该值时,认为 y_dir 是正交的, +# 予以保留;否则视为偏斜数据,正交化并显式警告。 +_Y_DIR_ORTHOGONALITY_TOL = 1e-6 + def _vector3(value: Any, *, field_name: str) -> Vector3: if not isinstance(value, (list, tuple)) or len(value) != 3: @@ -176,7 +181,25 @@ class PlaneSpec: x_raw = _vector3(value.get("x_dir"), field_name="plane.x_dir") projected_x = tuple(x_raw[index] - _dot(x_raw, normal) * normal[index] for index in range(3)) x_dir = _unit(projected_x, field_name="plane.x_dir") - y_dir = _unit(_cross(normal, x_dir), field_name="plane.y_dir") + generated = _unit(_cross(normal, x_dir), field_name="plane.y_dir") + y_raw = value.get("y_dir") + if y_raw is None: + # y_dir 缺失:用 normal × x_dir 补全右手系(默认行为)。 + y_dir = generated + else: + y_vec = _unit(_vector3(y_raw, field_name="plane.y_dir"), field_name="plane.y_dir") + if abs(_dot(y_vec, x_dir)) <= _Y_DIR_ORTHOGONALITY_TOL and abs(_dot(y_vec, normal)) <= _Y_DIR_ORTHOGONALITY_TOL: + # 输入 y_dir 与 x_dir / normal 正交:尊重文档作者给的坐标方向, + # 不再静默丢弃(SolidWorks 导出的非标准 y_dir 得以保留)。 + y_dir = y_vec + else: + # 偏斜 y_dir:正交化并显式警告,避免"静默丢语义"。 + warnings.warn( + f"plane y_dir {list(y_vec)} is not orthogonal to x_dir/normal; re-orthogonalized to {list(generated)}", + UserWarning, + stacklevel=2, + ) + y_dir = generated return cls(origin_mm=origin, x_dir=x_dir, y_dir=y_dir, normal=normal) def as_dict(self) -> dict[str, list[float]]: diff --git a/backend/engine/cdsl_engine/sketch_solver.py b/backend/engine/cdsl_engine/sketch_solver.py index c9c5e4a6..29bff622 100644 --- a/backend/engine/cdsl_engine/sketch_solver.py +++ b/backend/engine/cdsl_engine/sketch_solver.py @@ -44,14 +44,33 @@ def _to_3d(workplane: _Ctx, u: float, v: float) -> list[float]: origin = workplane.get("origin_mm") or [0, 0, 0] x_dir = workplane.get("x_dir") or [1, 0, 0] normal = workplane.get("normal") or [0, 0, 1] - y_dir = [ - normal[1] * x_dir[2] - normal[2] * x_dir[1], - normal[2] * x_dir[0] - normal[0] * x_dir[2], - normal[0] * x_dir[1] - normal[1] * x_dir[0], - ] + y_raw = workplane.get("y_dir") + y_dir = _default_y_dir(x_dir, normal) + if y_raw: + magnitude = math.sqrt(sum(component * component for component in y_raw)) + if magnitude > 1e-12: + y_unit = [component / magnitude for component in y_raw] + # 与 PlaneSpec.from_mapping 同策略:只有与 x_dir / normal 正交的 + # y_dir 才尊重(SolidWorks 导出的 y_dir==x_dir 占位数据与 X 平行, + # 直接使用会让轮廓塌缩成一条线,必须回退到 normal×x_dir)。 + if abs(_dot(y_unit, x_dir)) <= 1e-6 and abs(_dot(y_unit, normal)) <= 1e-6: + y_dir = y_unit return [origin[0] + u * x_dir[0] + v * y_dir[0], origin[1] + u * x_dir[1] + v * y_dir[1], origin[2] + u * x_dir[2] + v * y_dir[2]] +def _dot(left: Iterable[float], right: Iterable[float]) -> float: + return sum(a * b for a, b in zip(left, right)) + + +def _default_y_dir(x_dir: Iterable[float], normal: Iterable[float]) -> list[float]: + x, n = list(x_dir), list(normal) + return [ + n[1] * x[2] - n[2] * x[1], + n[2] * x[0] - n[0] * x[2], + n[0] * x[1] - n[1] * x[0], + ] + + def _transform_contours(contours: list[_Ctx], workplane: _Ctx) -> list[_Ctx]: transformed: list[_Ctx] = [] normal = workplane.get("normal") or [0, 0, 1]