Files
cdsl-cad/backend/tests/test_engine_circular_pattern_geometry.py
T

167 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""N2 pattern_circular —— 旋转几何契约层测试。
中文说明
--------
统计报告把 N2(直线/环形阵列)归为 A 类;runtime 的 pattern 采用源特征
重放范式(pattern_linear/_translated_node、pattern_mirror/_mirrored_node),
本次为 pattern_circular 补齐了缺失的"环形重放实现入口":旋转重放节点
_rotated_node)与 Rodrigues 旋转辅助(_rotated_vector/_rotated_point)。
本文件是三个测试角度中的**几何单元层**:只验证旋转数学与草图/参数变换
函数的正确性,不驱动 executor、不跑布尔运算。旋转是环形阵列几何正确性
的全部基础——若绕轴旋转的坐标变换有误,任何 count/包角组合都会产出错误
实例位置。runtime 端到端层见 test_engine_circular_pattern_runtime.py。
契约要点(与 runtime 层的分工):
1. Rodrigues 旋转是等距变换:轴向分量守恒、长度/到轴距离不变、
非轴向按 cos/sin 旋转 → 用 90°/180° 与随机向量精确断言坐标。
2. 方向约定:绕轴 direction 正角度为右手逆时针(+Z 轴把 +X 转到 +Y)。
3. _rotated_sketch 只旋转世界坐标(workplane frame 与 start/end/center_mm),
2D 局部实体坐标不动(frame 旋转后由草图求解器映射到新世界位置)。
4. _box_circular_is_exactbox_add 是固定世界轴对齐图元,仅坐标轴旋转
且每份转角为 180° 整数倍时才精确(对齐 _execute_mirror_pattern 的
box 坐标平面限制思路)。
sys.path 说明:把 backend 与 backend/engine 加入搜索路径,直接 import
cdsl_engine 包内模块直测 adapter(与既有测试风格一致)。
"""
from __future__ import annotations
import math
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 import ( # noqa: E402
_box_circular_is_exact,
_coordinate_axis_direction,
_rotated_point,
_rotated_sketch,
_rotated_vector,
)
from cdsl_engine.runtime_types import AxisSpec # noqa: E402
X_AXIS = AxisSpec(origin_mm=(0.0, 0.0, 0.0), direction=(1.0, 0.0, 0.0))
Y_AXIS = AxisSpec(origin_mm=(0.0, 0.0, 0.0), direction=(0.0, 1.0, 0.0))
Z_AXIS = AxisSpec(origin_mm=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0))
# Rodrigues 要求单位轴(运行时 AxisSpec.from_mapping 会单位化);测试直接
# 构造 dataclass,必须显式传入单位方向。
_UNIT_DIAGONAL = math.sqrt(1.0 / 3.0)
SKEW_AXIS = AxisSpec(origin_mm=(0.0, 0.0, 0.0),
direction=(_UNIT_DIAGONAL, _UNIT_DIAGONAL, _UNIT_DIAGONAL))
def _assert_vector_close(test: unittest.TestCase, actual, expected, *, places: int = 9) -> None:
for actual_component, expected_component in zip(actual, expected):
test.assertAlmostEqual(float(actual_component), float(expected_component), places=places)
class CircularPatternGeometryTests(unittest.TestCase):
def test_rotated_point_around_z_axis_cardinal_angles(self) -> None:
# +Z 轴右手逆时针:90° 把 (10, 0, 0) 转到 (0, 10, 0)180° 到 (-10, 0, 0)
# 270° 到 (0, -10, 0)。z 坐标不变。
point = (10.0, 0.0, 4.0)
for angle_deg, expected in [
(90.0, (0.0, 10.0, 4.0)),
(180.0, (-10.0, 0.0, 4.0)),
(270.0, (0.0, -10.0, 4.0)),
(360.0, (10.0, 0.0, 4.0)),
]:
with self.subTest(angle=angle_deg):
rotated = _rotated_point(point, Z_AXIS, math.radians(angle_deg))
_assert_vector_close(self, rotated, expected)
def test_rotated_point_negative_angle_rotates_clockwise(self) -> None:
# -90° 顺时针:+X 转到 -Y。
rotated = _rotated_point((10.0, 0.0, 0.0), Z_AXIS, math.radians(-90.0))
_assert_vector_close(self, rotated, (0.0, -10.0, 0.0))
def test_rotated_point_about_arbitrary_axis_keeps_axis_distance(self) -> None:
# 绕空间对角轴:轴向分量守恒、到轴距离不变(等距变换)。
point = (3.0, -2.0, 7.0)
rotated = _rotated_point(point, SKEW_AXIS, math.radians(40.0))
axial = sum(component * value for component, value in zip(point, SKEW_AXIS.direction))
rotated_axial = sum(component * value for component, value in zip(rotated, SKEW_AXIS.direction))
self.assertAlmostEqual(rotated_axial, axial, places=9)
distance_sq = sum(c * c for c in point) - axial * axial
rotated_distance_sq = sum(c * c for c in rotated) - rotated_axial * rotated_axial
self.assertAlmostEqual(rotated_distance_sq, distance_sq, places=9)
def test_rotated_point_around_offset_axis(self) -> None:
# 轴不过原点:先平移到轴、旋转、再平移回。绕 (10, 0, 0) 竖直轴转 90°,
# 点 (10, 5, 0) 的相对矢量 (0, 5, 0) 转到 (-5, 0, 0) → (5, 0, 0)。
offset_axis = AxisSpec(origin_mm=(10.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0))
rotated = _rotated_point((10.0, 5.0, 0.0), offset_axis, math.radians(90.0))
_assert_vector_close(self, rotated, (5.0, 0.0, 0.0))
def test_rotated_vector_preserves_length_and_normalizes_axis(self) -> None:
# 向量旋转不含平移项(原点到向量尾所在轴上的投影分量守恒)。
value = (2.0, 0.0, 0.0)
rotated = _rotated_vector(value, Z_AXIS, math.radians(90.0))
_assert_vector_close(self, rotated, (0.0, 2.0, 0.0))
for axis, expected in [
(X_AXIS, (2.0, 0.0, 0.0)), # 绕自身轴旋转不变
(Y_AXIS, (0.0, 0.0, -2.0)), # 绕 +Y 转 90°(右手)把 +X 转到 -Z
]:
with self.subTest(axis=axis.direction):
_assert_vector_close(self, _rotated_vector(value, axis, math.radians(90.0)), expected)
def test_rotated_vector_arbitrary_angle_is_length_preserving(self) -> None:
value = (1.0, 2.0, 3.0)
rotated = _rotated_vector(value, SKEW_AXIS, math.radians(71.0))
self.assertAlmostEqual(
sum(c * c for c in rotated), sum(c * c for c in value), places=9)
def test_rotated_sketch_rotates_workplane_and_world_contours_only(self) -> None:
# workplane frame(原点 + 三向量)与 contour 世界坐标点绕 +Z 转 90°;
# 2D 局部实体坐标保持原样(frame 旋转负责映射到新世界位置)。
sketch = {
"workplane": {"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]},
"entities": [{"type": "circle", "center": [0.0, 1.0], "radius_mm": 2.0}],
"contour_edges_mm": [{"start_mm": [1.0, 2.0, 0.0], "end_mm": [3.0, 4.0, 5.0],
"center_mm": [2.0, 3.0, 0.0]}],
"contour_regions_mm": [{"outer": [{"start_mm": [0.0, 1.0, 0.0],
"end_mm": [1.0, 0.0, 0.0]}]}],
}
rotated = _rotated_sketch(sketch, Z_AXIS, math.radians(90.0))
_assert_vector_close(self, rotated["workplane"]["origin_mm"], (0.0, 0.0, 0.0))
_assert_vector_close(self, rotated["workplane"]["x_dir"], (0.0, 1.0, 0.0))
_assert_vector_close(self, rotated["workplane"]["y_dir"], (-1.0, 0.0, 0.0))
_assert_vector_close(self, rotated["workplane"]["normal"], (0.0, 0.0, 1.0))
self.assertEqual(rotated["entities"], sketch["entities"])
edge = rotated["contour_edges_mm"][0]
_assert_vector_close(self, edge["start_mm"], (-2.0, 1.0, 0.0))
_assert_vector_close(self, edge["end_mm"], (-4.0, 3.0, 5.0))
_assert_vector_close(self, edge["center_mm"], (-3.0, 2.0, 0.0))
outer = rotated["contour_regions_mm"][0]["outer"][0]
_assert_vector_close(self, outer["start_mm"], (-1.0, 0.0, 0.0))
_assert_vector_close(self, outer["end_mm"], (0.0, 1.0, 0.0))
def test_box_circular_exactness_boundary(self) -> None:
# box_add 固定世界轴对齐:仅坐标轴旋转且每份转角为 180° 整数倍时精确。
self.assertTrue(_box_circular_is_exact(Z_AXIS, math.radians(180.0)))
self.assertTrue(_box_circular_is_exact(Z_AXIS, math.radians(360.0)))
self.assertFalse(_box_circular_is_exact(Z_AXIS, math.radians(90.0)))
self.assertFalse(_box_circular_is_exact(Z_AXIS, math.radians(45.0)))
# 绕 x/y 的非 180° 转角同样不可精确表达;空间对角轴任何转角都不行。
self.assertFalse(_box_circular_is_exact(X_AXIS, math.radians(90.0)))
self.assertFalse(_box_circular_is_exact(SKEW_AXIS, math.radians(180.0)))
def test_coordinate_axis_direction_detection(self) -> None:
self.assertTrue(_coordinate_axis_direction([0.0, 0.0, 1.0]))
self.assertTrue(_coordinate_axis_direction((0.0, -1.0, 0.0)))
self.assertFalse(_coordinate_axis_direction([1.0, 1.0, 1.0]))
self.assertFalse(_coordinate_axis_direction([1.0, 0.0, 0.5]))
if __name__ == "__main__":
unittest.main()