"""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()