first commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
|
||||
from cadpy.assembly import AssemblyHelper, MateTarget, label_shape, label_text, target
|
||||
from cadpy.step_export import _collect_assembly_mates
|
||||
|
||||
|
||||
class FakeLocation:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __mul__(self, other):
|
||||
return FakeLocation(("mul", self.value, other.value))
|
||||
|
||||
|
||||
class FakePart:
|
||||
def __init__(self):
|
||||
self.joints = {}
|
||||
self.label = None
|
||||
|
||||
|
||||
class FakeJoint:
|
||||
def __init__(self, *, label, to_part, joint_location=None, **options):
|
||||
self.label = label
|
||||
self.to_part = to_part
|
||||
self.location = joint_location
|
||||
self.options = options
|
||||
self.connections = []
|
||||
to_part.joints[label] = self
|
||||
|
||||
def connect_to(self, other, **options):
|
||||
self.connections.append((other, options))
|
||||
|
||||
|
||||
class FakeCompound:
|
||||
def __init__(self, *, label, children):
|
||||
self.label = label
|
||||
self.children = tuple(children)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def fake_build123d():
|
||||
module = types.SimpleNamespace(
|
||||
BallJoint=FakeJoint,
|
||||
Compound=FakeCompound,
|
||||
CylindricalJoint=FakeJoint,
|
||||
LinearJoint=FakeJoint,
|
||||
Location=FakeLocation,
|
||||
RevoluteJoint=FakeJoint,
|
||||
RigidJoint=FakeJoint,
|
||||
)
|
||||
original = sys.modules.get("build123d")
|
||||
sys.modules["build123d"] = module
|
||||
try:
|
||||
yield module
|
||||
finally:
|
||||
if original is None:
|
||||
sys.modules.pop("build123d", None)
|
||||
else:
|
||||
sys.modules["build123d"] = original
|
||||
|
||||
|
||||
class AssemblyHelperTests(unittest.TestCase):
|
||||
def test_label_text_normalizes_tokens(self) -> None:
|
||||
self.assertEqual(
|
||||
"base_plate:left_side",
|
||||
label_text("base plate", "left:side"),
|
||||
)
|
||||
|
||||
def test_label_shape_sets_native_label_and_color(self) -> None:
|
||||
shape = types.SimpleNamespace()
|
||||
color = object()
|
||||
|
||||
returned = label_shape(shape, "m3 standoff", "front left", color=color)
|
||||
|
||||
self.assertIs(returned, shape)
|
||||
self.assertEqual("m3_standoff:front_left", shape.label)
|
||||
self.assertIs(color, shape.color)
|
||||
|
||||
def test_helper_connects_fixed_joint_to_moving_joint(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("enclosure")
|
||||
base = assembly.add(FakePart(), "base")
|
||||
lid = assembly.add(FakePart(), "lid")
|
||||
base_frame = assembly.rigid_frame(base, "lid_seat", FakeLocation("base_frame"))
|
||||
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid_frame"))
|
||||
|
||||
relation = assembly.face_to_face(base_frame, lid_frame)
|
||||
|
||||
fixed_joint = base.joints["lid_seat"]
|
||||
moving_joint = lid.joints["underside"]
|
||||
self.assertEqual("face_to_face", relation.relation)
|
||||
self.assertEqual("lid_seat", relation.fixed)
|
||||
self.assertEqual("underside", relation.moving)
|
||||
self.assertEqual({"part": "base", "frame": "lid_seat"}, relation.fixed_endpoint)
|
||||
self.assertEqual({"part": "lid", "frame": "underside"}, relation.moving_endpoint)
|
||||
self.assertEqual([(moving_joint, {})], fixed_joint.connections)
|
||||
|
||||
def test_build_records_mate_endpoint_payloads(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("enclosure")
|
||||
base = assembly.add(FakePart(), "base")
|
||||
lid = assembly.add(FakePart(), "lid")
|
||||
base_frame = assembly.rigid_frame(base, "lid_seat", FakeLocation("base_frame"))
|
||||
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid_frame"))
|
||||
|
||||
assembly.face_to_face(base_frame, lid_frame, label="lid_mate")
|
||||
compound = assembly.build()
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
{
|
||||
"id": "m1",
|
||||
"label": "m1",
|
||||
"sourceLabel": "lid_mate",
|
||||
"type": "face_to_face",
|
||||
"relation": "face_to_face",
|
||||
"fixed": "lid_seat",
|
||||
"moving": "underside",
|
||||
"parameters": {},
|
||||
"fixedEndpoint": {"part": "base", "frame": "lid_seat"},
|
||||
"movingEndpoint": {"part": "lid", "frame": "underside"},
|
||||
}
|
||||
],
|
||||
compound.assembly_mates,
|
||||
)
|
||||
|
||||
def test_export_collection_renumbers_mates_globally(self) -> None:
|
||||
with fake_build123d():
|
||||
first = AssemblyHelper("first")
|
||||
first_base = first.add(FakePart(), "first_base")
|
||||
first_lid = first.add(FakePart(), "first_lid")
|
||||
first_base_frame = first.rigid_frame(first_base, "seat", FakeLocation("first_base"))
|
||||
first_lid_frame = first.rigid_frame(first_lid, "underside", FakeLocation("first_lid"))
|
||||
first.face_to_face(first_base_frame, first_lid_frame, label="first_mate")
|
||||
first_compound = first.build()
|
||||
|
||||
second = AssemblyHelper("second")
|
||||
second_base = second.add(FakePart(), "second_base")
|
||||
second_lid = second.add(FakePart(), "second_lid")
|
||||
second_base_frame = second.rigid_frame(second_base, "seat", FakeLocation("second_base"))
|
||||
second_lid_frame = second.rigid_frame(second_lid, "underside", FakeLocation("second_lid"))
|
||||
second.face_to_face(second_base_frame, second_lid_frame, label="second_mate")
|
||||
second_compound = second.build()
|
||||
|
||||
root = FakeCompound(label="root", children=[first_compound, second_compound])
|
||||
|
||||
mates = _collect_assembly_mates(root)
|
||||
|
||||
self.assertEqual(["m1", "m2"], [mate["id"] for mate in mates])
|
||||
self.assertEqual(["m1", "m2"], [mate["label"] for mate in mates])
|
||||
self.assertEqual(["first_mate", "second_mate"], [mate["sourceLabel"] for mate in mates])
|
||||
|
||||
def test_helper_accepts_existing_native_joint_labels(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("hinge")
|
||||
frame = FakePart()
|
||||
leaf = FakePart()
|
||||
fixed_joint = FakeJoint(
|
||||
label="hinge_axis",
|
||||
to_part=frame,
|
||||
joint_location=FakeLocation("frame_axis"),
|
||||
)
|
||||
moving_joint = FakeJoint(
|
||||
label="leaf_axis",
|
||||
to_part=leaf,
|
||||
joint_location=FakeLocation("leaf_axis"),
|
||||
)
|
||||
|
||||
relation = assembly.revolute(
|
||||
(frame, "hinge_axis"),
|
||||
(leaf, "leaf_axis"),
|
||||
angle=45,
|
||||
)
|
||||
|
||||
self.assertEqual("revolute", relation.relation)
|
||||
self.assertEqual({"angle": 45}, relation.parameters)
|
||||
self.assertEqual([(moving_joint, {"angle": 45})], fixed_joint.connections)
|
||||
|
||||
def test_axis_frames_use_native_joint_axis_argument(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("hinge")
|
||||
frame = FakePart()
|
||||
|
||||
frame_target = assembly.revolute_frame(
|
||||
frame,
|
||||
"hinge_axis",
|
||||
"Axis.Z",
|
||||
angular_range=(-90, 90),
|
||||
)
|
||||
|
||||
self.assertEqual(MateTarget(frame, "hinge_axis"), frame_target)
|
||||
joint = frame.joints["hinge_axis"]
|
||||
self.assertIsNone(joint.location)
|
||||
self.assertEqual({"axis": "Axis.Z", "angular_range": (-90, 90)}, joint.options)
|
||||
|
||||
def test_offset_target_creates_temporary_native_joint(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("offset")
|
||||
base = FakePart()
|
||||
lid = FakePart()
|
||||
base_frame = assembly.rigid_frame(base, "seat", FakeLocation("base"))
|
||||
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid"))
|
||||
|
||||
relation = assembly.face_to_face(base_frame, lid_frame, offset=0.5)
|
||||
|
||||
offset_joint = base.joints["seat:offset"]
|
||||
self.assertIsInstance(offset_joint.location, FakeLocation)
|
||||
self.assertEqual(("mul", "base", (0.0, 0.0, 0.5)), offset_joint.location.value)
|
||||
self.assertEqual("seat:offset", relation.fixed)
|
||||
|
||||
def test_build_returns_labeled_compound(self) -> None:
|
||||
with fake_build123d():
|
||||
assembly = AssemblyHelper("robot arm")
|
||||
base = assembly.add(FakePart(), "base")
|
||||
arm = assembly.add(FakePart(), "arm")
|
||||
|
||||
compound = assembly.build()
|
||||
|
||||
self.assertEqual("robot_arm", compound.label)
|
||||
self.assertEqual((base, arm), compound.children)
|
||||
|
||||
def test_target_tuple_is_available_for_call_sites(self) -> None:
|
||||
part = object()
|
||||
|
||||
self.assertEqual(MateTarget(part, "axis"), target(part, "axis"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,460 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from tests.python.support.paths import add_repo_path
|
||||
|
||||
add_repo_path("packages/cadpy/src")
|
||||
|
||||
from cadpy import generation
|
||||
from cadpy.assembly_spec import (
|
||||
IDENTITY_TRANSFORM,
|
||||
AssemblyInstanceSpec,
|
||||
AssemblyNodeSpec,
|
||||
AssemblySpec,
|
||||
)
|
||||
from cadpy.catalog import CadSource
|
||||
from cadpy.metadata import parse_generator_metadata
|
||||
from cadpy.step_export import _create_bin_xcaf_doc, export_build123d_step_scene
|
||||
from cadpy.step_scene import LoadedStepScene, _bbox_from_shape, scene_leaf_occurrences, scene_occurrence_shape
|
||||
|
||||
|
||||
def _rounded_color(color: tuple[float, ...]) -> tuple[float, ...]:
|
||||
return tuple(round(component, 3) for component in color)
|
||||
|
||||
|
||||
def _srgb_to_linear(component: float) -> float:
|
||||
if component <= 0.04045:
|
||||
return component / 12.92
|
||||
return ((component + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
class CompoundAssemblyGenerationTests(unittest.TestCase):
|
||||
def test_step_payload_rejects_legacy_output_field(self) -> None:
|
||||
with self.assertRaisesRegex(TypeError, "unsupported field\\(s\\): step_output"):
|
||||
generation._normalize_step_payload(
|
||||
{"shape": object(), "step_output": "legacy.step"},
|
||||
script_path=Path("part.py"),
|
||||
)
|
||||
|
||||
def test_step_payload_preserves_instance_assembly_mates(self) -> None:
|
||||
payload = generation._normalize_step_payload(
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"path": "servo.step",
|
||||
"name": "servo",
|
||||
}
|
||||
],
|
||||
"assembly_mates": [
|
||||
{
|
||||
"sourceLabel": "servo_to_bracket",
|
||||
"relation": "rigid",
|
||||
"fixed": "servo_mount",
|
||||
"moving": "bracket_foot",
|
||||
}
|
||||
],
|
||||
},
|
||||
script_path=Path("assembly.py"),
|
||||
)
|
||||
|
||||
self.assertEqual("servo_to_bracket", payload["assembly_mates"][0]["sourceLabel"])
|
||||
|
||||
def test_attach_envelope_assembly_mates_renumbers_for_scene(self) -> None:
|
||||
scene = LoadedStepScene(step_path=Path("assembly.step"), roots=[], prototype_shapes={})
|
||||
|
||||
generation._attach_envelope_assembly_mates(
|
||||
scene,
|
||||
{
|
||||
"assembly_mates": [
|
||||
{
|
||||
"sourceLabel": "servo_to_bracket",
|
||||
"relation": "rigid",
|
||||
"fixed": "servo_mount",
|
||||
"moving": "bracket_foot",
|
||||
}
|
||||
]
|
||||
},
|
||||
script_path=Path("assembly.py"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
{
|
||||
"id": "m1",
|
||||
"label": "m1",
|
||||
"sourceLabel": "servo_to_bracket",
|
||||
"relation": "rigid",
|
||||
"fixed": "servo_mount",
|
||||
"moving": "bracket_foot",
|
||||
}
|
||||
],
|
||||
scene.assembly_mates,
|
||||
)
|
||||
|
||||
def test_dxf_payload_rejects_legacy_output_field(self) -> None:
|
||||
with self.assertRaisesRegex(TypeError, "unsupported field\\(s\\): dxf_output"):
|
||||
generation._normalize_dxf_payload(
|
||||
{"document": object(), "dxf_output": "legacy.dxf"},
|
||||
script_path=Path("part.py"),
|
||||
)
|
||||
|
||||
def test_metadata_rejects_legacy_output_fields(self) -> None:
|
||||
cases = [
|
||||
("gen_step", "return {'shape': object(), 'step_output': 'legacy.step'}", "step_output"),
|
||||
("gen_dxf", "return {'document': object(), 'dxf_output': 'legacy.dxf'}", "dxf_output"),
|
||||
("gen_urdf", "return {'xml': '<robot />', 'urdf_output': 'legacy.urdf'}", "urdf_output"),
|
||||
("gen_sdf", "return {'xml': '<sdf version=\"1.12\" />', 'sdf_output': 'legacy.sdf'}", "sdf_output"),
|
||||
]
|
||||
for function_name, return_line, field_name in cases:
|
||||
with self.subTest(function_name=function_name), tempfile.TemporaryDirectory(prefix="cadpy-output-field-") as tempdir:
|
||||
script_path = Path(tempdir) / "part.py"
|
||||
script_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"def gen_step():",
|
||||
" return {'shape': object()}",
|
||||
"",
|
||||
f"def {function_name}():",
|
||||
f" {return_line}",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, f"unsupported field\\(s\\): {field_name}"):
|
||||
parse_generator_metadata(script_path)
|
||||
|
||||
def test_run_selected_specs_preserves_action_stdout(self) -> None:
|
||||
spec = SimpleNamespace(source_ref="part.py")
|
||||
stdout = io.StringIO()
|
||||
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
generation._run_selected_specs(
|
||||
[spec],
|
||||
action=lambda _spec: print("generator summary"),
|
||||
logger=generation.CliLogger("test", stream=io.StringIO()),
|
||||
success_message=None,
|
||||
)
|
||||
|
||||
self.assertEqual("generator summary\n", stdout.getvalue())
|
||||
|
||||
def test_compound_with_explicit_children_is_discovered_as_assembly(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
script_path = Path(tempdir) / "robot_arm.py"
|
||||
script_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"from build123d import Compound",
|
||||
"",
|
||||
"def gen_step():",
|
||||
" parts = []",
|
||||
" assembly = Compound(",
|
||||
" obj=parts,",
|
||||
" children=parts,",
|
||||
" label='robot_arm_static_display_pose',",
|
||||
" )",
|
||||
" return assembly",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
metadata = parse_generator_metadata(script_path)
|
||||
|
||||
self.assertIsNotNone(metadata)
|
||||
self.assertEqual("assembly", metadata.kind)
|
||||
|
||||
def test_compound_with_literal_obj_sequence_is_discovered_as_assembly(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
script_path = Path(tempdir) / "compound_arm.py"
|
||||
script_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"from build123d import Box, Compound",
|
||||
"",
|
||||
"def gen_step():",
|
||||
" left = Box(1, 1, 1)",
|
||||
" right = Box(1, 1, 1)",
|
||||
" return Compound(obj=[left, right], label='compound_arm')",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
metadata = parse_generator_metadata(script_path)
|
||||
|
||||
self.assertIsNotNone(metadata)
|
||||
self.assertEqual("assembly", metadata.kind)
|
||||
|
||||
def test_childless_compound_obj_sequence_is_runtime_assembly(self) -> None:
|
||||
import build123d
|
||||
|
||||
left = build123d.Box(1, 1, 1)
|
||||
right = build123d.Box(1, 1, 1)
|
||||
shape = build123d.Compound(obj=[left, right], label="compound_arm")
|
||||
|
||||
self.assertEqual("assembly", generation._shape_payload_entry_kind(shape, fallback="part"))
|
||||
|
||||
def test_labeled_childless_compound_does_not_warn_without_color(self) -> None:
|
||||
import build123d
|
||||
|
||||
left = build123d.Box(1, 1, 1)
|
||||
right = build123d.Box(1, 1, 1)
|
||||
shape = build123d.Compound(obj=[left, right], label="compound_arm")
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
_create_bin_xcaf_doc(shape)
|
||||
|
||||
messages = [str(item.message) for item in caught]
|
||||
self.assertNotIn("Unknown Compound type, color not set", messages)
|
||||
|
||||
def test_colored_child_shapes_survive_compound_assembly_export(self) -> None:
|
||||
import build123d
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
left = build123d.Box(1, 1, 1)
|
||||
left.label = "red_child"
|
||||
left.color = build123d.Color(1, 0, 0)
|
||||
right = build123d.Pos(2, 0, 0) * build123d.Box(1, 1, 1)
|
||||
right.label = "blue_child"
|
||||
right.color = build123d.Color(0, 0, 1)
|
||||
shape = build123d.Compound(children=[left, right], label="colored_assembly")
|
||||
|
||||
scene = export_build123d_step_scene(
|
||||
shape,
|
||||
Path(tempdir) / "colored_assembly.step",
|
||||
text_to_cad_entry_kind="assembly",
|
||||
)
|
||||
|
||||
colors = {
|
||||
tuple(round(component, 3) for component in color)
|
||||
for color in scene.prototype_colors.values()
|
||||
}
|
||||
colors.update(
|
||||
tuple(round(component, 3) for component in node.color)
|
||||
for root in scene.roots
|
||||
for node in root.children
|
||||
if node.color is not None
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(scene.roots))
|
||||
self.assertEqual(2, len(scene.roots[0].children))
|
||||
self.assertIn((1.0, 0.0, 0.0, 1.0), colors)
|
||||
self.assertIn((0.0, 0.0, 1.0, 1.0), colors)
|
||||
|
||||
def test_nested_colored_compound_keeps_parent_transform(self) -> None:
|
||||
import build123d
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
child = build123d.Box(1, 1, 1)
|
||||
child.label = "motor_body"
|
||||
child.color = build123d.Color(0.1, 0.2, 0.3)
|
||||
expected_color = _rounded_color(child.color)
|
||||
expected_linear_color = _rounded_color(
|
||||
(
|
||||
*(_srgb_to_linear(component) for component in expected_color[:3]),
|
||||
expected_color[3],
|
||||
)
|
||||
)
|
||||
nested = build123d.Compound(children=[child], label="imported_motor")
|
||||
placed = build123d.Pos(20, 0, 0) * nested
|
||||
placed.label = "placed_motor"
|
||||
root = build123d.Compound(children=[placed], label="arm")
|
||||
|
||||
scene = export_build123d_step_scene(
|
||||
root,
|
||||
Path(tempdir) / "arm.step",
|
||||
text_to_cad_entry_kind="assembly",
|
||||
)
|
||||
|
||||
leaves = scene_leaf_occurrences(scene)
|
||||
self.assertEqual(1, len(leaves))
|
||||
bbox = _bbox_from_shape(scene_occurrence_shape(scene, leaves[0]))
|
||||
self.assertGreater(bbox["min"][0], 19.0)
|
||||
self.assertLess(bbox["max"][0], 21.0)
|
||||
self.assertIn(
|
||||
_rounded_color(leaves[0].color),
|
||||
{expected_color, expected_linear_color},
|
||||
)
|
||||
|
||||
def test_shape_payload_can_export_with_assembly_entry_kind(self) -> None:
|
||||
import build123d
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
script_path = Path(tempdir) / "robot_arm.py"
|
||||
script_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
|
||||
output_path = script_path.with_suffix(".step")
|
||||
scene = LoadedStepScene(step_path=output_path.resolve(), roots=[], prototype_shapes={})
|
||||
left = build123d.Box(1, 1, 1)
|
||||
right = build123d.Box(1, 1, 1)
|
||||
shape = build123d.Compound(children=[left, right], label="robot_arm")
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
generation,
|
||||
"python_source_hash",
|
||||
return_value=SimpleNamespace(
|
||||
source_path="robot_arm.py",
|
||||
source_hash="hash-123",
|
||||
),
|
||||
),
|
||||
mock.patch.object(generation, "export_build123d_step_scene", return_value=scene) as export_scene,
|
||||
):
|
||||
result = generation._write_shape_step_payload(
|
||||
{"shape": shape},
|
||||
output_path=output_path,
|
||||
script_path=script_path,
|
||||
logger=generation.CliLogger("test"),
|
||||
entry_kind="assembly",
|
||||
)
|
||||
|
||||
self.assertIs(result, scene)
|
||||
self.assertEqual("assembly", export_scene.call_args.kwargs["text_to_cad_entry_kind"])
|
||||
self.assertEqual("assembly", getattr(scene, "text_to_cad_entry_kind", None))
|
||||
self.assertEqual("shape", getattr(scene, "step_payload_kind", None))
|
||||
|
||||
def test_effective_spec_follows_runtime_shape_entry_kind(self) -> None:
|
||||
step_path = Path("/tmp/compound.step")
|
||||
scene = LoadedStepScene(step_path=step_path, roots=[], prototype_shapes={})
|
||||
scene.text_to_cad_entry_kind = "assembly"
|
||||
spec = generation.EntrySpec(
|
||||
source_ref="compound.py",
|
||||
cad_ref="compound",
|
||||
kind="part",
|
||||
source_path=Path("/tmp/compound.py"),
|
||||
display_name="compound",
|
||||
source="generated",
|
||||
step_path=step_path,
|
||||
script_path=Path("/tmp/compound.py"),
|
||||
)
|
||||
|
||||
effective = generation._effective_step_spec_for_scene(spec, scene)
|
||||
|
||||
self.assertEqual("assembly", effective.kind)
|
||||
self.assertEqual("part", spec.kind)
|
||||
|
||||
def test_artifact_outputs_use_runtime_shape_entry_kind(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
|
||||
step_path = Path(tempdir) / "compound.step"
|
||||
script_path = Path(tempdir) / "compound.py"
|
||||
scene = LoadedStepScene(step_path=step_path.resolve(), roots=[], prototype_shapes={})
|
||||
scene.text_to_cad_entry_kind = "assembly"
|
||||
scene.source_kind = "python"
|
||||
scene.source_path = "compound.py"
|
||||
scene.source_hash = "source-hash"
|
||||
spec = generation.EntrySpec(
|
||||
source_ref="compound.py",
|
||||
cad_ref="compound",
|
||||
kind="part",
|
||||
source_path=script_path,
|
||||
display_name="compound",
|
||||
source="generated",
|
||||
step_path=step_path,
|
||||
script_path=script_path,
|
||||
)
|
||||
selector_bundle = generation.SelectorBundle(manifest={"stats": {}})
|
||||
|
||||
with (
|
||||
mock.patch.object(generation, "_existing_topology_artifact_matches_spec_without_scene", return_value=False),
|
||||
mock.patch.object(generation, "_existing_topology_artifact_matches_options", return_value=False),
|
||||
mock.patch.object(generation, "_selector_options_for_part", return_value=generation.SelectorOptions()),
|
||||
mock.patch.object(generation, "mesh_step_scene"),
|
||||
mock.patch.object(generation, "scene_export_shape"),
|
||||
mock.patch.object(generation, "_reset_step_artifact_dir"),
|
||||
mock.patch.object(generation, "_run_artifact_jobs", return_value={"GLB/topology": selector_bundle}),
|
||||
):
|
||||
result = generation._generate_part_outputs(
|
||||
spec,
|
||||
entries_by_step_path={step_path.resolve(): spec},
|
||||
preloaded_scene=scene,
|
||||
require_step_file=False,
|
||||
force=True,
|
||||
)
|
||||
|
||||
self.assertEqual("assembly", result.spec.kind)
|
||||
self.assertIs(result.selector_bundle, selector_bundle)
|
||||
|
||||
def test_dependency_expansion_walks_flattened_grouping_node_leaves(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-dependencies-") as tempdir:
|
||||
root = Path(tempdir)
|
||||
assembly_path = root / "grouped_assembly.py"
|
||||
assembly_step = root / "grouped_assembly.step"
|
||||
leaf_step = root / "nested_part.step"
|
||||
assembly_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
|
||||
leaf_step.write_text("ISO-10303-21; END-ISO-10303-21;\n", encoding="utf-8")
|
||||
|
||||
identity = tuple(IDENTITY_TRANSFORM)
|
||||
leaf_instance = AssemblyInstanceSpec(
|
||||
instance_id="nested_part",
|
||||
source_path=leaf_step.resolve(),
|
||||
path="nested_part.step",
|
||||
name="nested_part",
|
||||
transform=identity,
|
||||
)
|
||||
leaf_node = AssemblyNodeSpec(
|
||||
instance_id="nested_part",
|
||||
name="nested_part",
|
||||
transform=identity,
|
||||
source_path=leaf_step.resolve(),
|
||||
path="nested_part.step",
|
||||
children=(),
|
||||
)
|
||||
grouping_node = AssemblyNodeSpec(
|
||||
instance_id="front_group",
|
||||
name="front_group",
|
||||
transform=identity,
|
||||
source_path=None,
|
||||
path=None,
|
||||
children=(leaf_node,),
|
||||
)
|
||||
assembly_spec = AssemblySpec(
|
||||
assembly_path=assembly_path.resolve(),
|
||||
instances=(leaf_instance,),
|
||||
children=(grouping_node,),
|
||||
)
|
||||
entry = generation.EntrySpec(
|
||||
source_ref="grouped_assembly.py",
|
||||
cad_ref="grouped_assembly",
|
||||
kind="assembly",
|
||||
source_path=assembly_path.resolve(),
|
||||
display_name="grouped_assembly",
|
||||
source="generated",
|
||||
step_path=assembly_step.resolve(),
|
||||
script_path=assembly_path.resolve(),
|
||||
)
|
||||
leaf_source = CadSource(
|
||||
source_ref="nested_part.step",
|
||||
cad_ref="nested_part",
|
||||
kind="part",
|
||||
source_path=leaf_step.resolve(),
|
||||
source="imported",
|
||||
origin_path=leaf_step.resolve(),
|
||||
step_path=leaf_step.resolve(),
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(generation, "read_assembly_spec", return_value=assembly_spec),
|
||||
mock.patch.object(generation, "_source_lookup_by_path", return_value={leaf_step.resolve(): leaf_source}),
|
||||
):
|
||||
expanded = generation._expand_specs_with_file_dependencies([entry])
|
||||
|
||||
self.assertEqual(
|
||||
[assembly_path.resolve(), leaf_step.resolve()],
|
||||
[spec.source_path for spec in expanded],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests.python.support.paths import add_repo_path
|
||||
|
||||
add_repo_path("packages/cadpy/src")
|
||||
|
||||
from cadpy.generation_status import GenerationOutput, track_generation_run
|
||||
|
||||
|
||||
class GenerationStatusTests(unittest.TestCase):
|
||||
def test_generation_status_uses_hidden_lock_next_to_output_and_cleans_up(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-generation-status-") as tempdir:
|
||||
workspace = Path(tempdir) / "workspace"
|
||||
model_dir = workspace / "models"
|
||||
skill_dir = Path(tempdir) / "installed-skill"
|
||||
source_path = model_dir / "part.py"
|
||||
step_path = model_dir / "part.step"
|
||||
model_dir.mkdir(parents=True)
|
||||
source_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
|
||||
(skill_dir / "scripts").mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text("# skill\n", encoding="utf-8")
|
||||
|
||||
with track_generation_run(
|
||||
source_path=source_path,
|
||||
generator="gen_step",
|
||||
outputs=[GenerationOutput(step_path, "step")],
|
||||
repo_root=skill_dir,
|
||||
):
|
||||
lock_paths = tuple(model_dir.glob(".part.step.*.generation.lock.json"))
|
||||
self.assertEqual(1, len(lock_paths))
|
||||
payload = json.loads(lock_paths[0].read_text(encoding="utf-8"))
|
||||
self.assertEqual("running", payload["status"])
|
||||
self.assertEqual("gen_step", payload["generator"])
|
||||
self.assertEqual("part.py", payload["sourcePath"])
|
||||
self.assertEqual("part.step", payload["outputs"][0]["path"])
|
||||
self.assertFalse(tuple(skill_dir.rglob("*.generation.lock.json")))
|
||||
|
||||
self.assertFalse(lock_paths[0].exists())
|
||||
|
||||
def test_generation_status_writes_one_lock_for_each_output_without_blocking(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-generation-status-") as tempdir:
|
||||
model_dir = Path(tempdir) / "models"
|
||||
step_path = model_dir / "part.step"
|
||||
glb_path = model_dir / ".part.step.glb"
|
||||
model_dir.mkdir(parents=True)
|
||||
|
||||
with track_generation_run(
|
||||
source_path=None,
|
||||
generator="gen_step",
|
||||
outputs=[
|
||||
GenerationOutput(step_path, "step"),
|
||||
GenerationOutput(glb_path, "glb"),
|
||||
],
|
||||
repo_root=Path(tempdir),
|
||||
):
|
||||
step_locks = tuple(model_dir.glob(".part.step.*.generation.lock.json"))
|
||||
glb_locks = tuple(model_dir.glob("..part.step.glb.*.generation.lock.json"))
|
||||
self.assertEqual(1, len(step_locks))
|
||||
self.assertEqual(1, len(glb_locks))
|
||||
self.assertNotEqual(step_locks[0], glb_locks[0])
|
||||
|
||||
self.assertFalse(tuple(model_dir.glob("*.generation.lock.json")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from tests.python.support.paths import add_repo_path
|
||||
|
||||
add_repo_path("packages/cadpy/src")
|
||||
|
||||
from cadpy.glb import _GlbBuilder
|
||||
|
||||
|
||||
class GlbMaterialTests(unittest.TestCase):
|
||||
def test_materials_record_source_color_hint(self) -> None:
|
||||
builder = _GlbBuilder()
|
||||
|
||||
source_index = builder.add_material((0.1, 0.1, 0.1, 1.0), source_color=True)
|
||||
fallback_index = builder.add_material((0.72, 0.72, 0.72, 1.0), source_color=False)
|
||||
|
||||
materials = builder.json["materials"]
|
||||
self.assertEqual({"cadSourceColor": True}, materials[source_index]["extras"])
|
||||
self.assertEqual({"cadSourceColor": False}, materials[fallback_index]["extras"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests.python.support.paths import add_repo_path
|
||||
|
||||
add_repo_path("packages/cadpy_metadata/src")
|
||||
|
||||
from cadpy_metadata.generator import (
|
||||
GenerationOutput,
|
||||
PythonSourceIdentity,
|
||||
track_generation_run,
|
||||
xml_with_text_to_cad_metadata,
|
||||
)
|
||||
|
||||
|
||||
class MetadataGenerationStatusTests(unittest.TestCase):
|
||||
def test_generation_status_uses_hidden_lock_next_to_output_and_cleans_up(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-generation-status-") as tempdir:
|
||||
workspace = Path(tempdir) / "workspace"
|
||||
model_dir = workspace / "models"
|
||||
skill_dir = Path(tempdir) / "installed-skill"
|
||||
source_path = model_dir / "robot_urdf.py"
|
||||
urdf_path = model_dir / "robot.urdf"
|
||||
model_dir.mkdir(parents=True)
|
||||
source_path.write_text("def gen_urdf():\n return '<robot />'\n", encoding="utf-8")
|
||||
(skill_dir / "scripts").mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text("# skill\n", encoding="utf-8")
|
||||
|
||||
with track_generation_run(
|
||||
source_path=source_path,
|
||||
generator="gen_urdf",
|
||||
outputs=[GenerationOutput(urdf_path, "urdf")],
|
||||
repo_root=skill_dir,
|
||||
):
|
||||
lock_paths = tuple(model_dir.glob(".robot.urdf.*.generation.lock.json"))
|
||||
self.assertEqual(1, len(lock_paths))
|
||||
payload = json.loads(lock_paths[0].read_text(encoding="utf-8"))
|
||||
self.assertEqual("running", payload["status"])
|
||||
self.assertEqual("gen_urdf", payload["generator"])
|
||||
self.assertEqual("robot_urdf.py", payload["sourcePath"])
|
||||
self.assertEqual("robot.urdf", payload["outputs"][0]["path"])
|
||||
self.assertFalse(tuple(skill_dir.rglob("*.generation.lock.json")))
|
||||
|
||||
self.assertFalse(lock_paths[0].exists())
|
||||
|
||||
def test_generation_status_writes_one_lock_for_each_output(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-generation-status-") as tempdir:
|
||||
model_dir = Path(tempdir) / "models"
|
||||
urdf_path = model_dir / "robot.urdf"
|
||||
srdf_path = model_dir / "robot.srdf"
|
||||
model_dir.mkdir(parents=True)
|
||||
|
||||
with track_generation_run(
|
||||
source_path=None,
|
||||
generator="gen_srdf",
|
||||
outputs=[
|
||||
GenerationOutput(urdf_path, "urdf"),
|
||||
GenerationOutput(srdf_path, "srdf"),
|
||||
],
|
||||
repo_root=Path(tempdir),
|
||||
):
|
||||
urdf_locks = tuple(model_dir.glob(".robot.urdf.*.generation.lock.json"))
|
||||
srdf_locks = tuple(model_dir.glob(".robot.srdf.*.generation.lock.json"))
|
||||
self.assertEqual(1, len(urdf_locks))
|
||||
self.assertEqual(1, len(srdf_locks))
|
||||
self.assertNotEqual(urdf_locks[0], srdf_locks[0])
|
||||
|
||||
self.assertFalse(tuple(model_dir.glob("*.generation.lock.json")))
|
||||
|
||||
def test_xml_metadata_source_path_is_relative_to_output(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-xml-") as tempdir:
|
||||
workspace = Path(tempdir)
|
||||
source_path = workspace / "src" / "robot.py"
|
||||
output_path = workspace / "out" / "robot.urdf"
|
||||
identity = PythonSourceIdentity(
|
||||
source_path="src/robot.py",
|
||||
source_hash="source-hash",
|
||||
)
|
||||
|
||||
xml = xml_with_text_to_cad_metadata(
|
||||
"<robot name=\"sample\" />",
|
||||
identity,
|
||||
output_path=output_path,
|
||||
source_path=source_path,
|
||||
)
|
||||
|
||||
self.assertIn("<!-- cadpy:sourcePath=../src/robot.py -->", xml)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user