167 lines
8.9 KiB
Python
167 lines
8.9 KiB
Python
"""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_exact:box_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()
|