Assembly Positioning Using Part-Local Datums and Explicit Transforms in text-to-cad
The text-to-cad repository implements assembly positioning through a Pythonic wrapper around build123d's joint system, where parts expose named datum frames and explicit transforms create offset joints for precise spatial relationships.
Assembly positioning in modern CAD workflows requires a clean separation between part geometry and spatial relationships. The text-to-cad project—earthtojake/text-to-cad—solves this through a purpose-built abstraction layer in packages/cadpy/src/cadpy/assembly.py that bridges high-level Python scripting with build123d's native joint mechanics. This article explains how part-local datums and explicit transforms enable reusable, parametric assembly definitions.
Part-Local Datums: Named Frames on Every Part
Every part in a text-to-cad assembly can expose named frames (datums) stored as native build123d joints. The AssemblyHelper.datum() method labels a shape with a frame name, delegating actual joint creation to the underlying library:
def datum(self, shape, name, *details, color=None):
return label_shape(shape, name, *details, color=color) # lines 132-139
The returned object is a MateTarget—a lightweight wrapper from the target() factory—that records both the part and the datum label for later relationship wiring. This design keeps datum definitions declarative and reusable across multiple assemblies.
Key benefits of part-local datums:
- Frames travel with the part, not the assembly
- Multiple assemblies can reference the same part-level frame
- Color and visualization metadata attach at definition time
Explicit Transforms: Creating Offset Copies of Datums
When a fixed datum requires spatial adjustment—such as shifting a mounting hole by 10 mm—the offset_target() function constructs a temporary RigidJoint that applies explicit translation or rotation:
def offset_target(fixed, offset, *, label=None):
fixed_target = _normalize_target(fixed)
fixed_joint_label, fixed_joint = _joint_for_target(fixed_target)
...
offset_location = _offset_location(offset)
target_location = location * offset_location
target_label = label_text(label or fixed_joint_label, "offset")
build123d.RigidJoint(
label=target_label,
to_part=fixed_target.part,
joint_location=target_location,
)
return MateTarget(part=fixed_target.part, frame=target_label) # lines 292-314
The function composes the original joint's location with user-supplied offset parameters, registers a new joint with a distinct label (e.g., "frameA:offset"), and returns a fresh MateTarget pointing to this transformed frame. This explicit transform pattern enables precise placement without mutating the original part geometry.
Connecting Parts: From Datums to Relationships
With datums and offset datums established, AssemblyHelper.connect() creates a MateRelation that records:
- The fixed and moving frames (as
(part_name, frame_label)tuples orMateTargetobjects) - The joint type:
rigid,revolute,prismatic,ball_socket, etc. - Type-specific parameters (axis vectors, angle limits, linear ranges)
The method also generates a JSON-friendly payload via assembly_mate_payload() for downstream STEP-export tooling integrated with the text-to-cad skill CLI.
Complete Working Example
from cadpy.assembly import AssemblyHelper
# 1️⃣ Create a helper for a new assembly
asm = AssemblyHelper("my_robot")
# 2️⃣ Add parts (shapes are native build123d objects)
base = asm.add(my_base_shape, "base")
arm = asm.add(my_arm_shape, "arm")
# 3️⃣ Define a datum on the arm (part-local frame)
arm_joint = asm.datum(arm, "joint_center")
# 4️⃣ Create an offset datum 10 mm along the X-axis
offset_joint = asm.offset_target(arm_joint, (10, 0, 0), label="joint_center_offset")
# 5️⃣ Connect the base to the offset datum with a revolute joint
asm.revolute_frame(base, "base_hole") # creates a datum on the base
asm.connect(
fixed=("base", "base_hole"),
moving=offset_joint,
relation="revolute",
axis=(0, 0, 1), # Z-axis of rotation
limit=(-180, 180)
)
# 6️⃣ Export the assembled CAD as a STEP file (via the skill CLI)
# $ text-to-cad cad step build --assembly my_robot
Pattern demonstrated: datum → offset_target → connect. Each stage remains pure and inspectable, with full metadata preserved for export.
Architecture Principles
The implementation in packages/cadpy/src/cadpy/assembly.py enforces three design constraints that distinguish text-to-cad from raw build123d scripting:
| Principle | Implementation | Benefit |
|---|---|---|
| Separation of concerns | datum/offset_target vs. connect/face_to_face |
Geometry and relationships evolve independently |
| Explicit over implicit | Offset transforms require explicit label and vector | No hidden state; assemblies remain reproducible |
| JSON-serializable metadata | assembly_mate_payload() generates export-ready structures |
Seamless CLI integration and viewer compatibility |
Summary
AssemblyHelperinpackages/cadpy/src/cadpy/assembly.pyprovides the core API for assembly positioning using part-local datums and explicit transforms.datum()creates named, part-local frames stored as native build123d joints (lines 132-139).offset_target()builds temporary RigidJoints for explicit spatial transforms, composing original locations with user offsets (lines 292-314).connect()wires frames into typed relationships (MateRelation) with export-ready metadata.- The architecture separates declarative frame definition from imperative relationship wiring, enabling reusable parts and parametric assemblies.
Frequently Asked Questions
How does text-to-cad differ from using build123d joints directly?
text-to-cad adds a naming and metadata layer that build123d lacks. While build123d joints are powerful, they are anonymous at runtime. The AssemblyHelper ensures every frame has a stable identifier, stores part-to-frame relationships, and generates STEP-compatible export payloads—essential for automated CAD pipelines.
Can I reuse the same part with different datum offsets in one assembly?
Yes. Because offset_target() creates new joints with unique labels (e.g., "joint_center:offset_1", "joint_center:offset_2"), you can instantiate multiple spatial variants of the same underlying part without duplication. Each variant receives its own MateTarget for independent connections.
What joint types are supported beyond revolute?
According to the source in packages/cadpy/src/cadpy/assembly.py, the connect() method accepts rigid, revolute, prismatic, and ball_socket relations. The helper also provides convenience wrappers like revolute_frame(), rigid_frame(), and face_to_face() that infer datum creation from geometric features.
Where does the actual STEP export happen?
The AssemblyHelper produces metadata only; STEP generation occurs through the skill CLI (text-to-cad cad step build --assembly <name>). The helper's assembly_mate_payload() method (in coordination with packages/cadpy/src/cadpy/metadata.py) formats joint data for this downstream consumer.
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 →