Files
2026-07-22 13:48:46 +08:00

901 lines
32 KiB
Python
Raw Permalink 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.
"""Comprehensive unit tests for the SimpleCAD API, covering basic operations and advanced features."""
import sys
import os
import io
import unittest
import numpy as np
import tempfile
import shutil
from contextlib import redirect_stdout
# 添加项目路径到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
import simplecadapi as scad
class TestBasicShapes(unittest.TestCase):
"""Tests for basic shape creation."""
def test_create_point(self):
"""Test create point."""
point = scad.make_point_rvertex(1, 2, 3)
# 暂时跳过坐标检查,因为点类型可能不同
self.assertIsInstance(point, scad.Vertex)
def test_create_line(self):
"""Test create line."""
line = scad.make_line_redge((0, 0, 0), (1, 0, 0))
self.assertIsInstance(line, scad.Edge)
def test_create_circle_edge(self):
"""Test create circle edge."""
circle_edge = scad.make_circle_redge((0, 0, 0), 1.0)
self.assertIsInstance(circle_edge, scad.Edge)
def test_create_circle_wire(self):
"""Test create circle wire."""
circle_wire = scad.make_circle_rwire((0, 0, 0), 1.0)
self.assertIsInstance(circle_wire, scad.Wire)
def test_create_circle_face(self):
"""Test create circle face."""
circle_face = scad.make_circle_rface((0, 0, 0), 1.0)
area = circle_face.get_area()
self.assertAlmostEqual(area, np.pi, places=6)
def test_create_rectangle_wire(self):
"""Test create rectangle wire."""
rect_wire = scad.make_rectangle_rwire(2.0, 1.0)
self.assertIsInstance(rect_wire, scad.Wire)
def test_create_rectangle_face(self):
"""Test create rectangle face."""
rect_face = scad.make_rectangle_rface(2.0, 1.0)
area = rect_face.get_area()
self.assertAlmostEqual(area, 2.0, places=6)
def test_create_box(self):
"""Test create box."""
box = scad.make_box_rsolid(1.0, 1.0, 1.0)
volume = box.get_volume()
self.assertAlmostEqual(volume, 1.0, places=6)
def test_create_cylinder(self):
"""Test create cylinder."""
cylinder = scad.make_cylinder_rsolid(1.0, 2.0)
volume = cylinder.get_volume()
expected_volume = np.pi * 1.0**2 * 2.0
self.assertAlmostEqual(volume, expected_volume, places=6)
def test_create_sphere(self):
"""Test create sphere."""
sphere = scad.make_sphere_rsolid(1.0)
volume = sphere.get_volume()
expected_volume = (4 / 3) * np.pi * 1.0**3
self.assertAlmostEqual(volume, expected_volume, places=5)
def test_create_cone(self):
"""Test create cone."""
# 测试标准圆锥体(尖锥)
cone = scad.make_cone_rsolid(2.0, 3.0)
volume = cone.get_volume()
expected_volume = (1 / 3) * np.pi * 2.0**2 * 3.0
self.assertAlmostEqual(volume, expected_volume, places=5)
self.assertIn("geom.primitive.cone", scad.list_tags(cone))
def test_create_truncated_cone(self):
"""Test create truncated cone."""
# 测试截锥体(顶面半径不为0
truncated_cone = scad.make_cone_rsolid(3.0, 4.0, 1.0)
volume = truncated_cone.get_volume()
# 截锥体积公式:V = (1/3)πh(R² + Rr + r²)
# 其中 R = 3.0, r = 1.0, h = 4.0
expected_volume = (1 / 3) * np.pi * 4.0 * (3.0**2 + 3.0 * 1.0 + 1.0**2)
self.assertAlmostEqual(volume, expected_volume, places=5)
self.assertIn("geom.primitive.cone", scad.list_tags(truncated_cone))
def test_create_cone_with_offset(self):
"""Test create cone with offset."""
# 测试底面中心偏移的圆锥体
offset_cone = scad.make_cone_rsolid(1.5, 2.0, bottom_face_center=(2, 2, 0))
self.assertIsInstance(offset_cone, scad.Solid)
self.assertIn("geom.primitive.cone", scad.list_tags(offset_cone))
def test_create_cone_with_axis(self):
"""Test create cone with axis."""
# 测试水平方向的圆锥体
horizontal_cone = scad.make_cone_rsolid(1.0, 3.0, axis=(1, 0, 0))
self.assertIsInstance(horizontal_cone, scad.Solid)
self.assertIn("geom.primitive.cone", scad.list_tags(horizontal_cone))
def test_create_arc(self):
"""Test create arc."""
arc = scad.make_three_point_arc_redge((0, 0, 0), (1, 1, 0), (2, 0, 0))
self.assertIsInstance(arc, scad.Edge)
def test_create_spline(self):
"""Test create spline."""
spline = scad.make_spline_redge(
control_points=[
(0.0, 0.0, 0.0),
(0.6, 1.0, 0.0),
(1.4, 1.0, 0.0),
(2.0, 0.0, 0.0),
]
)
self.assertIsInstance(spline, scad.Edge)
def test_create_segment_edge(self):
"""Test create segment edge."""
segment = scad.make_segment_redge((0, 0, 0), (1, 0, 0))
self.assertIsInstance(segment, scad.Edge)
def test_create_segment_wire(self):
"""Test create segment wire."""
segment_wire = scad.make_segment_rwire((0, 0, 0), (1, 0, 0))
self.assertIsInstance(segment_wire, scad.Wire)
def test_create_angle_arc_edge(self):
"""Test create angle arc edge."""
arc = scad.make_angle_arc_redge((0, 0, 0), 1.0, 0, np.pi / 2)
self.assertIsInstance(arc, scad.Edge)
def test_create_angle_arc_wire(self):
"""Test create angle arc wire."""
arc_wire = scad.make_angle_arc_rwire((0, 0, 0), 1.0, 0, np.pi / 2)
self.assertIsInstance(arc_wire, scad.Wire)
def test_create_three_point_arc_wire(self):
"""Test create three point arc wire."""
arc_wire = scad.make_three_point_arc_rwire((0, 0, 0), (1, 1, 0), (2, 0, 0))
self.assertIsInstance(arc_wire, scad.Wire)
def test_create_spline_wire(self):
"""Test create spline wire."""
spline_wire = scad.make_spline_rwire(
control_points=[
(0.0, 0.0, 0.0),
(0.6, 1.0, 0.0),
(1.4, 1.0, 0.0),
(2.0, 0.0, 0.0),
]
)
self.assertIsInstance(spline_wire, scad.Wire)
def test_create_polyline_wire(self):
"""Test create polyline wire."""
points = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)]
polyline_wire = scad.make_polyline_rwire(points)
self.assertIsInstance(polyline_wire, scad.Wire)
# 测试闭合多段线
closed_polyline = scad.make_polyline_rwire(points, closed=True)
self.assertIsInstance(closed_polyline, scad.Wire)
self.assertTrue(closed_polyline.is_closed())
def test_create_helix_edge(self):
"""Test create helix edge."""
helix = scad.make_helix_redge(pitch=1.0, height=3.0, radius=0.5)
self.assertIsInstance(helix, scad.Edge)
def test_create_helix_wire(self):
"""Test create helix wire."""
helix_wire = scad.make_helix_rwire(pitch=1.0, height=3.0, radius=0.5)
self.assertIsInstance(helix_wire, scad.Wire)
def test_new_function_error_handling(self):
"""Test new function error handling."""
# 测试无效参数
with self.assertRaises(ValueError):
scad.make_angle_arc_redge((0, 0, 0), -1.0, 0, np.pi / 2) # 负半径
with self.assertRaises(ValueError):
scad.make_angle_arc_redge((0, 0, 0), 1.0, 0, 0) # 相同角度
with self.assertRaises(ValueError):
scad.make_helix_redge(-1.0, 3.0, 0.5) # 负螺距
with self.assertRaises(ValueError):
scad.make_helix_redge(1.0, -3.0, 0.5) # 负高度
with self.assertRaises(ValueError):
scad.make_helix_redge(1.0, 3.0, -0.5) # 负半径
with self.assertRaises(ValueError):
scad.make_spline_redge(control_points=[(0, 0, 0)]) # 控制点不足
with self.assertRaises(ValueError):
scad.make_polyline_rwire([(0, 0, 0)]) # 点数不足
class TestTransformations(unittest.TestCase):
"""Tests for transformation operations."""
def setUp(self):
self.box = scad.make_box_rsolid(1.0, 1.0, 1.0)
def test_translate(self):
"""Test translate."""
translated = scad.translate_shape(self.box, (1, 0, 0))
self.assertIsInstance(translated, scad.Solid)
# 体积应保持不变
if isinstance(translated, scad.Solid):
self.assertAlmostEqual(
translated.get_volume(), self.box.get_volume(), places=6
)
def test_rotate(self):
"""Test rotate."""
rotated = scad.rotate_shape(self.box, np.pi / 4, (0, 0, 1))
self.assertIsInstance(rotated, scad.Solid)
# 体积应保持不变
if isinstance(rotated, scad.Solid):
self.assertAlmostEqual(
rotated.get_volume(), self.box.get_volume(), places=6
)
class Test3DOperations(unittest.TestCase):
"""Tests for 3D operations."""
def test_extrude(self):
"""Test extrude."""
rect = scad.make_rectangle_rface(2.0, 1.0)
extruded = scad.extrude_rsolid(rect, (0, 0, 1), 2.0)
self.assertIsInstance(extruded, scad.Solid)
# 体积应该是面积乘以高度
expected_volume = rect.get_area() * 2.0
self.assertAlmostEqual(extruded.get_volume(), expected_volume, places=6)
def test_revolve(self):
"""Test revolve."""
rect = scad.make_rectangle_rface(1.0, 2.0, center=(2, 0, 0))
revolved = scad.revolve_rsolid(rect, (0, 1, 0), 180, (0, 0, 0))
self.assertIsInstance(revolved, scad.Solid)
self.assertGreater(revolved.get_volume(), 0)
class TestBooleanOperations(unittest.TestCase):
"""Tests for boolean operations."""
def setUp(self):
self.box1 = scad.make_box_rsolid(2.0, 2.0, 2.0)
self.box2 = scad.make_box_rsolid(
1.0, 1.0, 3.0, bottom_face_center=(0.5, 0.5, 0)
)
def test_union(self):
"""Test union."""
result = scad.union_rsolid([self.box1, self.box2])
self.assertIsInstance(result, scad.Solid)
# 并集体积应该大于任一单独体积
self.assertGreater(result.get_volume(), self.box1.get_volume())
self.assertGreater(result.get_volume(), self.box2.get_volume())
def test_union_disconnected_solids(self):
"""Test union disconnected solids."""
box_far_1 = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(0, 0, 0))
box_far_2 = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(5, 0, 0))
box_far_3 = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(0, 5, 0))
solids = [box_far_1, box_far_2, box_far_3]
with self.assertRaises(scad.SimpleCADError) as ctx:
scad.union_rsolid(solids)
message = str(ctx.exception)
self.assertIn("union_rsolid", message)
self.assertIn("单个Solid结果", message)
def test_union_touching_boxes_cleans_splitter_faces(self):
"""Test union of face-touching boxes follows CadQuery-style clean behavior."""
box_left = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(0, 0, 0))
box_right = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(1.0, 0, 0))
stdout_buffer = io.StringIO()
with redirect_stdout(stdout_buffer):
result = scad.union_rsolid(box_left, box_right)
self.assertAlmostEqual(result.get_volume(), 2.0, places=6)
self.assertEqual(len(result.get_faces()), 6)
self.assertEqual(stdout_buffer.getvalue(), "")
def test_union_supports_fuzzy_tolerance(self):
"""Test union forwards CadQuery fuzzy tolerance to the OCC kernel."""
box_left = scad.make_box_rsolid(1.0, 1.0, 1.0, bottom_face_center=(0, 0, 0))
box_right = scad.make_box_rsolid(
1.0, 1.0, 1.0, bottom_face_center=(1.001, 0, 0)
)
with self.assertRaises(scad.SimpleCADError):
scad.union_rsolid(box_left, box_right)
with_tol = scad.union_rsolid(box_left, box_right, tol=1e-3)
self.assertIsInstance(with_tol, scad.Solid)
self.assertGreater(with_tol.get_volume(), 2.0)
self.assertLess(with_tol.get_volume(), 2.01)
def test_cut(self):
"""Test cut."""
result = scad.cut_rsolid(self.box1, self.box2)
self.assertIsInstance(result, scad.Solid)
# 差集体积应该小于原体积
self.assertLess(result.get_volume(), self.box1.get_volume())
def test_intersect(self):
"""Test intersect."""
result = scad.intersect_rsolid(self.box1, self.box2)
self.assertIsInstance(result, scad.Solid)
# 交集体积应该小于任一体积
self.assertLess(result.get_volume(), self.box1.get_volume())
self.assertLess(result.get_volume(), self.box2.get_volume())
def test_boolean_api_names(self):
"""Test canonical boolean API names."""
self.assertTrue(hasattr(scad, "union_rsolid"))
self.assertTrue(hasattr(scad, "cut_rsolid"))
self.assertTrue(hasattr(scad, "intersect_rsolid"))
class TestAdvancedFeatures(unittest.TestCase):
"""Tests for advanced feature operations."""
def setUp(self):
self.box = scad.make_box_rsolid(2.0, 2.0, 2.0)
self.box.auto_tag_faces("box")
def test_fillet(self):
"""Test fillet."""
# 获取所有边
edges = self.box.get_edges()
# 选择前4条边进行圆角
selected_edges = edges[:4]
try:
filleted = scad.fillet_rsolid(self.box, selected_edges, 0.2)
self.assertIsInstance(filleted, scad.Solid)
# 圆角后体积应该稍微减少
self.assertLess(filleted.get_volume(), self.box.get_volume())
except Exception as e:
self.skipTest(f"Fillet operation not fully implemented: {e}")
def test_chamfer(self):
"""Test chamfer."""
# 获取所有边
edges = self.box.get_edges()
# 选择前4条边进行倒角
selected_edges = edges[:4]
try:
chamfered = scad.chamfer_rsolid(self.box, selected_edges, 0.2)
self.assertIsInstance(chamfered, scad.Solid)
# 倒角后体积应该稍微减少
self.assertLess(chamfered.get_volume(), self.box.get_volume())
except Exception as e:
self.skipTest(f"Chamfer operation not fully implemented: {e}")
def test_shell(self):
"""Test shell."""
# 获取顶面
faces = self.box.get_faces()
top_faces = [face for face in faces if "face.top" in scad.list_tags(face)]
try:
shelled = scad.shell_rsolid(self.box, top_faces, 0.2)
self.assertIsInstance(shelled, scad.Solid)
# 抽壳后体积应该减少
self.assertLess(shelled.get_volume(), self.box.get_volume())
except Exception as e:
self.skipTest(f"Shell operation not fully implemented: {e}")
def test_loft(self):
"""Test loft."""
# 创建两个不同大小的矩形轮廓
rect1 = scad.create_rectangle_wire(2.0, 2.0, center=(0, 0, 0))
rect2 = scad.create_rectangle_wire(1.0, 1.0, center=(0, 0, 2))
try:
lofted = scad.loft_rsolid([rect1, rect2])
self.assertIsInstance(lofted, scad.Solid)
self.assertGreater(lofted.get_volume(), 0)
except Exception as e:
self.skipTest(f"Loft operation not fully implemented: {e}")
def test_linear_pattern(self):
"""Test linear pattern."""
small_box = scad.create_box(0.5, 0.5, 0.5)
pattern = scad.linear_pattern_rsolidlist(small_box, (1, 0, 0), 5, 1.0)
self.assertIsInstance(pattern, list)
# 检查复合体包含5个实体
solids = pattern
self.assertEqual(len(solids), 5)
self.assertIsInstance(solids[0], scad.Solid)
def test_radial_pattern(self):
"""Test radial pattern."""
small_box = scad.create_box(0.2, 0.2, 1.0, bottom_face_center=(2, 0, 0))
pattern = scad.radial_pattern_rsolidlist(
small_box, (0, 0, 0), (0, 0, 1), 6, 2 * np.pi
)
self.assertIsInstance(pattern, list)
# 检查复合体包含6个实体
solids = pattern
self.assertEqual(len(solids), 6)
self.assertIsInstance(solids[0], scad.Solid)
def test_mirror(self):
"""Test mirror."""
mirrored = scad.mirror_shape(self.box, (0, 0, 0), (1, 0, 0))
self.assertIsInstance(mirrored, scad.Solid)
# 镜像后体积应该保持不变
if isinstance(mirrored, scad.Solid):
self.assertAlmostEqual(
mirrored.get_volume(), self.box.get_volume(), places=6
)
class TestTagging(unittest.TestCase):
"""Tests for the tagging system."""
def setUp(self):
self.box = scad.create_box(1.0, 1.0, 1.0)
def test_apply_tag(self):
"""Test apply tag."""
scad.apply_tag(self.box, "test_box")
self.assertIn("test_box", scad.list_tags(self.box))
def test_multiple_tags(self):
"""Test multiple tags."""
scad.apply_tag(self.box, "tag1")
scad.apply_tag(self.box, "tag2")
tags = scad.list_tags(self.box)
self.assertIn("tag1", tags)
self.assertIn("tag2", tags)
def test_auto_tag_faces_box(self):
"""Test auto tag faces box."""
self.box.auto_tag_faces("box")
faces = self.box.get_faces()
# 检查是否有标记的面
tagged_faces = [face for face in faces if len(scad.list_tags(face)) > 0]
self.assertGreater(len(tagged_faces), 0)
def test_auto_tag_faces_cylinder(self):
"""Test auto tag faces cylinder."""
cylinder = scad.create_cylinder(1.0, 2.0)
cylinder.auto_tag_faces("cylinder")
faces = cylinder.get_faces()
# 检查是否有标记的面
tagged_faces = [face for face in faces if len(scad.list_tags(face)) > 0]
self.assertGreater(len(tagged_faces), 0)
def test_auto_tag_faces_sphere(self):
"""Test auto tag faces sphere."""
sphere = scad.create_sphere(1.0)
sphere.auto_tag_faces("sphere")
faces = sphere.get_faces()
# 球体应该只有一个面,且被标记为surface
self.assertEqual(len(faces), 1)
self.assertIn("face.surface", scad.list_tags(faces[0]))
class TestCoordinateSystem(unittest.TestCase):
"""Tests for coordinate system features."""
def test_world_coordinate_system(self):
"""Test world coordinate system."""
point = scad.make_point_rvertex(1, 0, 0)
# 暂时跳过坐标检查
self.assertIsInstance(point, scad.Vertex)
def test_workplane_translation(self):
"""Test workplane translation."""
with scad.SimpleWorkplane(origin=(1, 1, 1)):
point = scad.make_point_rvertex(1, 0, 0)
# 暂时跳过坐标检查
self.assertIsInstance(point, scad.Vertex)
def test_nested_workplane(self):
"""Test nested workplane."""
with scad.SimpleWorkplane(origin=(1, 0, 0)):
with scad.SimpleWorkplane(origin=(0, 1, 0)):
point = scad.make_point_rvertex(1, 0, 0)
# 暂时跳过坐标检查
self.assertIsInstance(point, scad.Vertex)
class TestExport(unittest.TestCase):
"""Tests for export functionality."""
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.box = scad.make_box_rsolid(1.0, 1.0, 1.0)
def tearDown(self):
shutil.rmtree(self.temp_dir)
def test_export_stl(self):
"""Test export STL."""
stl_path = os.path.join(self.temp_dir, "test.stl")
try:
scad.export_stl(self.box, stl_path)
# 检查文件是否创建
self.assertTrue(os.path.exists(stl_path))
# 检查文件是否有内容
self.assertGreater(os.path.getsize(stl_path), 0)
except Exception as e:
self.skipTest(f"STL export not fully implemented: {e}")
def test_export_step(self):
"""Test export STEP."""
step_path = os.path.join(self.temp_dir, "test.step")
try:
scad.export_step(self.box, step_path)
# 检查文件是否创建
self.assertTrue(os.path.exists(step_path))
# 检查文件是否有内容
self.assertGreater(os.path.getsize(step_path), 0)
except Exception as e:
self.skipTest(f"STEP export not fully implemented: {e}")
def test_export_multiple_shapes(self):
"""Test export multiple shapes."""
box1 = scad.make_box_rsolid(1.0, 1.0, 1.0)
box2 = scad.make_box_rsolid(0.5, 0.5, 0.5, bottom_face_center=(2, 0, 0))
stl_path = os.path.join(self.temp_dir, "multiple.stl")
try:
scad.export_stl([box1, box2], stl_path)
# 检查文件是否创建
self.assertTrue(os.path.exists(stl_path))
# 检查文件是否有内容
self.assertGreater(os.path.getsize(stl_path), 0)
except Exception as e:
self.skipTest(f"Multiple shapes export not fully implemented: {e}")
def test_export_nested_shape_list(self):
"""Test export nested shape list."""
box = scad.make_box_rsolid(0.8, 0.8, 0.8)
cylinder = scad.make_cylinder_rsolid(0.4, 1.0)
sphere = scad.make_sphere_rsolid(0.5)
nested_shapes = [box, [cylinder, sphere]]
step_path = os.path.join(self.temp_dir, "nested.step")
try:
scad.export_step(nested_shapes, step_path)
self.assertTrue(os.path.exists(step_path))
self.assertGreater(os.path.getsize(step_path), 0)
except Exception as e:
self.skipTest(f"Nested shapes export not fully implemented: {e}")
def test_export_step_multiple_solids_single_file(self):
"""Test export STEP multiple solids single file."""
box1 = scad.make_box_rsolid(1.0, 1.0, 1.0)
box2 = scad.make_box_rsolid(0.7, 0.7, 0.7, bottom_face_center=(2.0, 0, 0))
step_path = os.path.join(self.temp_dir, "assembly_like.step")
try:
scad.export_step([box1, box2], step_path)
self.assertTrue(os.path.exists(step_path))
self.assertGreater(os.path.getsize(step_path), 0)
with open(step_path, "r", encoding="utf-8", errors="ignore") as f:
step_text = f.read()
# STEP文本中每个实体通常对应一个MANIFOLD_SOLID_BREP定义
self.assertGreaterEqual(step_text.count("MANIFOLD_SOLID_BREP"), 2)
except Exception as e:
self.skipTest(
f"Multiple solids in one STEP export not fully implemented: {e}"
)
class TestComplexExamples(unittest.TestCase):
"""Tests for complex example workflows."""
def test_create_bracket(self):
"""Test create bracket."""
# 创建主体
base = scad.make_box_rsolid(10, 5, 2)
# 创建孔
hole1 = scad.make_cylinder_rsolid(1, 3, bottom_face_center=(2, 0, 0))
hole2 = scad.make_cylinder_rsolid(1, 3, bottom_face_center=(4, 0, 0))
# 组合
bracket = scad.cut_rsolid(base, hole1)
bracket = scad.cut_rsolid(bracket, hole2)
# 添加标签
scad.apply_tag(bracket, "bracket")
# 验证
self.assertIsInstance(bracket, scad.Solid)
self.assertIn("bracket", scad.list_tags(bracket))
self.assertLess(bracket.get_volume(), base.get_volume())
def test_create_gear_like_shape(self):
"""Test create gear like shape."""
# 创建基础圆盘
base_circle = scad.make_circle_rface((0, 0, 0), 5)
gear_base = scad.extrude_rsolid(base_circle, (0, 0, 1), 1)
# 创建中心孔
center_hole = scad.make_cylinder_rsolid(1, 1.5, bottom_face_center=(0, 0, 0.5))
gear_base = scad.cut_rsolid(gear_base, center_hole)
# 创建齿(简化版本)
tooth_profile = scad.make_rectangle_rface(0.5, 0.3, center=(5.0, 0, 0))
tooth = scad.extrude_rsolid(tooth_profile, (0, 0, 1), 1.2)
# 合并一个齿到基础上
gear = scad.union_rsolid([gear_base, tooth])
# 验证
self.assertIsInstance(gear, scad.Solid)
self.assertGreater(gear.get_volume(), gear_base.get_volume())
def test_create_cone_complex_shape(self):
"""Test create cone complex shape."""
# 创建基础圆柱体
base_cylinder = scad.make_cylinder_rsolid(2.0, 3.0)
# 创建圆锥体作为顶部
cone_top = scad.make_cone_rsolid(2.0, 2.0, 0.5, bottom_face_center=(0, 0, 3.0))
# 合并圆柱体和圆锥体
combined_shape = scad.union_rsolid([base_cylinder, cone_top])
# 验证
self.assertIsInstance(combined_shape, scad.Solid)
self.assertGreater(combined_shape.get_volume(), base_cylinder.get_volume())
# 测试从圆锥体上切割
cut_cone = scad.make_cone_rsolid(1.0, 1.5, bottom_face_center=(0, 0, 0))
result = scad.cut_rsolid(combined_shape, cut_cone)
# 验证切割后的体积小于原体积
self.assertIsInstance(result, scad.Solid)
self.assertLess(result.get_volume(), combined_shape.get_volume())
def test_complex_boolean_operations(self):
"""Test complex boolean operations."""
# 创建三个重叠的立方体
box1 = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(0, 0, 0))
box2 = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(1, 0, 0))
box3 = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(0, 1, 0))
# 复合布尔运算:(box1 box2) ∩ box3
union_result = scad.union_rsolid([box1, box2])
final_result = scad.intersect_rsolid(union_result, box3)
# 验证
self.assertIsInstance(final_result, scad.Solid)
self.assertGreater(final_result.get_volume(), 0)
self.assertLess(final_result.get_volume(), box1.get_volume())
class TestErrorHandling(unittest.TestCase):
"""Tests for error handling."""
def test_invalid_dimensions(self):
"""Test invalid dimensions."""
with self.assertRaises(ValueError):
scad.make_box_rsolid(-1, 1, 1)
with self.assertRaises(ValueError):
scad.make_cylinder_rsolid(-1, 1)
with self.assertRaises(ValueError):
scad.make_sphere_rsolid(-1)
with self.assertRaises(ValueError):
scad.make_cone_rsolid(-1, 1)
with self.assertRaises(ValueError):
scad.make_cone_rsolid(1, -1)
with self.assertRaises(ValueError):
scad.make_cone_rsolid(0, 1)
def test_invalid_coordinates(self):
"""Test invalid coordinates."""
# 这些不应该抛出异常,但结果应该是有效的
try:
_ = scad.make_point_rvertex(float("inf"), 0, 0)
# 只要不抛出异常就算通过
except Exception as _:
pass
def test_empty_profile_loft(self):
"""Test empty profile loft."""
try:
with self.assertRaises(ValueError):
scad.loft_rsolid([])
except Exception:
self.skipTest("Loft operation not fully implemented")
class TestNewFunctionIntegration(unittest.TestCase):
"""Tests for integration of newly added functions."""
def test_spline_with_exact_weights(self):
"""Test exact weighted spline."""
spline = scad.make_spline_redge(
control_points=[
(0.0, 0.0, 0.0),
(0.6, 1.0, 0.0),
(1.4, 1.0, 0.0),
(2.0, 0.0, 0.0),
],
weights=[1.0, 0.75, 0.75, 1.0],
)
self.assertIsInstance(spline, scad.Edge)
def test_complex_polyline_shapes(self):
"""Test complex polyline shapes."""
# 创建一个复杂的星形多段线
import math
star_points = []
for i in range(10):
angle = i * 2 * math.pi / 10
if i % 2 == 0:
radius = 2.0
else:
radius = 1.0
x = radius * math.cos(angle)
y = radius * math.sin(angle)
star_points.append((x, y, 0.0))
star_wire = scad.make_polyline_rwire(star_points, closed=True)
self.assertIsInstance(star_wire, scad.Wire)
self.assertTrue(star_wire.is_closed())
def test_helix_with_different_parameters(self):
"""Test helix with different parameters."""
# 测试不同的螺旋参数
helix1 = scad.make_helix_rwire(0.5, 2.0, 0.3) # 密螺旋
helix2 = scad.make_helix_rwire(2.0, 4.0, 1.0) # 疏螺旋
helix3 = scad.make_helix_rwire(1.0, 3.0, 0.5, center=(1, 1, 0)) # 偏心螺旋
self.assertIsInstance(helix1, scad.Wire)
self.assertIsInstance(helix2, scad.Wire)
self.assertIsInstance(helix3, scad.Wire)
def test_angle_arc_various_angles(self):
"""Test angle arc various angles."""
# 90度圆弧
arc90 = scad.make_angle_arc_rwire((0, 0, 0), 1.0, 0, np.pi / 2)
self.assertIsInstance(arc90, scad.Wire)
# 180度圆弧
arc180 = scad.make_angle_arc_rwire((0, 0, 0), 1.0, 0, np.pi)
self.assertIsInstance(arc180, scad.Wire)
# 270度圆弧
arc270 = scad.make_angle_arc_rwire((0, 0, 0), 1.0, 0, 3 * np.pi / 2)
self.assertIsInstance(arc270, scad.Wire)
def test_new_functions_with_extrusion(self):
"""Test new functions with extrusion."""
# 创建一个复杂轮廓并拉伸
points = [(0.0, 0.0, 0.0), (2.0, 0.0, 0.0), (2.0, 1.0, 0.0), (0.0, 1.0, 0.0)]
rect_wire = scad.make_polyline_rwire(points, closed=True)
rect_face = scad.make_face_from_wire_rface(rect_wire)
extruded = scad.extrude_rsolid(rect_face, (0, 0, 1), 1.0)
self.assertIsInstance(extruded, scad.Solid)
self.assertAlmostEqual(extruded.get_volume(), 2.0, places=6)
def test_alias_functions(self):
"""Test alias functions."""
# 测试一些主要的别名函数
segment = scad.create_segment((0, 0, 0), (1, 0, 0))
self.assertIsInstance(segment, scad.Edge)
arc = scad.create_arc((0, 0, 0), (1, 1, 0), (2, 0, 0))
self.assertIsInstance(arc, scad.Edge)
spline = scad.create_spline(
control_points=[(0, 0, 0), (0.6, 1, 0), (1.4, 1, 0), (2, 0, 0)]
)
self.assertIsInstance(spline, scad.Edge)
try:
helix = scad.create_helix(1.0, 3.0, 0.5)
self.assertIsInstance(helix, scad.Edge)
except AttributeError:
# 如果别名没有正确导出,跳过测试
self.skipTest("Alias functions not fully exported")
def run_comprehensive_tests():
"""Run the comprehensive test suite."""
print("SimpleCAD API 全面单元测试")
print("=" * 60)
# 创建测试套件
test_suite = unittest.TestSuite()
# 添加所有测试类
test_classes = [
TestBasicShapes,
TestNewFunctionIntegration,
TestTransformations,
Test3DOperations,
TestBooleanOperations,
TestAdvancedFeatures,
TestTagging,
TestCoordinateSystem,
TestExport,
TestComplexExamples,
TestErrorHandling,
]
for test_class in test_classes:
tests = unittest.TestLoader().loadTestsFromTestCase(test_class)
test_suite.addTests(tests)
# 运行测试
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(test_suite)
# 测试结果统计
print("\n" + "=" * 60)
print(f"测试总数: {result.testsRun}")
print(
f"成功: {result.testsRun - len(result.failures) - len(result.errors) - len(result.skipped)}"
)
print(f"失败: {len(result.failures)}")
print(f"错误: {len(result.errors)}")
print(f"跳过: {len(result.skipped)}")
if result.failures:
print("\n失败的测试:")
for test, traceback in result.failures:
print(f"- {test}: {traceback.split('AssertionError:')[-1].strip()}")
if result.errors:
print("\n错误的测试:")
for test, traceback in result.errors:
print(f"- {test}: {traceback.split('Exception:')[-1].strip()}")
if result.skipped:
print("\n跳过的测试:")
for test, reason in result.skipped:
print(f"- {test}: {reason}")
print("\n" + "=" * 60)
# 返回是否所有测试都通过
return len(result.failures) == 0 and len(result.errors) == 0
if __name__ == "__main__":
# 确保输出目录存在
os.makedirs("output", exist_ok=True)
# 运行测试
success = run_comprehensive_tests()
if success:
print("所有测试通过!SimpleCAD API 功能正常。")
else:
print("部分测试失败。请检查上述错误信息。")
sys.exit(0 if success else 1)