import inspect import json import math import pytest import simplecadapi as scad from simplecadapi import ql def test_material_validation_and_assignment_are_separate_from_part_creation(): body = scad.make_box_rsolid(2.0, 3.0, 1.0) part_signature = inspect.signature(scad.make_part_rpart) assert "material" not in part_signature.parameters material = scad.make_material_rmaterial( "aluminum_6061", name="Aluminum 6061", density=2.7e-6, density_unit="kg/mm^3", color=(0.7, 0.7, 0.75), ) part = scad.make_part_rpart("base_plate", body, name="Base plate") assigned = scad.assign_material_rpart(part, material) assert part.material is None assert assigned.material == material assert assigned.body is body with pytest.raises(Exception, match="density_unit"): scad.make_material_rmaterial("bad_density", density=1.0) with pytest.raises(Exception, match="color"): scad.make_material_rmaterial("bad_color", color=(1.2, 0.0, 0.0)) def test_product_and_constraint_public_apis_do_not_use_bare_star_parameters(): public_apis = [ scad.make_material_rmaterial, scad.make_placement_rplacement, scad.make_part_rpart, scad.make_assembly_rassembly, scad.add_component_rassembly, scad.make_face_connector_rconnector, scad.make_edge_connector_rconnector, scad.make_vertex_connector_rconnector, scad.make_placement_connector_rconnector, scad.add_connector_rpart, scad.add_connector_rassembly, scad.forward_connector_rassembly, scad.make_connector_ref_rconnectorref, scad.make_scalar_limit_rscalarlimit, scad.ground_component_rassembly, scad.unground_component_rassembly, scad.add_fixed_constraint_rassembly, scad.add_revolute_constraint_rassembly, scad.add_prismatic_constraint_rassembly, scad.add_gear_constraint_rassembly, scad.add_belt_constraint_rassembly, scad.add_rack_pinion_constraint_rassembly, scad.solve_assembly_constraints_rassembly, ] for api in public_apis: signature = inspect.signature(api) assert all( parameter.kind is not inspect.Parameter.KEYWORD_ONLY for parameter in signature.parameters.values() ) def test_placement_is_canonical_right_handed_frame(): placement = scad.make_placement_rplacement( origin=(10.0, 20.0, 30.0), x_axis=(0.0, 1.0, 0.0), y_axis=(-1.0, 0.0, 0.0), ) assert placement.origin == (10.0, 20.0, 30.0) assert placement.z_axis == (0.0, -0.0, 1.0) assert placement.transform_point((1.0, 2.0, 3.0)) == (8.0, 21.0, 33.0) with pytest.raises(Exception, match="orthogonal"): scad.make_placement_rplacement( origin=(0.0, 0.0, 0.0), x_axis=(1.0, 0.0, 0.0), y_axis=(1.0, 0.0, 0.0), ) with pytest.raises(Exception, match="non-zero"): scad.make_placement_rplacement( origin=(0.0, 0.0, 0.0), x_axis=(0.0, 0.0, 0.0), ) def test_assembly_components_reuse_part_and_project_to_compound(): bolt_body = scad.make_cylinder_rsolid(1.0, 2.0) bolt_part = scad.make_part_rpart("bolt", bolt_body) assembly = scad.make_assembly_rassembly("fixture") assembly = scad.add_component_rassembly( assembly, bolt_part, component_id="bolt_left", placement=scad.make_placement_rplacement(origin=(-5.0, 0.0, 0.0)), ) assembly = scad.add_component_rassembly( assembly, bolt_part, component_id="bolt_right", placement=scad.make_placement_rplacement(origin=(5.0, 0.0, 0.0)), ) assert assembly.component_ids() == ("bolt_left", "bolt_right") assert assembly.get_component("bolt_left").item is bolt_part compound = scad.make_compound_from_assembly_rcompound(assembly) assert isinstance(compound, scad.Compound) assert len(compound.get_solids()) == 2 assert math.isclose(compound.get_volume(), 2.0 * bolt_body.get_volume(), rel_tol=1e-7) face_centers_x = sorted(round(face.get_center().x, 1) for face in ql.faces().resolve(compound)) assert face_centers_x[0] < 0.0 assert face_centers_x[-1] > 0.0 def test_nested_assembly_projection_composes_component_placements(): body = scad.make_box_rsolid(1.0, 1.0, 1.0) part = scad.make_part_rpart("cube_part", body) child = scad.make_assembly_rassembly("child_assembly") child = scad.add_component_rassembly( child, part, component_id="cube", placement=scad.make_placement_rplacement(origin=(2.0, 0.0, 0.0)), ) root = scad.make_assembly_rassembly("root_assembly") root = scad.add_component_rassembly( root, child, component_id="child", placement=scad.make_placement_rplacement(origin=(10.0, 0.0, 0.0)), ) compound = scad.make_compound_from_assembly_rcompound(root) face_centers_x = sorted(round(face.get_center().x, 1) for face in ql.faces().resolve(compound)) assert len(compound.get_solids()) == 1 assert face_centers_x[0] >= 11.5 assert face_centers_x[-1] <= 12.5 def _part_with_face_connector(part_id): body = scad.make_box_rsolid(1.0, 1.0, 1.0) part = scad.make_part_rpart(part_id, body) top_face = ql.faces().resolve(body)[-1] connector = scad.make_face_connector_rconnector("axis", top_face) return scad.add_connector_rpart(part, connector) def _part_with_placement_connector(part_id, connector_origin=(0.0, 0.0, 0.0)): body = scad.make_box_rsolid(1.0, 1.0, 1.0) part = scad.make_part_rpart(part_id, body) placement = scad.make_placement_rplacement(origin=connector_origin) connector = scad.make_placement_connector_rconnector("axis", placement) return scad.add_connector_rpart(part, connector) def test_placement_connector_can_drive_fixed_constraints(): part = _part_with_placement_connector("placement_constraint_part") assembly = scad.make_assembly_rassembly("placement_constraint_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.make_placement_rplacement(origin=(1.0, 2.0, 3.0)), ) assembly = scad.add_component_rassembly( assembly, part, component_id="follower", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") assembly = scad.add_fixed_constraint_rassembly( assembly, "fixed", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("follower", "axis"), ) solved = scad.solve_assembly_constraints_rassembly(assembly) assert solved.get_component("follower").placement.origin == (1.0, 2.0, 3.0) assert scad.measure_constraint_residual_rconstraintresidual(solved, "fixed").within_tolerance def test_forwarded_connector_resolves_and_solves_at_parent_level(): inner_part = _part_with_placement_connector("forwarded_inner_part", (2.0, 0.0, 0.0)) base_part = _part_with_placement_connector("forwarded_base_part") child = scad.make_assembly_rassembly("forwarded_child") child = scad.add_component_rassembly( child, inner_part, component_id="inner", placement=scad.make_placement_rplacement(origin=(5.0, 0.0, 0.0)), ) child = scad.forward_connector_rassembly( child, connector_id="public_axis", source_component_id="inner", source_connector_id="axis", ) assert child.connector_ids() == ("public_axis",) assert child.get_connector("public_axis").placement.origin == (7.0, 0.0, 0.0) root = scad.make_assembly_rassembly("forwarded_root") root = scad.add_component_rassembly( root, base_part, component_id="base", placement=scad.make_placement_rplacement(origin=(10.0, 0.0, 0.0)), ) root = scad.add_component_rassembly( root, child, component_id="child", placement=scad.identity_placement_rplacement(), ) root = scad.ground_component_rassembly(root, "base") root = scad.add_fixed_constraint_rassembly( root, "bind_forwarded_axis", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("child", "public_axis"), ) solved = scad.solve_assembly_constraints_rassembly(root) assert solved.get_component("child").placement.origin == (3.0, 0.0, 0.0) assert scad.measure_constraint_residual_rconstraintresidual( solved, "bind_forwarded_axis", ).within_tolerance def test_forwarded_connector_validation_reports_missing_sources(): assembly = scad.make_assembly_rassembly("bad_forwarded_connector_asm") with pytest.raises(Exception, match="missing component"): scad.forward_connector_rassembly( assembly, connector_id="public_axis", source_component_id="inner", source_connector_id="axis", ) def test_fixed_revolute_and_prismatic_constraints_solve_component_placements(): part = _part_with_face_connector("constraint_part") fixed_assembly = scad.make_assembly_rassembly("fixed_asm") fixed_assembly = scad.add_component_rassembly( fixed_assembly, part, component_id="base", placement=scad.make_placement_rplacement((1.0, 2.0, 3.0)), ) fixed_assembly = scad.add_component_rassembly( fixed_assembly, part, component_id="follower", placement=scad.identity_placement_rplacement(), ) fixed_assembly = scad.ground_component_rassembly(fixed_assembly, "base") fixed_assembly = scad.add_fixed_constraint_rassembly( fixed_assembly, "fixed", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("follower", "axis"), ) fixed_solved = scad.solve_assembly_constraints_rassembly(fixed_assembly) assert fixed_solved.get_component("follower").placement.origin == (1.0, 2.0, 3.0) assert scad.measure_constraint_residual_rconstraintresidual( fixed_solved, "fixed" ).within_tolerance revolute_assembly = scad.make_assembly_rassembly("revolute_asm") revolute_assembly = scad.add_component_rassembly( revolute_assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) revolute_assembly = scad.add_component_rassembly( revolute_assembly, part, component_id="arm", placement=scad.identity_placement_rplacement(), ) revolute_assembly = scad.ground_component_rassembly(revolute_assembly, "base") revolute_assembly = scad.add_revolute_constraint_rassembly( revolute_assembly, "hinge", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("arm", "axis"), drive_angle_degrees=90.0, ) revolute_solved = scad.solve_assembly_constraints_rassembly(revolute_assembly) arm_placement = revolute_solved.get_component("arm").placement assert math.isclose(arm_placement.origin[0], 0.0, abs_tol=1e-12) assert math.isclose(arm_placement.origin[1], 0.0, abs_tol=1e-12) assert math.isclose(arm_placement.origin[2], 0.0, abs_tol=1e-12) assert math.isclose(revolute_solved.get_component("arm").placement.x_axis[0], 0.0, abs_tol=1e-10) assert math.isclose(revolute_solved.get_component("arm").placement.x_axis[1], 1.0, abs_tol=1e-10) prismatic_assembly = scad.make_assembly_rassembly("prismatic_asm") prismatic_assembly = scad.add_component_rassembly( prismatic_assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) prismatic_assembly = scad.add_component_rassembly( prismatic_assembly, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) prismatic_assembly = scad.ground_component_rassembly(prismatic_assembly, "base") prismatic_assembly = scad.add_prismatic_constraint_rassembly( prismatic_assembly, "slide", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("slider", "axis"), drive_distance=5.0, ) prismatic_solved = scad.solve_assembly_constraints_rassembly(prismatic_assembly) assert prismatic_solved.get_component("slider").placement.origin == (0.0, 0.0, 5.0) report = scad.inspect_assembly_constraints_rconstraintreport(prismatic_solved) assert report.solved def test_constraint_validation_rejects_missing_refs_and_limit_violations(): part = _part_with_face_connector("limited_part") assembly = scad.make_assembly_rassembly("limited_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) connector_a = scad.make_connector_ref_rconnectorref("base", "axis") connector_b = scad.make_connector_ref_rconnectorref("slider", "axis") assembly_with_limit = scad.ground_component_rassembly(assembly, "base") assembly_with_limit = scad.add_prismatic_constraint_rassembly( assembly_with_limit, "slide", connector_a, connector_b, drive_distance=10.0, distance_limit=scad.make_scalar_limit_rscalarlimit(0.0, 5.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly_with_limit) assert solved.get_component("slider").placement.origin == (0.0, 0.0, 5.0) with pytest.raises(Exception, match="connector"): scad.add_fixed_constraint_rassembly( assembly, "missing", connector_a, scad.make_connector_ref_rconnectorref("slider", "missing_axis"), ) with pytest.raises(Exception, match="grounded"): ungrounded = scad.make_assembly_rassembly("ungrounded_asm") ungrounded = scad.add_component_rassembly( ungrounded, part, component_id="base", placement=scad.identity_placement_rplacement(), ) ungrounded = scad.add_component_rassembly( ungrounded, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) ungrounded = scad.add_prismatic_constraint_rassembly( ungrounded, "slide_ungrounded", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("slider", "axis"), drive_distance=1.0, ) scad.solve_assembly_constraints_rassembly(ungrounded) def test_assembly_rejects_duplicate_components_raw_solids_and_cycles(): body = scad.make_box_rsolid(1.0, 1.0, 1.0) part = scad.make_part_rpart("box_part", body) placement = scad.identity_placement_rplacement() assembly = scad.make_assembly_rassembly("root") assembly = scad.add_component_rassembly( assembly, part, component_id="box", placement=placement ) with pytest.raises(Exception, match="duplicate component_id"): scad.add_component_rassembly( assembly, part, component_id="box", placement=placement ) with pytest.raises(Exception, match="item"): scad.add_component_rassembly( assembly, body, component_id="raw_solid", placement=placement ) child = scad.make_assembly_rassembly("child") child = scad.add_component_rassembly( child, assembly, component_id="parent_instance", placement=placement ) with pytest.raises(Exception, match="cycle"): scad.add_component_rassembly( assembly, child, component_id="child_instance", placement=placement ) def test_product_graph_model_json_and_replay_roundtrip(): with scad.GraphSession() as session: body = scad.make_box_rsolid(2.0, 3.0, 1.0) material = scad.make_material_rmaterial( "steel_8_8", density=7.85e-6, density_unit="kg/mm^3", ) part = scad.make_part_rpart("plate", body) part = scad.assign_material_rpart(part, material) assembly = scad.make_assembly_rassembly("fixture") assembly = scad.add_component_rassembly( assembly, part, component_id="plate_1", placement=scad.identity_placement_rplacement(), ) compound = scad.make_compound_from_assembly_rcompound(assembly) payload = json.loads(scad.export_model_json(session)) ops = [node["op"] for node in payload["graph"]["nodes"]] assert "make_material_rmaterial" in ops assert "make_part_rpart" in ops assert "make_assign_material_rpart" in ops assert "make_assembly_rassembly" in ops assert "make_add_component_rassembly" in ops assert "make_compound_from_assembly_rcompound" in ops assert any( item["entity_type"] == "Part" and item["entity_id"] == "plate" for item in payload["semantic_entity_registry"] ) replayed = scad.replay_model_json(json.dumps(payload)) assert len(replayed) == 1 assert isinstance(replayed[0], scad.Compound) assert math.isclose(replayed[0].get_volume(), compound.get_volume(), rel_tol=1e-7) def test_constraint_graph_model_json_and_replay_roundtrip(): with scad.GraphSession() as session: part = _part_with_face_connector("replay_constraint_part") assembly = scad.make_assembly_rassembly("replay_constraint_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") connector_a = scad.make_connector_ref_rconnectorref("base", "axis") connector_b = scad.make_connector_ref_rconnectorref("slider", "axis") limit = scad.make_scalar_limit_rscalarlimit(0.0, 10.0) assembly = scad.add_prismatic_constraint_rassembly( assembly, "slide", connector_a, connector_b, drive_distance=4.0, distance_limit=limit, ) solved = scad.solve_assembly_constraints_rassembly(assembly) payload = json.loads(scad.export_model_json(session)) ops = [node["op"] for node in payload["graph"]["nodes"]] assert "make_face_connector_rconnector" in ops assert "make_add_connector_rpart" in ops assert "make_connector_ref_rconnectorref" in ops assert "make_scalar_limit_rscalarlimit" in ops assert "make_ground_component_rassembly" in ops assert "make_prismatic_constraint_rassembly" in ops assert "make_solve_assembly_constraints_rassembly" in ops replayed = scad.replay_model_json(json.dumps(payload)) assert len(replayed) == 1 assert isinstance(replayed[0], scad.Assembly) assert replayed[0].get_component("slider").placement.origin == (0.0, 0.0, 4.0) assert replayed[0].constraints[0].distance_limit.lower_value == 0.0 assert solved.get_component("slider").placement.origin == (0.0, 0.0, 4.0) def test_graph_session_rejects_duplicate_product_ids(): with pytest.raises(Exception, match="duplicate part"): with scad.GraphSession(): scad.make_part_rpart("same_part", scad.make_box_rsolid(1, 1, 1)) scad.make_part_rpart("same_part", scad.make_box_rsolid(1, 1, 1)) def test_limit_aware_prismatic_tree_clamps_scalar_to_bounds(): part = _part_with_face_connector("clamp_part") assembly = scad.make_assembly_rassembly("clamp_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") assembly = scad.add_prismatic_constraint_rassembly( assembly, "slide", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("slider", "axis"), drive_distance=100.0, distance_limit=scad.make_scalar_limit_rscalarlimit(0.0, 5.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly) assert solved.get_component("slider").placement.origin == (0.0, 0.0, 5.0) residual = scad.measure_constraint_residual_rconstraintresidual(solved, "slide") assert residual.within_tolerance def test_limit_aware_prismatic_tree_uses_drive_when_within_bounds(): part = _part_with_face_connector("inrange_part") assembly = scad.make_assembly_rassembly("inrange_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="slider", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") assembly = scad.add_prismatic_constraint_rassembly( assembly, "slide", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("slider", "axis"), drive_distance=3.0, distance_limit=scad.make_scalar_limit_rscalarlimit(0.0, 10.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly) assert solved.get_component("slider").placement.origin == (0.0, 0.0, 3.0) def test_limit_aware_revolute_tree_clamps_angle_to_bounds(): part = _part_with_face_connector("rev_clamp_part") assembly = scad.make_assembly_rassembly("rev_clamp_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="base", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="arm", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") assembly = scad.add_revolute_constraint_rassembly( assembly, "hinge", scad.make_connector_ref_rconnectorref("base", "axis"), scad.make_connector_ref_rconnectorref("arm", "axis"), drive_angle_degrees=999.0, angle_limit=scad.make_scalar_limit_rscalarlimit(0.0, 45.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly) arm_x = solved.get_component("arm").placement.x_axis expected_cos = math.cos(math.radians(45.0)) expected_sin = math.sin(math.radians(45.0)) assert math.isclose(arm_x[0], expected_cos, abs_tol=1e-10) assert math.isclose(arm_x[1], expected_sin, abs_tol=1e-10) def test_limit_aware_revolute_loop_finds_optimal_angle(): part_a = _part_with_face_connector("loop_a") part_b = _part_with_face_connector("loop_b") assembly = scad.make_assembly_rassembly("rev_loop") assembly = scad.add_component_rassembly( assembly, part_a, component_id="link1", placement=scad.make_placement_rplacement(origin=(5.0, 0.0, 0.0)), ) assembly = scad.add_component_rassembly( assembly, part_b, component_id="link2", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "link1") connector_a = scad.make_connector_ref_rconnectorref("link1", "axis") connector_b = scad.make_connector_ref_rconnectorref("link2", "axis") assembly = scad.add_revolute_constraint_rassembly( assembly, "hinge", connector_a, connector_b, angle_limit=scad.make_scalar_limit_rscalarlimit(0.0, 90.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly, strict=False) residual = scad.measure_constraint_residual_rconstraintresidual(solved, "hinge") assert residual.within_tolerance def _signed_z_angle_degrees(placement): return math.degrees(math.atan2(placement.x_axis[1], placement.x_axis[0])) def test_gear_and_belt_constraints_couple_revolute_support_joints(): part = _part_with_face_connector("coupled_rotor_part") base_ref = scad.make_connector_ref_rconnectorref("base", "axis") gear_a_ref = scad.make_connector_ref_rconnectorref("gear_a", "axis") gear_b_ref = scad.make_connector_ref_rconnectorref("gear_b", "axis") gear_assembly = scad.make_assembly_rassembly("gear_coupler_asm") for component_id in ("base", "gear_a", "gear_b"): gear_assembly = scad.add_component_rassembly( gear_assembly, part, component_id=component_id, placement=scad.identity_placement_rplacement(), ) gear_assembly = scad.ground_component_rassembly(gear_assembly, "base") gear_assembly = scad.add_revolute_constraint_rassembly( gear_assembly, "drive_a", base_ref, gear_a_ref, drive_angle_degrees=90.0, ) gear_assembly = scad.add_revolute_constraint_rassembly( gear_assembly, "free_b", base_ref, gear_b_ref, ) gear_assembly = scad.add_gear_constraint_rassembly( gear_assembly, "mesh", gear_a_ref, gear_b_ref, pitch_radius_a=1.0, pitch_radius_b=2.0, ) gear_solved = scad.solve_assembly_constraints_rassembly(gear_assembly) assert math.isclose( _signed_z_angle_degrees(gear_solved.get_component("gear_b").placement), -45.0, abs_tol=1e-9, ) assert scad.measure_constraint_residual_rconstraintresidual( gear_solved, "mesh" ).within_tolerance belt_assembly = scad.make_assembly_rassembly("belt_coupler_asm") for component_id in ("base", "gear_a", "gear_b"): belt_assembly = scad.add_component_rassembly( belt_assembly, part, component_id=component_id, placement=scad.identity_placement_rplacement(), ) belt_assembly = scad.ground_component_rassembly(belt_assembly, "base") belt_assembly = scad.add_revolute_constraint_rassembly( belt_assembly, "drive_a", base_ref, gear_a_ref, drive_angle_degrees=90.0, ) belt_assembly = scad.add_revolute_constraint_rassembly( belt_assembly, "free_b", base_ref, gear_b_ref, ) belt_assembly = scad.add_belt_constraint_rassembly( belt_assembly, "belt", gear_a_ref, gear_b_ref, pulley_radius_a=1.0, pulley_radius_b=2.0, ) belt_solved = scad.solve_assembly_constraints_rassembly(belt_assembly) assert math.isclose( _signed_z_angle_degrees(belt_solved.get_component("gear_b").placement), 45.0, abs_tol=1e-9, ) assert scad.measure_constraint_residual_rconstraintresidual( belt_solved, "belt" ).within_tolerance def test_rack_pinion_constraint_couples_prismatic_and_revolute_support_joints(): part = _part_with_face_connector("rack_pinion_part") base_ref = scad.make_connector_ref_rconnectorref("base", "axis") rack_ref = scad.make_connector_ref_rconnectorref("rack", "axis") pinion_ref = scad.make_connector_ref_rconnectorref("pinion", "axis") assembly = scad.make_assembly_rassembly("rack_pinion_asm") for component_id in ("base", "rack", "pinion"): assembly = scad.add_component_rassembly( assembly, part, component_id=component_id, placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "base") assembly = scad.add_prismatic_constraint_rassembly( assembly, "rack_slide", base_ref, rack_ref, ) assembly = scad.add_revolute_constraint_rassembly( assembly, "pinion_axis", base_ref, pinion_ref, drive_angle_degrees=90.0, ) assembly = scad.add_rack_pinion_constraint_rassembly( assembly, "rack_mesh", rack_ref, pinion_ref, pitch_radius=2.0, ) solved = scad.solve_assembly_constraints_rassembly(assembly) assert math.isclose( solved.get_component("rack").placement.origin[2], -math.pi, abs_tol=1e-9, ) assert scad.measure_constraint_residual_rconstraintresidual( solved, "rack_mesh" ).within_tolerance def test_coupling_constraints_validate_positive_radii(): part = _part_with_face_connector("invalid_coupler_part") assembly = scad.make_assembly_rassembly("invalid_coupler_asm") assembly = scad.add_component_rassembly( assembly, part, component_id="a", placement=scad.identity_placement_rplacement(), ) assembly = scad.add_component_rassembly( assembly, part, component_id="b", placement=scad.identity_placement_rplacement(), ) ref_a = scad.make_connector_ref_rconnectorref("a", "axis") ref_b = scad.make_connector_ref_rconnectorref("b", "axis") with pytest.raises(Exception, match="pitch_radius_a"): scad.add_gear_constraint_rassembly( assembly, "bad_gear", ref_a, ref_b, pitch_radius_a=0.0, pitch_radius_b=1.0, ) with pytest.raises(Exception, match="pulley_radius_b"): scad.add_belt_constraint_rassembly( assembly, "bad_belt", ref_a, ref_b, pulley_radius_a=1.0, pulley_radius_b=-1.0, ) with pytest.raises(Exception, match="pitch_radius"): scad.add_rack_pinion_constraint_rassembly( assembly, "bad_rack", ref_a, ref_b, pitch_radius=0.0, ) def test_limit_aware_prismatic_loop_finds_optimal_distance(): part = _part_with_face_connector("ploop_part") assembly = scad.make_assembly_rassembly("prism_loop") assembly = scad.add_component_rassembly( assembly, part, component_id="fixed_part", placement=scad.make_placement_rplacement(origin=(0.0, 0.0, 2.0)), ) assembly = scad.add_component_rassembly( assembly, part, component_id="movable", placement=scad.identity_placement_rplacement(), ) assembly = scad.ground_component_rassembly(assembly, "fixed_part") connector_a = scad.make_connector_ref_rconnectorref("fixed_part", "axis") connector_b = scad.make_connector_ref_rconnectorref("movable", "axis") assembly = scad.add_prismatic_constraint_rassembly( assembly, "slide", connector_a, connector_b, distance_limit=scad.make_scalar_limit_rscalarlimit(-5.0, 5.0), ) solved = scad.solve_assembly_constraints_rassembly(assembly, strict=False) residual = scad.measure_constraint_residual_rconstraintresidual(solved, "slide") assert residual.within_tolerance