Implementing AssemblyHelper with build123d Joints and Mating Datums in Text-to-CAD
The AssemblyHelper class in cadpy provides a semantic wrapper around build123d's joint system, enabling stateless assembly construction with automatic metadata generation for named frames, native joint instantiation, and JSON-compatible mate payloads.
The text-to-cad repository by earthtojake implements a modular, agent-driven CAD workflow where high-level skills delegate geometry handling to reusable Python packages. At the center of this architecture sits the AssemblyHelper, a thin abstraction layer that transforms verbose build123d joint operations into concise, readable assembly code with built-in export metadata.
What AssemblyHelper Provides
Located in [viewer/packages/cadpy/src/cadpy/assembly.py](https://github.com/earthtojake/text-to-cad/blob/main/viewer/packages/cadpy/src/cadpy/assembly.py), the AssemblyHelper addresses three core challenges in programmatic CAD assembly:
- Named coordinate frames – MateTarget objects combine a part reference with a human-readable label from
label_text - Native joint creation – Helper methods instantiate build123d joints (
RevoluteJoint,LinearJoint,RigidJoint) without direct API manipulation - Automatic relation tracking – Every connection generates a MateRelation record capturing labels, joint types, fixed/moving names, and parameters
This stateless design means you add shapes, define datums, connect them, then call build() to receive a labeled build123d.Compound ready for STEP or GLB export.
Core Components of the Assembly System
MateTarget: Named Frames for Parts
Before connecting parts, you establish datum frames using semantic labels. The MateTarget class stores:
- A reference to the parent part
- A generated label from
label_text - Position and orientation data
from cadpy.assembly import AssemblyHelper
asm = AssemblyHelper("mechanism")
part = asm.add(shape, "bracket")
datum = asm.revolute_frame(part, "hinge_point", axis=(0, 1, 0))
Joint Helper Functions
The helper provides specialized methods for common joint types in [assembly.py](https://github.com/earthtojake/text-to-cad/blob/main/viewer/packages/cadpy/src/cadpy/assembly.py):
| Method | Joint Type | Parameters |
|---|---|---|
add_rigid_frame() |
RigidJoint |
Fixed transformation |
add_axis_frame() |
RevoluteJoint / LinearJoint |
Axis vector, origin |
add_joint_frame() |
Generic | Depends on joint subclass |
revolute_frame() |
Convenience wrapper | Axis, angle limits |
revolute() |
Creates connection | fixed, moving, label, angle |
These methods handle the underlying build123d instantiation while preserving metadata for downstream export.
MateRelation and Payload Generation
Every successful connection produces a MateRelation record. When you call assembly_mate_payload(), these records serialize to JSON-compatible dictionaries:
{
'id': 'm1',
'label': 'm1',
'sourceLabel': 'shoulder',
'type': 'revolute',
'fixed': 'base_axis',
'moving': 'arm_axis',
'parameters': {'angle': 90}
}
The compound() method injects this assembly_mates metadata directly into the returned build123d.Compound, making it available to the viewer runtime.
Practical Implementation Examples
Two-Part Assembly with Revolute Joint
This example from the test suite demonstrates the complete workflow:
from cadpy.assembly import AssemblyHelper
# 1️⃣ Create the helper for the assembly
asm = AssemblyHelper("my_robot")
# 2️⃣ Add parts (these could be any build123d shapes)
base = asm.add(shape_base, "base")
arm = asm.add(shape_arm, "arm")
# 3️⃣ Define datum frames on each part
base_axis = asm.revolute_frame(base, "base_axis", axis=(0, 0, 1))
arm_axis = asm.revolute_frame(arm, "arm_axis", axis=(0, 0, 1))
# 4️⃣ Connect the frames with a revolute joint
asm.revolute(fixed=base_axis, moving=arm_axis, label="shoulder", angle=90)
# 5️⃣ Build the final compound (contains joint metadata)
robot = asm.build()
Offset Datum Positioning
For precise part placement, use offset_target() to translate a datum before connection:
# Offset the datum 10 mm along Z before connecting
offset_target = asm.offset_target(fixed, offset=(0, 0, 10), label="offset_z")
asm.connect(fixed_target=offset_target, moving=arm_axis, relation="rigid")
This pattern appears frequently in cadpy's assembly logic when parts need clearance or specific mounting positions.
Inspecting Generated Metadata
Verify your assembly structure before export:
payload = asm.compound().assembly_mates
print(payload)
# → [{'id': 'm1', 'label': 'm1', 'sourceLabel': 'shoulder',
# 'type': 'revolute', 'fixed': 'base_axis', 'moving': 'arm_axis',
# 'parameters': {'angle': 90}}]
This metadata drives the web viewer's joint visualization and enables accurate URDF generation in the CAD skill layer.
Architecture Integration
The AssemblyHelper operates within a four-layer stack as implemented in the text-to-cad repository:
- build123d – External dependency providing low-level geometry and joint primitives
- cadpy package – Internal wrapper with
AssemblyHelper,MateTarget, and utilities - Skill layer – Agent-written code using the helper (see SKILL.md)
- Viewer runtime – JavaScript consumer of
assembly_matesmetadata for web rendering
Unit tests in [tests/python/packages/cadpy/test_assembly_helper.py](https://github.com/earthtojake/text-to-cad/blob/main/tests/python/packages/cadpy/test_assembly_helper.py) validate fixed-joint versus moving-joint connection semantics, ensuring predictable behavior across assembly complexity levels.
Summary
- AssemblyHelper in [
cadpy/assembly.py](https://github.com/earthtojake/text-to-cad/blob/main/viewer/packages/cadpy/src/cadpy/assembly.py) wraps build123d joints with semantic, label-driven APIs - MateTarget objects provide named coordinate frames on parts using
label_textgeneration - MateRelation records capture connection metadata for JSON export via
assembly_mate_payload() - Stateless design enables clean skill code: add parts, define datums, connect, then
build() - Generated compounds carry
assembly_matesmetadata consumed by the viewer runtime and export pipelines
Frequently Asked Questions
How does AssemblyHelper differ from using build123d joints directly?
AssemblyHelper eliminates boilerplate by managing joint instantiation, frame labeling, and metadata serialization in a single fluent interface. Direct build123d usage requires manual Joint object creation, explicit Label attachment, and separate bookkeeping for export metadata. The helper's stateless pattern also makes skill code more testable and readable.
Can I mix custom build123d joints with AssemblyHelper?
Yes, but with trade-offs. The helper's add_joint_frame() method accepts custom joint instances, though you'll lose automatic metadata generation for those connections. For full metadata capture, extend the helper with additional connection methods following the pattern in [assembly.py](https://github.com/earthtojake/text-to-cad/blob/main/viewer/packages/cadpy/src/cadpy/assembly.py).
What export formats preserve the mate metadata?
GLB and STEP exports consume the assembly_mates payload injected by compound(). The viewer runtime reads this metadata for interactive joint manipulation. URDF generation in the CAD skill transforms the same payload into robot description format.
How do I validate an assembly before export?
Run unit test patterns from [test_assembly_helper.py](https://github.com/earthtojake/text-to-cad/blob/main/tests/python/packages/cadpy/test_assembly_helper.py): inspect asm.compound().assembly_mates for expected connection counts, verify fixed and moving labels match your intent, and confirm parameters like angle or offset appear in the payload.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →