Skip to content

Joints

This page as Markdown/docs/schema/joints.mdView .md

A joint does three things from one declaration:

  1. It positions parts — the mate places one anchor against another.
  2. It creates hardware — the fixture becomes real items on the BOM.
  3. 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.
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.top

through 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_frommating 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.

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 / 2

Each 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: false
removable: true
fit: { 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.

- 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.

- 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 / 2

That 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.