120 lines
5.3 KiB
Python
120 lines
5.3 KiB
Python
"""#1 y_dir 静默丢弃:PlaneSpec.from_mapping 与 sketch_solver 必须尊重输入的 y_dir。
|
||
|
||
中文说明
|
||
--------
|
||
这个文件在测试什么(issue #1「y_dir 静默丢弃」的回归测试):
|
||
|
||
1. 背景:SolidWorks 导出草图常带冗余或非正交的 y_dir。
|
||
runtime 的 PlaneSpec.from_mapping(runtime_types.py:172-180)与
|
||
sketch_solver._to_3d(sketch_solver.py:43-52)都**无视输入的
|
||
y_dir 字段**,永远用 normal × x_dir 叉积补全。后果:
|
||
a) 文档作者显式给出的**正交** y_dir 被悄悄替换成叉积结果
|
||
(当 x_dir 与 normal 不正交时,重建的 y_dir 与真实几何不符);
|
||
b) **偏斜** y_dir 被静默丢弃,没有任何提示,用户不知道
|
||
坐标系被正交化过了。
|
||
|
||
2. 本测试把 y_dir 处理契约固定下来:
|
||
- 输入 y_dir 存在且与 x_dir / normal 正交 → **保留**输入值;
|
||
- 输入 y_dir 存在但偏斜(非正交)→ 正交化 + **显式 UserWarning**;
|
||
- 输入 y_dir 缺失 → 用 normal × x_dir 补全(回归护栏);
|
||
- sketch_solver 的轮廓点变换(_transform_contours → _to_3d)
|
||
同样尊重正交的输入 y_dir。
|
||
|
||
3. sys.path 说明:把 backend/engine 加入搜索路径,直接 import
|
||
cdsl_engine 包内模块(与前面几个回归测试风格一致)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT / "backend"))
|
||
sys.path.insert(0, str(ROOT / "backend" / "engine"))
|
||
|
||
from cdsl_engine.runtime_types import PlaneSpec # noqa: E402
|
||
from cdsl_engine.sketch_solver import _transform_contours # noqa: E402
|
||
|
||
|
||
def _assert_vector_close(testcase: unittest.TestCase, actual: tuple | list, expected: tuple | list, *, tol: float = 1e-6) -> None:
|
||
testcase.assertEqual(len(actual), len(expected))
|
||
for left, right in zip(actual, expected):
|
||
testcase.assertAlmostEqual(float(left), float(right), delta=tol)
|
||
|
||
|
||
class PlaneYDirPreservationTests(unittest.TestCase):
|
||
"""y_dir 处理契约:正交保留 / 偏斜警告 / 缺失补全。"""
|
||
|
||
def test_orthogonal_input_y_dir_is_preserved(self) -> None:
|
||
"""主契约:输入的正交 y_dir 必须被保留,而不是被叉积结果覆盖。
|
||
|
||
构造一个 x_dir 与 normal 不垂直的 frame(这是 SolidWorks 导出里
|
||
最常见的情形):x_dir=(1,0,0)、normal=(0,1,0)。此时
|
||
normal × x_dir = (0,0,-1),但文档显式给出 y_dir=(0,0,1)。
|
||
修复前:PlaneSpec.y_dir == (0,0,-1)(静默丢弃,红灯)。
|
||
修复后:PlaneSpec.y_dir == (0,0,1)(保留输入,绿灯)。
|
||
"""
|
||
plane = PlaneSpec.from_mapping({
|
||
"origin_mm": [0.0, 0.0, 0.0],
|
||
"x_dir": [1.0, 0.0, 0.0],
|
||
"y_dir": [0.0, 0.0, 1.0],
|
||
"normal": [0.0, 1.0, 0.0],
|
||
})
|
||
_assert_vector_close(self, plane.y_dir, (0.0, 0.0, 1.0))
|
||
|
||
def test_skewed_input_y_dir_orthogonalizes_and_warns(self) -> None:
|
||
"""主契约:偏斜 y_dir 必须被正交化,并且**显式**发出 UserWarning。
|
||
|
||
输入 y_dir=(0, 1, 1) 与 normal=(0,0,1) 的夹角不是 90°,无法直接
|
||
作为右手系 y 轴。修复前:静默替换为 normal × x_dir(无警告,红灯)。
|
||
修复后:正交化为 normal × x_dir,同时 raise UserWarning(绿灯)。
|
||
"""
|
||
with self.assertWarns(UserWarning):
|
||
plane = PlaneSpec.from_mapping({
|
||
"origin_mm": [0.0, 0.0, 0.0],
|
||
"x_dir": [1.0, 0.0, 0.0],
|
||
"y_dir": [0.0, 1.0, 1.0],
|
||
"normal": [0.0, 0.0, 1.0],
|
||
})
|
||
# normal × x_dir = (0,0,1) × (1,0,0) = (0,1,0),且为右手系补全
|
||
_assert_vector_close(self, plane.y_dir, (0.0, 1.0, 0.0))
|
||
|
||
def test_missing_y_dir_gets_orthonormal_default(self) -> None:
|
||
"""回归护栏:y_dir 字段缺失时,仍用 normal × x_dir 补全。
|
||
|
||
修复前后都应通过,这条测试防止我们把默认补全路径改坏。
|
||
"""
|
||
plane = PlaneSpec.from_mapping({
|
||
"origin_mm": [0.0, 0.0, 0.0],
|
||
"x_dir": [1.0, 0.0, 0.0],
|
||
"normal": [0.0, 0.0, 1.0],
|
||
})
|
||
_assert_vector_close(self, plane.y_dir, (0.0, 1.0, 0.0))
|
||
|
||
def test_contour_transform_respects_input_y_dir(self) -> None:
|
||
"""端到端:sketch_solver 的轮廓点变换必须尊重正交的输入 y_dir。
|
||
|
||
workplane 与 test_orthogonal_input_y_dir_is_preserved 相同
|
||
(x=(1,0,0), n=(0,1,0), 输入 y=(0,0,1))。一条从 (0,0) 到 (0,1)
|
||
的轮廓直线,修复前 _to_3d 用默认 y_dir=(0,0,-1) 变换,终点落到
|
||
(0,0,-1);修复后用输入 y_dir=(0,0,1),终点落到 (0,0,1)。
|
||
"""
|
||
workplane = {
|
||
"origin_mm": [0.0, 0.0, 0.0],
|
||
"x_dir": [1.0, 0.0, 0.0],
|
||
"y_dir": [0.0, 0.0, 1.0],
|
||
"normal": [0.0, 1.0, 0.0],
|
||
}
|
||
contours = _transform_contours([
|
||
{"type": "line", "start_mm": [0.0, 0.0], "end_mm": [0.0, 1.0]},
|
||
], workplane)
|
||
_assert_vector_close(self, contours[0]["start_mm"], (0.0, 0.0, 0.0))
|
||
_assert_vector_close(self, contours[0]["end_mm"], (0.0, 0.0, 1.0))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|