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

209 lines
9.3 KiB
Python

"""Focused tests for declarative sketch construction and constraints."""
from __future__ import annotations
import json
import unittest
import simplecadapi as scad
class TestSketchApi(unittest.TestCase):
def test_sketch_accepts_wire_and_can_build_profile_faces(self):
wire = scad.make_rectangle_rwire(2.0, 1.0)
sketch = scad.Sketch([wire])
self.assertEqual(len(sketch.curves()), 1)
self.assertEqual(len(sketch.closed_wires()), 1)
faces = sketch.to_faces()
self.assertEqual(len(faces), 1)
self.assertIsInstance(faces[0], scad.Face)
def _make_constrained_rectangle(self):
width = scad.var("sketch_width", 2.0)
height = scad.var("sketch_height", 1.0)
sketch = scad.make_sketch_rsketch("rect")
sketch = scad.add_point_rsketch(sketch, "p0", 0.0, 0.0)
sketch = scad.add_point_rsketch(sketch, "p1", 2.0, 0.0)
sketch = scad.add_point_rsketch(sketch, "p2", 2.0, 1.0)
sketch = scad.add_point_rsketch(sketch, "p3", 0.0, 1.0)
sketch = scad.add_line_rsketch(sketch, "bottom", "p0", "p1")
sketch = scad.add_line_rsketch(sketch, "right", "p1", "p2")
sketch = scad.add_line_rsketch(sketch, "top", "p2", "p3")
sketch = scad.add_line_rsketch(sketch, "left", "p3", "p0")
sketch = scad.constrain_horizontal_rsketch(sketch, "bottom")
sketch = scad.constrain_vertical_rsketch(sketch, "right")
sketch = scad.constrain_parallel_rsketch(sketch, "bottom", "top")
sketch = scad.constrain_parallel_rsketch(sketch, "left", "right")
sketch = scad.constrain_perpendicular_rsketch(sketch, "bottom", "right")
sketch = scad.constrain_equal_length_rsketch(sketch, "bottom", "top")
sketch = scad.constrain_equal_length_rsketch(sketch, "left", "right")
sketch = scad.constrain_distance_rsketch(sketch, "p0", "p1", width)
sketch = scad.constrain_distance_rsketch(sketch, "p0", "p3", height)
sketch = scad.constrain_fix_rsketch(sketch, "p0")
return sketch
def test_sketch_document_updates_are_functional(self):
original = scad.make_sketch_rsketch("functional")
with_point = scad.add_point_rsketch(original, "p0", 0.0, 0.0)
self.assertNotIn("p0", original.entities)
self.assertIn("p0", with_point.entities)
self.assertIsNot(original, with_point)
def test_isomorphic_sketch_api_solves_rectangle_and_builds_face(self):
sketch = self._make_constrained_rectangle()
result = scad.inspect_sketch_rsketchresult(
sketch, require_fully_constrained=True
)
self.assertEqual(result.status, "solved")
self.assertEqual(result.dof, 0)
self.assertAlmostEqual(result.residual_norm, 0.0, places=7)
face = scad.make_face_from_sketch_rface(sketch, require_fully_constrained=True)
self.assertIsInstance(face, scad.Face)
self.assertAlmostEqual(face.get_area(), 2.0, places=6)
self.assertEqual(face.get_metadata("sketch_solve")["status"], "solved")
self.assertEqual(face.get_metadata("source_sketch")["name"], "rect")
edge_tags = set()
for edge in face.get_edges():
edge_tags.update(scad.list_tags(edge))
self.assertIn("sketch.rect", scad.list_tags(face))
self.assertIn("sketch_entity.bottom", edge_tags)
self.assertIn("sketch_entity.right", edge_tags)
self.assertIn("sketch_entity.top", edge_tags)
self.assertIn("sketch_entity.left", edge_tags)
def test_circle_sketch_constraints_build_circular_face(self):
sketch = scad.make_sketch_rsketch("circle")
sketch = scad.add_point_rsketch(sketch, "center", 0.0, 0.0)
sketch = scad.add_circle_rsketch(sketch, "outer", "center", 1.5)
circle = scad.get_sketch_entity_rsketchref(sketch, "outer")
sketch = scad.constrain_fix_rsketch(sketch, "center")
sketch = scad.constrain_radius_rsketch(sketch, circle, 1.5)
result = scad.inspect_sketch_rsketchresult(
sketch, require_fully_constrained=True
)
self.assertEqual(result.status, "solved")
face = scad.make_face_from_sketch_rface(sketch, require_fully_constrained=True)
self.assertAlmostEqual(face.get_area(), 3.141592653589793 * 2.25, places=5)
self.assertIn("sketch_entity.outer", scad.list_tags(face.get_edges(0)))
def test_underconstrained_and_conflicting_sketches_report_diagnostics(self):
sketch = scad.make_sketch_rsketch("open")
sketch = scad.add_point_rsketch(sketch, "p0", 0.0, 0.0)
sketch = scad.add_point_rsketch(sketch, "p1", 1.0, 0.0)
sketch = scad.add_line_rsketch(sketch, "line", "p0", "p1")
result = scad.inspect_sketch_rsketchresult(sketch, strict=False)
self.assertEqual(result.status, "underconstrained")
self.assertGreater(result.dof, 0)
bad = scad.make_sketch_rsketch("bad")
bad = scad.add_point_rsketch(bad, "a", 0.0, 0.0)
bad = scad.add_point_rsketch(bad, "b", 1.0, 0.0)
bad = scad.add_line_rsketch(bad, "line", "a", "b")
bad = scad.constrain_distance_rsketch(bad, "a", "b", 1.0)
bad = scad.constrain_distance_rsketch(bad, "a", "b", 2.0)
bad = scad.constrain_fix_rsketch(bad, "a")
bad_result = scad.inspect_sketch_rsketchresult(bad, strict=False)
self.assertEqual(bad_result.status, "conflicting")
self.assertTrue(any(diag.code == "residual_too_large" for diag in bad_result.diagnostics))
def test_sketch_refs_are_scoped_to_their_sketch(self):
first = scad.make_sketch_rsketch("first")
second = scad.make_sketch_rsketch("second")
first = scad.add_point_rsketch(first, "p0", 0.0, 0.0)
second = scad.add_point_rsketch(second, "p1", 1.0, 0.0)
p0 = scad.get_sketch_point_rsketchref(first, "p0")
p1 = scad.get_sketch_point_rsketchref(second, "p1")
with self.assertRaises(Exception):
scad.add_line_rsketch(first, "bad", p0, p1)
def test_graph_replay_preserves_sketch_to_face_result(self):
with scad.GraphSession() as session:
sketch = self._make_constrained_rectangle()
face = scad.make_face_from_sketch_rface(sketch)
ops = [node.op for node in session.graph.nodes]
self.assertIn("make_sketch_rsketch", ops)
self.assertIn("make_add_point_rsketch", ops)
self.assertIn("make_constrain_parallel_rsketch", ops)
self.assertIn("make_face_from_sketch_rface", ops)
self.assertNotIn("make_sketch_point_rsketchref", ops)
self.assertNotIn("make_solve_sketch_rsketchresult", ops)
payload = json.loads(scad.export_model_json(session))
promotion = next(
node for node in payload["graph"]["nodes"] if node["op"] == "make_face_from_sketch_rface"
)
self.assertEqual(promotion["params"]["solve_snapshot"]["status"], "solved")
self.assertIn("promotion_map", promotion["params"])
replayed = scad.replay_model_json(json.dumps(payload))
self.assertEqual(len(replayed), 1)
self.assertIsInstance(replayed[0], scad.Face)
self.assertAlmostEqual(replayed[0].get_area(), face.get_area(), places=6)
def test_graph_replay_preserves_sketch_bspline_definition(self):
with scad.GraphSession() as session:
sketch = scad.make_sketch_rsketch("spline")
sketch = scad.add_point_rsketch(sketch, "p0", 0.0, 0.0)
sketch = scad.add_point_rsketch(sketch, "p1", 4.0, 0.0)
sketch = scad.add_bspline_rsketch(
sketch,
"curve",
"p0",
"p1",
control_points=[
[0.0, 0.0],
[1.0, 1.5],
[3.0, 1.5],
[4.0, 0.0],
],
degree=3,
knots=[0.0, 1.0],
multiplicities=[4, 4],
)
payload = json.loads(scad.export_model_json(session))
spline_node = next(
node for node in payload["graph"]["nodes"] if node["op"] == "make_add_bspline_rsketch"
)
self.assertEqual(len(spline_node["params"]["control_points"]), 4)
self.assertEqual(spline_node["params"]["knots"], [0.0, 1.0])
self.assertEqual(spline_node["params"]["multiplicities"], [4, 4])
replayed = scad.replay_model_json(json.dumps(payload))
self.assertEqual(len(replayed), 1)
self.assertIsInstance(replayed[0], scad.Sketch)
self.assertEqual(
replayed[0].entities["curve"].data["control_points"],
sketch.entities["curve"].data["control_points"],
)
def test_strict_replay_requires_sketch_solve_snapshot(self):
with scad.GraphSession() as session:
sketch = self._make_constrained_rectangle()
scad.make_face_from_sketch_rface(sketch)
payload = json.loads(scad.export_model_json(session))
promotion = next(
node for node in payload["graph"]["nodes"] if node["op"] == "make_face_from_sketch_rface"
)
del promotion["params"]["solve_snapshot"]
with self.assertRaises(Exception):
scad.replay_model_json(json.dumps(payload))
replayed = scad.replay_model_json(json.dumps(payload), strict=False)
self.assertEqual(len(replayed), 1)
self.assertIsInstance(replayed[0], scad.Face)
if __name__ == "__main__":
unittest.main()