Joints
A joint does three things from one declaration:
- It positions parts — the mate places one anchor against another.
- It creates hardware — the fixture becomes real items on the BOM.
- It cuts machining into both parts — every hole, counterbore and countersink in a compiled design comes from a fixture’s recipe.
Hardware can never be decorative. If there is a screw in the model, there is a pilot hole in the receiving part and a clearance hole in the through part, and the validator has checked its engagement and its edge distance.
joints: - id: cleat_to_rail_left kind: screwed fixture: fixtures.wood_screw.4x50 mate: a: slat_cleat@left:face.top b: side_rail@left:face.bottom align: contact: true offset: [0, -cleat_drop, 0] fasteners: pattern: { along: length, count: 10, spacing: auto, margin: 80 }| Key | ||
|---|---|---|
id |
required | Referenced by outputs.drawings.details and by findings. |
kind |
required | One of the twelve below. |
mate |
required | The two anchors, and how they align. |
fixture |
A catalogue fixture id. Required for hardware kinds. | |
through |
Which part the fastener passes through. | |
fasteners |
{ pattern: … } — where the fasteners go. |
|
for_each |
Expand once per named instance. | |
array |
Expand into a distributed run — see Arrays. | |
glue |
Whether the joint is glued. | |
removable |
Whether it comes apart. | |
fit |
{ clearance: … } — overrides the fixture’s default. |
The twelve kinds
Section titled “The twelve kinds”| Kind | Fixture | |
|---|---|---|
butt |
optional | Two parts simply touching |
glue_only |
no | Held by glue alone |
screwed |
required | A wood screw through one part into another |
doweled |
required | Fluted dowels, glued |
mortise_tenon |
no | Traditional joinery; the mortise is a feature |
dado |
no | One part seated in the other’s trench |
rabbet |
no | One part seated in the other’s L-cut |
bed_bolt |
required | Bolt into a cross dowel — knock-down |
cam_lock |
required | Flat-pack cam and dowel bolt |
threaded_insert |
required | Machine screw into an insert — knock-down |
pocket_screw |
required | Angled screw from a pocket |
bracket |
required | Metal bracket |
kind is what the assembly steps and the drawing call the joint. It does not change the
geometry — the fixture does that.
mate: a: side_rail@left:face.start b: head_post@left:face.top align: contact: true roll: -90 offset: [rail_drop_on_head_post, 0, 0]a moves; b is the target. Both are
anchor paths. The part on the a side is placed so that
its anchor meets the anchor on the b side.
Placement propagates outwards from the ground part. The chain matters: ground the headboard panel, hang the posts off its ends, the rails off the posts and the foot posts off the rails, and the whole piece stays symmetric about its centre line for free.
| Key | |
|---|---|
contact: true |
Make the two faces coincident and opposed. The normal case. |
offset: [x, y, z] |
A nudge, in the target anchor’s local frame. |
roll: <degrees> |
Rotation about the mate normal, when the default is wrong. |
flip: true |
Flip the moving part end-for-end about the mate normal. |
offset is local, not a position. It is expressed in the frame of the anchor named in
b, which is what keeps it small and meaningful — “35 mm down the post’s inner face” rather
than “at z = 265”. If an offset in your file is a large number that looks like a coordinate,
the mate is probably attached to the wrong anchor.
# Slide the cleat down the rail's inner face.align: contact: true offset: [0, -cleat_drop, 0]roll in 90° steps is normal. Each anchor carries a secondary axis so that one mate is a
complete placement, and the convention that sets it is mechanical rather than clever — see
why an anchor carries a secondary axis.
When a part arrives rotated about the mate normal, roll is the correction, and roll: 180
is the usual fix for a mirrored instance arriving upside down.
through — which way the hardware goes in
Section titled “through — which way the hardware goes in”- id: rail_to_head_post_left kind: bed_bolt fixture: fixtures.bed_bolt.m8x150 through: head_post@left mate: a: side_rail@left:face.start b: head_post@left:face.topthrough names the part the fastener passes through. It defaults to the a side of the
mate, and it is stated explicitly whenever that is wrong.
These are two different questions. The mate says where the parts sit. through says which way
the hardware is driven in, and there is no reason the two should agree: a bed bolt goes
through the post from the outside into the end of the rail, whichever order the mate happened
to be written in. Tying them together would mean rewriting a working mate in order to fix a
hole.
The fixture then supplies the rest. Each entry declares a through_from — mating for a
screw that enters on the face touching the other part, opposite for a bed bolt whose
counterbore is on the outside face. Get through wrong and you put an 18 mm counterbore in
the wrong surface and run the clearance hole down the length of the board instead of across
it, which is exactly the kind of unbuildable design that still compiles and exports cleanly.
fasteners
Section titled “fasteners”Where the fasteners go, as a pattern:
fasteners: pattern: { along: length, count: 10, spacing: auto, margin: 80 }fasteners: pattern: along: length count: 2 spacing: bolt_pitch from: rail_centre_height - bolt_pitch / 2 to: rail_centre_height + bolt_pitch / 2Each position in the pattern becomes one fixture on the BOM and one full set of machining operations on both parts. The fixture’s recipe decides what those are; you never write a pilot hole yourself.
glue, removable and fit
Section titled “glue, removable and fit”glue: falseremovable: truefit: { clearance: 0.3 }removable: true is how a joint declares itself knock-down, and it is what
requirements.knock_down is checked against. fit.clearance overrides the fixture’s
catalogue default for this joint only.
for_each, and when not to use it
Section titled “for_each, and when not to use it”- id: shelf_pins kind: butt for_each: [a, b, c] mate: a: shelf@{i}:face.bottom b: side@left:face.right{i} is substituted with each name in turn. It is a genuine saving when the instances are
interchangeable.
It is not usable for mirrored left/right pairs. The inner face of the left post is its
right face and the inner face of the right post is its left face, and no substitution can
express that difference. Write those two joints out:
- id: rail_to_head_post_left mate: { a: side_rail@left:face.start, b: head_post@left:face.top }
- id: rail_to_head_post_right mate: { a: side_rail@right:face.start, b: head_post@right:face.bottom }The duplication is honest. A for_each that quietly places one rail on the wrong face is not.
A joint may expand into a distributed run of one part instead — slats, shelves, pickets. That has its own page: Arrays.
Worked: a bed bolt
Section titled “Worked: a bed bolt”- id: rail_to_head_post_left kind: bed_bolt fixture: fixtures.bed_bolt.m8x150 removable: true through: head_post@left mate: a: side_rail@left:face.start b: head_post@left:face.top align: contact: true roll: -90 offset: [rail_drop_on_head_post, 0, 0] fasteners: pattern: along: length count: 2 spacing: bolt_pitch from: rail_centre_height - bolt_pitch / 2 to: rail_centre_height + bolt_pitch / 2That one declaration produces, per bolt: a 9 mm clearance hole and an 18 mm × 8 mm counterbore through the post from its outside face, a 9 mm bore 100 mm into the end of the rail, and a 12.5 mm cross bore through the rail’s width at 80 mm depth for the dowel to capture the bolt. Ten operations across two parts from two lines of pattern, and every one of them checked.
The cross-dowel bore is the one that ruins knock-down beds when it lands in the wrong place, and it is not a number anyone should be typing.