fix(engine): 保留并正交化平面 y_dir 方向

This commit is contained in:
2026-08-27 14:36:08 +08:00
parent 2ef31db17e
commit c5f789c385
3 changed files with 60 additions and 7 deletions
+12 -1
View File
@@ -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") host_frame.get(key) is not None for key in ("origin_mm", "x_dir", "normal")
) )
if positions_are_local: 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): if host_frame.get(key):
host_frame[key] = _reflect_point(host_frame[key], plane, vector=key != "origin_mm") 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 # See _mirrored_sketch: the canonical reflected plane reverses local
# y, so local hole coordinates must do the same. # y, so local hole coordinates must do the same.
for position in params.get("positions") or []: for position in params.get("positions") or []:
+24 -1
View File
@@ -7,6 +7,7 @@ any geometry adapter without importing OCC objects.
from __future__ import annotations from __future__ import annotations
import warnings
from dataclasses import dataclass, field from dataclasses import dataclass, field
from math import sqrt from math import sqrt
from typing import Any, Iterable from typing import Any, Iterable
@@ -14,6 +15,10 @@ from typing import Any, Iterable
Vector3 = tuple[float, float, float] 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: def _vector3(value: Any, *, field_name: str) -> Vector3:
if not isinstance(value, (list, tuple)) or len(value) != 3: 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") 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)) 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") 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) return cls(origin_mm=origin, x_dir=x_dir, y_dir=y_dir, normal=normal)
def as_dict(self) -> dict[str, list[float]]: def as_dict(self) -> dict[str, list[float]]:
+24 -5
View File
@@ -44,14 +44,33 @@ def _to_3d(workplane: _Ctx, u: float, v: float) -> list[float]:
origin = workplane.get("origin_mm") or [0, 0, 0] origin = workplane.get("origin_mm") or [0, 0, 0]
x_dir = workplane.get("x_dir") or [1, 0, 0] x_dir = workplane.get("x_dir") or [1, 0, 0]
normal = workplane.get("normal") or [0, 0, 1] normal = workplane.get("normal") or [0, 0, 1]
y_dir = [ y_raw = workplane.get("y_dir")
normal[1] * x_dir[2] - normal[2] * x_dir[1], y_dir = _default_y_dir(x_dir, normal)
normal[2] * x_dir[0] - normal[0] * x_dir[2], if y_raw:
normal[0] * x_dir[1] - normal[1] * x_dir[0], 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]] 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]: def _transform_contours(contours: list[_Ctx], workplane: _Ctx) -> list[_Ctx]:
transformed: list[_Ctx] = [] transformed: list[_Ctx] = []
normal = workplane.get("normal") or [0, 0, 1] normal = workplane.get("normal") or [0, 0, 1]