Files
cadSet/SimpleCADAPI/test/test_operation_output_roles.py

870 lines
32 KiB
Python

"""Kernel-backed feature output roles, tagging, projection, and replay."""
import json
import unittest
from copy import deepcopy
import simplecadapi as scad
from simplecadapi import ql as Q
def _role_count(shape, role):
candidates = shape.get_edges() if role == "shell.wall" else shape.get_faces()
return sum(Q.output_role(role)(item) for item in candidates)
class TestOperationOutputRoles(unittest.TestCase):
def test_feature_role_matrix_is_kernel_backed(self):
profile = scad.make_rectangle_rface(2.0, 1.0)
extruded = scad.extrude_rsolid(profile, (0, 0, 1), 2.0)
self.assertEqual(_role_count(extruded, "extrusion.start"), 1)
self.assertEqual(_role_count(extruded, "extrusion.end"), 1)
self.assertEqual(_role_count(extruded, "extrusion.side"), 4)
revolve_profile = scad.make_rectangle_rface(2.0, 1.0, center=(3, 0, 0))
revolved = scad.revolve_rsolid(
revolve_profile, (0, 1, 0), 180.0, (0, 0, 0)
)
self.assertEqual(_role_count(revolved, "revolution.start"), 1)
self.assertEqual(_role_count(revolved, "revolution.end"), 1)
self.assertEqual(_role_count(revolved, "revolution.side"), 4)
first = scad.make_rectangle_rwire(2.0, 2.0)
last = scad.make_rectangle_rwire(1.0, 1.0, center=(0, 0, 2))
lofted = scad.loft_rsolid([first, last])
self.assertEqual(_role_count(lofted, "loft.start"), 1)
self.assertEqual(_role_count(lofted, "loft.end"), 1)
self.assertEqual(_role_count(lofted, "loft.side"), 4)
sweep_profile = scad.make_rectangle_rface(1.0, 1.0)
path = scad.make_segment_rwire((0, 0, 0), (0, 0, 3))
swept = scad.sweep_rsolid(sweep_profile, path)
self.assertEqual(_role_count(swept, "sweep.start"), 1)
self.assertEqual(_role_count(swept, "sweep.end"), 1)
self.assertEqual(_role_count(swept, "sweep.side"), 4)
twisted = scad.twisted_sweep_rsolid(
profile=sweep_profile,
distance=3.0,
twist_angle=45.0,
)
self.assertEqual(_role_count(twisted, "twisted_sweep.start"), 1)
self.assertEqual(_role_count(twisted, "twisted_sweep.end"), 1)
self.assertEqual(_role_count(twisted, "twisted_sweep.side"), 4)
def test_revolve_and_sweep_topology_tags_require_exact_side_correspondence(self):
revolve_profile = scad.make_rectangle_rface(
2.0,
1.0,
center=(3, 0, 0),
edge_tags=("a", "b", "c", "d"),
)
revolved = scad.revolve_rsolid(
revolve_profile,
(0, 1, 0),
180.0,
(0, 0, 0),
tag_prefix="revolved",
)
self.assertEqual(
{
tag
for face in revolved.get_faces()
for tag in scad.list_tags(face, scope="local")
if tag.startswith("revolved.face.side.")
},
{
"revolved.face.side.a",
"revolved.face.side.b",
"revolved.face.side.c",
"revolved.face.side.d",
},
)
profile = scad.make_rectangle_rface(
1.0, 1.0, edge_tags=("a", "b", "c", "d")
)
path = scad.make_segment_rwire((0, 0, 0), (0, 0, 3))
swept = scad.sweep_rsolid(profile, path, tag_prefix="swept")
self.assertEqual(
{
tag
for face in swept.get_faces()
for tag in scad.list_tags(face, scope="local")
if tag.startswith("swept.face.side.")
},
{
"swept.face.side.a",
"swept.face.side.b",
"swept.face.side.c",
"swept.face.side.d",
},
)
def test_loft_does_not_guess_topology_tags_for_merged_side_faces(self):
first = scad.make_rectangle_rwire(
2.0,
2.0,
edge_tags=("first_a", "first_b", "first_c", "first_d"),
)
last = scad.make_rectangle_rwire(
1.0,
1.0,
center=(0, 0, 2),
edge_tags=("last_a", "last_b", "last_c", "last_d"),
)
lofted = scad.loft_rsolid([first, last], tag_prefix="lofted")
side_tags = {
tag
for face in lofted.get_faces()
for tag in scad.list_tags(face, scope="local")
if tag == "lofted.face.side" or tag.startswith("lofted.face.side.")
}
self.assertEqual(side_tags, {"lofted.face.side"})
def test_native_cylinder_has_kernel_face_and_edge_roles(self):
cylinder = scad.make_cylinder_rsolid(2.0, 5.0, tag_prefix="shaft")
for role in ("cylinder.start", "cylinder.end", "cylinder.side"):
self.assertEqual(
sum(Q.output_role(role)(face) for face in cylinder.get_faces()), 1
)
for role in (
"cylinder.start_boundary",
"cylinder.end_boundary",
"cylinder.seam",
):
self.assertEqual(
sum(Q.output_role(role)(edge) for edge in cylinder.get_edges()), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("shaft.face.start")).resolve(cylinder)), 1
)
self.assertEqual(
len(Q.edges().where(Q.tag("shaft.face.start")).resolve(cylinder)), 1
)
def test_native_box_has_kernel_face_roles_and_exact_topology_tags(self):
box = scad.make_box_rsolid(2.0, 3.0, 4.0, tag_prefix="housing")
for role in (
"box.bottom",
"box.top",
"box.front",
"box.back",
"box.left",
"box.right",
):
self.assertEqual(
sum(Q.output_role(role)(face) for face in box.get_faces()), 1
)
top = Q.faces().where(Q.tag("housing.face.top"))
front = Q.faces().where(Q.tag("housing.face.front"))
self.assertEqual(len(top.resolve(box)), 1)
self.assertEqual(len(front.resolve(box)), 1)
self.assertEqual(
len(Q.edges().incident_to(top, front, distinct=True).resolve(box)), 1
)
def test_native_box_roles_and_tags_replay(self):
with scad.GraphSession() as session:
box = scad.make_box_rsolid(
2.0,
3.0,
4.0,
tag_prefix="housing",
bottom_face_tag="anchor.base",
right_face_tag="role.mounting_surface",
result_tag="part.housing",
)
payload = json.loads(scad.export_model_json(session))
primitive = next(
node
for node in payload["graph"]["nodes"]
if node["op"] == "make_box_rsolid"
)
self.assertNotIn("name", primitive["params"])
self.assertEqual(len(primitive["topo_delta"]["roles"]), 6)
replayed = scad.replay_model_json(json.dumps(payload))[0]
for shape in (box, replayed):
self.assertEqual(
len(Q.faces().where(Q.tag("housing.face.bottom")).resolve(shape)),
1,
)
self.assertEqual(
len(Q.faces().where(Q.tag("anchor.base")).resolve(shape)), 1
)
self.assertEqual(
len(
Q.faces()
.where(Q.tag("role.mounting_surface"))
.resolve(shape)
),
1,
)
self.assertIn("part.housing", scad.list_tags(shape, scope="local"))
self.assertEqual(
sum(
Q.output_role("box.right")(face)
for face in shape.get_faces()
),
1,
)
def test_generic_role_tag_mapping_is_not_a_public_entrypoint(self):
with self.assertRaises(TypeError):
scad.make_box_rsolid(
2.0,
3.0,
4.0,
**{"output_tags": {"box.top_front_edge": "role.edge"}},
)
def test_removed_topology_name_keywords_are_not_public_entrypoints(self):
with self.assertRaises(TypeError):
scad.make_box_rsolid(2.0, 3.0, 4.0, **{"name": "housing"})
with self.assertRaises(TypeError):
scad.make_rectangle_rface(
2.0,
1.0,
**{"edge_names": ("bottom", "right", "top", "left")},
)
with self.assertRaises(TypeError):
scad.make_circle_rface(
(0.0, 0.0, 0.0),
1.0,
**{"edge_name": "outer"},
)
def test_native_cone_and_frustum_have_kernel_roles(self):
cone = scad.make_cone_rsolid(2.0, 4.0, tag_prefix="tip")
frustum = scad.make_cone_rsolid(
2.0, 4.0, top_radius=1.0, tag_prefix="adapter"
)
for shape, end_count in ((cone, 0), (frustum, 1)):
self.assertEqual(_role_count(shape, "cone.start"), 1)
self.assertEqual(_role_count(shape, "cone.end"), end_count)
self.assertEqual(_role_count(shape, "cone.side"), 1)
for role in (
"cone.start_boundary",
"cone.end_boundary",
"cone.seam",
):
self.assertEqual(
sum(Q.output_role(role)(edge) for edge in shape.get_edges()), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("tip.face.start")).resolve(cone)), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("tip.face.end")).resolve(cone)), 0
)
self.assertEqual(
len(Q.edges().where(Q.tag("tip.edge.end")).resolve(cone)), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("adapter.face.end")).resolve(frustum)), 1
)
def test_native_cone_roles_and_tags_replay(self):
with scad.GraphSession() as session:
cone = scad.make_cone_rsolid(
2.0,
4.0,
top_radius=1.0,
tag_prefix="adapter",
start_face_tag="anchor.base",
end_face_tag="role.outlet",
seam_edge_tag="role.seam",
result_tag="part.adapter",
)
payload = json.loads(scad.export_model_json(session))
primitive = next(
node
for node in payload["graph"]["nodes"]
if node["op"] == "make_cone_rsolid"
)
self.assertNotIn("name", primitive["params"])
self.assertEqual(len(primitive["topo_delta"]["roles"]), 6)
replayed = scad.replay_model_json(json.dumps(payload))[0]
for shape in (cone, replayed):
self.assertEqual(
len(Q.faces().where(Q.tag("adapter.face.end")).resolve(shape)), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("role.outlet")).resolve(shape)), 1
)
self.assertEqual(
len(Q.edges().where(Q.tag("role.seam")).resolve(shape)), 1
)
self.assertIn("part.adapter", scad.list_tags(shape, scope="local"))
self.assertEqual(
sum(
Q.output_role("cone.end_boundary")(edge)
for edge in shape.get_edges()
),
1,
)
def test_pointed_cone_rejects_absent_end_face_tag(self):
with self.assertRaisesRegex(
scad.SimpleCADError, "requires exactly one kernel-proven result, got 0"
):
scad.make_cone_rsolid(2.0, 4.0, end_face_tag="role.outlet")
def test_native_cylinder_roles_and_tags_replay(self):
with scad.GraphSession() as session:
cylinder = scad.make_cylinder_rsolid(
2.0,
5.0,
tag_prefix="shaft",
start_face_tag="role.base",
seam_edge_tag="role.seam",
result_tag="part.shaft",
)
payload = json.loads(scad.export_model_json(session))
primitive = next(
node
for node in payload["graph"]["nodes"]
if node["op"] == "make_cylinder_rsolid"
)
self.assertNotIn("name", primitive["params"])
self.assertTrue(
any(
node["params"]["tag_binding"]["evidence"].get("topology_name")
for node in payload["graph"]["nodes"]
if node["op"] == "apply_tag_rselection"
)
)
replayed = scad.replay_model_json(json.dumps(payload))[0]
for shape in (cylinder, replayed):
self.assertEqual(
len(Q.faces().where(Q.tag("shaft.face.start")).resolve(shape)), 1
)
self.assertEqual(
len(Q.faces().where(Q.tag("role.base")).resolve(shape)), 1
)
self.assertEqual(
len(Q.edges().where(Q.tag("role.seam")).resolve(shape)), 1
)
self.assertEqual(
len(Q.edges().where(Q.tag("shaft.edge.seam")).resolve(shape)), 1
)
self.assertEqual(
sum(
Q.output_role("cylinder.seam")(edge)
for edge in shape.get_edges()
),
1,
)
def test_detail_feature_role_matrix_is_kernel_backed(self):
box = scad.make_box_rsolid(4.0, 4.0, 4.0)
edge = box.get_edges(0)
filleted = scad.fillet_rsolid(box, [edge], 0.2)
chamfered = scad.chamfer_rsolid(box, [edge], 0.2)
self.assertGreaterEqual(_role_count(filleted, "fillet.patch"), 1)
self.assertGreaterEqual(_role_count(chamfered, "chamfer.patch"), 1)
shelled = scad.shell_rsolid(box, [box.get_faces(0)], 0.2)
self.assertEqual(_role_count(shelled, "shell.offset_face"), 5)
self.assertEqual(_role_count(shelled, "shell.closing_descendant"), 1)
self.assertEqual(_role_count(shelled, "shell.wall"), 4)
def test_full_revolve_has_no_cap_roles_and_rejects_cap_tag(self):
profile = scad.make_rectangle_rface(2.0, 1.0, center=(3, 0, 0))
revolved = scad.revolve_rsolid(
profile, (0, 1, 0), 360.0, (0, 0, 0)
)
self.assertEqual(_role_count(revolved, "revolution.start"), 0)
self.assertEqual(_role_count(revolved, "revolution.end"), 0)
with self.assertRaisesRegex(
scad.SimpleCADError, "requires exactly one kernel-proven result, got 0"
):
scad.revolve_rsolid(
profile,
(0, 1, 0),
360.0,
(0, 0, 0),
start_face_tag="role.start",
)
def test_unavailable_shell_role_fails_instead_of_guessing(self):
box = scad.make_box_rsolid(4.0, 4.0, 4.0)
with self.assertRaisesRegex(
scad.SimpleCADError, "shell.body_face.*kernel-proven"
):
scad.shell_rsolid(
box,
[box.get_faces(0)],
0.2,
body_faces_tag="group.body",
)
def test_sweep_rejects_profiles_with_inner_wires(self):
outer = scad.make_rectangle_rwire(3.0, 3.0)
inner = scad.make_rectangle_rwire(1.0, 1.0, center=(1.0, 1.0, 0.0))
profile = scad.make_face_from_wires_rface(outer, [inner])
path = scad.make_segment_rwire((0, 0, 0), (0, 0, 3))
with self.assertRaisesRegex(scad.SimpleCADError, "inner wires are unsupported"):
scad.sweep_rsolid(profile, path)
class TestOperationRoleTags(unittest.TestCase):
@staticmethod
def _binding_nodes(payload):
return [
node
for node in payload["graph"]["nodes"]
if node["op"] == "apply_tag_rselection"
]
def test_named_role_tags_lower_to_role_aware_semantic_nodes(self):
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(5.0, 3.0)
result = scad.extrude_rsolid(
profile,
(0, 0, 1),
2.0,
start_face_tag="anchor.base",
end_face_tag="role.mounting_surface",
side_faces_tag="group.outer_walls",
result_tag="part.body",
)
self.assertEqual(
len(scad.select_faces_by_tag(result, "anchor.base", scope="local")), 1
)
self.assertEqual(
len(
scad.select_faces_by_tag(
result, "role.mounting_surface", scope="local"
)
),
1,
)
self.assertEqual(
len(
scad.select_faces_by_tag(
result, "group.outer_walls", scope="local"
)
),
4,
)
self.assertIn("part.body", scad.list_tags(result, scope="local"))
payload = json.loads(scad.export_model_json(session))
feature = next(
node
for node in payload["graph"]["nodes"]
if node["op"] == "make_extrude_rsolid"
)
self.assertNotIn("result_tag", feature["params"])
binding_nodes = self._binding_nodes(payload)
self.assertEqual(len(binding_nodes), 4)
role_evidence = [
node["params"]["tag_binding"]["evidence"].get(
"operation_output_role"
)
for node in binding_nodes
]
self.assertEqual(
[item["role"] for item in role_evidence if item is not None],
["extrusion.start", "extrusion.end", "extrusion.side"],
)
self.assertTrue(
all(
node["params"]["tag_binding"]["producer"]["kind"]
== "user_operation"
for node in binding_nodes
)
)
expected_binding_ids = {
node["params"]["tag_binding"]["binding_id"] for node in binding_nodes
}
replayed = scad.replay_model_json(json.dumps(payload))[0]
self.assertEqual(
len(
scad.select_faces_by_tag(
replayed, "group.outer_walls", scope="local"
)
),
4,
)
actual_binding_ids = {
explanation["binding_id"]
for shape, tag in [
(replayed, "part.body"),
*[
(face, tag)
for tag in (
"anchor.base",
"role.mounting_surface",
"group.outer_walls",
)
for face in scad.select_faces_by_tag(replayed, tag, scope="local")
],
]
for explanation in scad.explain_tag(shape, tag, scope="local")
}
self.assertEqual(actual_binding_ids, expected_binding_ids)
def test_shell_tags_face_and_edge_roles_and_replays(self):
with scad.GraphSession() as session:
box = scad.make_box_rsolid(4.0, 4.0, 4.0)
result = scad.shell_rsolid(
box,
[box.get_faces(0)],
0.2,
offset_faces_tag="group.offset",
closing_faces_tag="group.closing",
wall_edges_tag="group.wall",
)
self.assertEqual(
len(scad.select_faces_by_tag(result, "group.offset", scope="local")), 5
)
self.assertEqual(
len(scad.select_faces_by_tag(result, "group.closing", scope="local")),
1,
)
self.assertEqual(
len(Q.edges().where(Q.tag("group.wall", scope="local")).resolve(result)),
4,
)
replayed = scad.replay_model_json(scad.export_model_json(session))[0]
self.assertEqual(
len(Q.edges().where(Q.tag("group.wall", scope="local")).resolve(replayed)),
4,
)
def test_remaining_feature_output_tag_surfaces_replay(self):
cases = []
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(2.0, 1.0, center=(3, 0, 0))
result = scad.revolve_rsolid(
profile,
(0, 1, 0),
180.0,
(0, 0, 0),
start_face_tag="test.revolve.start",
end_face_tag="test.revolve.end",
side_faces_tag="test.revolve.side",
)
cases.append(
(
"revolve",
result,
scad.export_model_json(session),
{
"test.revolve.start": 1,
"test.revolve.end": 1,
"test.revolve.side": 4,
},
)
)
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(1.0, 1.0)
result = scad.twisted_sweep_rsolid(
profile=profile,
distance=3.0,
twist_angle=30.0,
start_face_tag="test.twisted.start",
end_face_tag="test.twisted.end",
side_faces_tag="test.twisted.side",
)
cases.append(
(
"twisted_sweep",
result,
scad.export_model_json(session),
{
"test.twisted.start": 1,
"test.twisted.end": 1,
"test.twisted.side": 4,
},
)
)
with scad.GraphSession() as session:
first = scad.make_rectangle_rwire(2.0, 2.0)
last = scad.make_rectangle_rwire(1.0, 1.0, center=(0, 0, 2))
result = scad.loft_rsolid(
[first, last],
start_face_tag="test.loft.start",
end_face_tag="test.loft.end",
side_faces_tag="test.loft.side",
)
cases.append(
(
"loft",
result,
scad.export_model_json(session),
{
"test.loft.start": 1,
"test.loft.end": 1,
"test.loft.side": 4,
},
)
)
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(1.0, 1.0)
path = scad.make_segment_rwire((0, 0, 0), (0, 0, 3))
result = scad.sweep_rsolid(
profile,
path,
start_face_tag="test.sweep.start",
end_face_tag="test.sweep.end",
side_faces_tag="test.sweep.side",
)
cases.append(
(
"sweep",
result,
scad.export_model_json(session),
{
"test.sweep.start": 1,
"test.sweep.end": 1,
"test.sweep.side": 4,
},
)
)
for operation, feature, payload, expected in cases:
replayed = scad.replay_model_json(payload)[0]
for tag, count in expected.items():
with self.subTest(operation=operation, tag=tag):
authored = scad.select_faces_by_tag(feature, tag, scope="local")
rebuilt = scad.select_faces_by_tag(replayed, tag, scope="local")
self.assertEqual(len(authored), count)
self.assertEqual(len(rebuilt), count)
self.assertEqual(
{
item["binding_id"]
for face in authored
for item in scad.explain_tag(face, tag, scope="local")
},
{
item["binding_id"]
for face in rebuilt
for item in scad.explain_tag(face, tag, scope="local")
},
)
def test_fillet_and_chamfer_patch_tags_replay(self):
for operation in ("fillet", "chamfer"):
with self.subTest(operation=operation):
with scad.GraphSession() as session:
box = scad.make_box_rsolid(4.0, 4.0, 4.0)
edge = box.get_edges(0)
if operation == "fillet":
result = scad.fillet_rsolid(
box,
[edge],
0.2,
generated_faces_tag="test.fillet.patch",
)
tag = "test.fillet.patch"
else:
result = scad.chamfer_rsolid(
box,
[edge],
0.2,
generated_faces_tag="test.chamfer.patch",
)
tag = "test.chamfer.patch"
authored = scad.select_faces_by_tag(result, tag, scope="local")
replayed = scad.replay_model_json(scad.export_model_json(session))[0]
rebuilt = scad.select_faces_by_tag(replayed, tag, scope="local")
self.assertGreaterEqual(len(authored), 1)
self.assertEqual(len(rebuilt), len(authored))
self.assertEqual(
{
item["binding_id"]
for face in authored
for item in scad.explain_tag(face, tag, scope="local")
},
{
item["binding_id"]
for face in rebuilt
for item in scad.explain_tag(face, tag, scope="local")
},
)
def test_generic_role_tag_mapping_is_not_a_public_entrypoint(self):
profile = scad.make_rectangle_rface(2.0, 1.0)
with self.assertRaises(TypeError):
scad.extrude_rsolid(
profile,
(0, 0, 1),
1.0,
**{"output_tags": {"extrusion.end": "role.end"}},
)
with self.assertRaisesRegex(scad.SimpleCADError, "is not normalized"):
scad.extrude_rsolid(
profile,
(0, 0, 1),
1.0,
end_face_tag="Not Normalized",
)
def test_topology_and_role_tags_share_surface_with_distinct_evidence(self):
with scad.GraphSession():
box = scad.make_box_rsolid(
2.0,
3.0,
4.0,
tag_prefix="housing",
top_face_tag="role.cover",
result_tag="part.housing",
)
top = Q.faces().where(Q.tag("housing.face.top")).exactly(1).resolve(box)[0]
topology_binding = scad.explain_tag(
top, "housing.face.top", scope="local"
)[0]["binding"]
role_binding = scad.explain_tag(
top, "role.cover", scope="local"
)[0]["binding"]
result_binding = scad.explain_tag(
box, "part.housing", scope="local"
)[0]["binding"]
self.assertIn("topology_name", topology_binding["evidence"])
self.assertNotIn("operation_output_role", topology_binding["evidence"])
self.assertIn("operation_output_role", role_binding["evidence"])
self.assertNotIn("topology_name", role_binding["evidence"])
self.assertIn("operation_result", result_binding["evidence"])
self.assertNotIn("topology_name", result_binding["evidence"])
def test_profile_edge_binding_projects_with_exact_source_identity(self):
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(5.0, 3.0)
source_edge = profile.get_edges(0)
profile = scad.apply_tag_rselection(
profile, [source_edge], "role.source_edge"
)
tagged_source = scad.select_edges_by_tag(
profile, "role.source_edge", scope="local"
)[0]
source_explanation = scad.explain_tag(
tagged_source, "role.source_edge", scope="local"
)[0]
source_binding_id = source_explanation["binding_id"]
source_topo_id = tagged_source.topo_id
result = scad.extrude_rsolid(profile, (0, 0, 1), 2.0)
projected = (
Q.faces()
.where(Q.source_binding(source_binding_id))
.exactly(1)
.resolve(result)
)
self.assertTrue(Q.source_topology(source_topo_id)(projected[0]))
projected_explanation = scad.explain_tag(
projected[0], "role.source_edge", scope="local"
)[0]
self.assertEqual(
projected_explanation["binding"]["evidence"]["source_binding_id"],
source_binding_id,
)
replayed = next(
shape
for shape in scad.replay_model_json(scad.export_model_json(session))
if isinstance(shape, scad.Solid)
)
replayed_projected = (
Q.faces()
.where(Q.source_binding(source_binding_id))
.exactly(1)
.resolve(replayed)
)
self.assertTrue(Q.source_topology(source_topo_id)(replayed_projected[0]))
def test_replay_rejects_role_binding_and_topology_role_tampering(self):
with scad.GraphSession() as session:
profile = scad.make_rectangle_rface(5.0, 3.0)
scad.extrude_rsolid(
profile, (0, 0, 1), 2.0, start_face_tag="anchor.base"
)
raw = json.loads(scad.export_model_json(session))
binding_node = self._binding_nodes(raw)[0]
damaged = deepcopy(raw)
damaged_binding_node = next(
node
for node in damaged["graph"]["nodes"]
if node["node_id"] == binding_node["node_id"]
)
binding = damaged_binding_node["params"]["tag_binding"]
binding["evidence"]["operation_output_role"]["role"] = "extrusion.end"
damaged["semantic_bindings"] = [binding]
with self.assertRaisesRegex(
scad.SimpleCADError, "binding targets do not match"
):
scad.replay_model_json(json.dumps(damaged))
damaged = deepcopy(raw)
feature = next(
node
for node in damaged["graph"]["nodes"]
if node["op"] == "make_extrude_rsolid"
)
feature["topo_delta"]["roles"].pop()
with self.assertRaisesRegex(scad.SimpleCADError, "output-role evidence drifted"):
scad.replay_model_json(json.dumps(damaged))
class TestOperationRoleQL(unittest.TestCase):
def test_role_and_source_predicates_roundtrip(self):
predicates = (
Q.output_role("Extrusion.End"),
Q.source_binding("tag_binding_source"),
Q.source_topology("edge_0"),
)
for predicate in predicates:
with self.subTest(kind=predicate.kind):
self.assertEqual(
Q.SerializablePredicate.from_dict(predicate.to_dict()).to_dict(),
predicate.to_dict(),
)
def test_predicate_deserialization_rejects_invalid_payloads(self):
invalid = (
{"kind": "unknown", "data": {}, "children": []},
{"kind": "output_role", "data": {}, "children": []},
{
"kind": "source_binding",
"data": {"source_binding_id": "x", "extra": True},
"children": [],
},
{"kind": "not", "data": {}, "children": []},
)
for payload in invalid:
with self.subTest(payload=payload):
with self.assertRaises(ValueError):
Q.SerializablePredicate.from_dict(payload)
if __name__ == "__main__":
unittest.main()