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

783 lines
30 KiB
Python

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