Anchors
Parts are never placed by absolute transform. Every part exposes named anchors, and a
joint mates anchor A to anchor B. There is no escape hatch, and no at: [x, y, z] on a part.
Read this page before writing a joint. It is the concept that a model composing YAML gets wrong most often, and the failure is the quiet kind: the file validates, the geometry builds, and the piece of furniture is simply incorrect.
Why there is no coordinate field
Section titled “Why there is no coordinate field”Absolute coordinates encode an answer — where the rail ended up — instead of the reason: the rail’s end sits against the post’s inner face. When the mattress size changes, the reason still holds and the answer does not. A design written in coordinates has to be recomputed everywhere; a design written in anchors recomputes itself.
It also removes an entire failure class. A wrong anchor path is caught in milliseconds with a JSON Pointer at the line that wrote it. A plausible-looking wrong coordinate is caught by someone in a workshop.
Part-local coordinates
Section titled “Part-local coordinates”Every blank is a box centred on its own origin:
+Z thickness length +X ±length/2 │ ┌───────────┐ width +Y ±width/2 │ ╱ ╱│ thickness +Z ±thickness/2 │ ┌───────────┐ │ │ │ │ │ The origin is the centre of the ───────┼─│─────── ───│─┼── +X box, not a corner. ╱│ │ │ ╱ length ╱ │ └───────────┘ +Y ╱ widthThe six faces
Section titled “The six faces”| Face | Direction | On a board |
|---|---|---|
start |
−X | One end, along the length |
end |
+X | The other end |
left |
−Y | One long edge |
right |
+Y | The other long edge |
bottom |
−Z | One broad face |
top |
+Z | The other broad face |
start and end are the ends of a board — the end-grain faces on a solid part. top and
bottom are the broad faces. These names are about the part’s own axes, not about how it ends
up sitting in the room: a post standing vertically still has its start at the bottom, because
that is the low end of its length.
The twelve edges
Section titled “The twelve edges”An edge is named for the two faces that meet at it, and the order does not matter — top_left
and left_top are the same edge, and both resolve to the canonical top_left.
top_left top_right top_start top_endbottom_left bottom_right bottom_start bottom_endleft_start left_end right_start right_endOpposite faces never meet, so there is no top_bottom.
Wildcards work in edge selectors, which is how chamfers and roundovers are usually written:
| Selector | Selects |
|---|---|
top_* |
The four edges of the top face |
*_start |
The four edges around the start end |
end_* |
The four edges around the end end |
* |
All twelve |
features: - kind: chamfer edges: [end_*] size: 3Anchor paths
Section titled “Anchor paths”<part>[@<instance>]:<kind>.<name>[.<qualifier>]side_rail:face.start # a part with no instancesside_rail@left:face.start # one named instancehead_post@left:feature.panel_slot # a named feature on an instanceslat_cleat@left:edge.top_startcentre_rail:point.centercentre_rail:center # shorthand — a bare name means point.center| Kind | Names | What the frame is |
|---|---|---|
face |
top bottom left right start end |
The centre of that face, normal pointing out |
edge |
top_left, … |
The midpoint of the edge, normal bisecting the two faces |
feature |
a feature’s id |
The feature’s own frame — see below |
point |
center (or centre) |
The part’s origin |
axis |
length width thickness |
The origin, normal along that axis |
A malformed or unknown path is RES-007, and the
finding lists what the part does have.
Feature anchors are the ones that matter
Section titled “Feature anchors are the ones that matter”A named feature exposes an anchor at its own frame, sitting in the recess with its normal pointing out of the part. Mating to it seats the neighbour in the recess rather than on the surface:
parts: - id: head_post features: - kind: groove id: panel_slot # ← the id is what makes it mateable on: right along: length width: 18 depth: 10 center: true stopped: 40
joints: - id: panel_to_head_post_left kind: dado mate: a: head_post@left:feature.panel_slot b: headboard_panel:face.startThe point of that indirection: the groove and the panel that lives in it cannot drift apart. Move the groove and the panel follows. Positioning the panel with an offset and cutting the groove independently is exactly the quiet error this model exists to prevent.
Grooves, dados, rabbets, mortises, pockets and holes can be mated to. A chamfer cannot — it has no frame to sit in.
Why an anchor carries a secondary axis
Section titled “Why an anchor carries a secondary axis”A point and a normal leave the roll about that normal free. One unconstrained degree of freedom is precisely what an author would otherwise have to pin down with a coordinate, which would put the escape hatch back.
So every anchor carries a secondary axis as well, and a single mate is a complete placement. The convention is mechanical:
The secondary axis is +Z, except on the faces whose normal is already ±Z —
topandbottom— where it is +X.
When a part arrives rotated the wrong way about the mate normal, that is the secondary axis
disagreeing with you, and align.roll overrides it. roll in
90° steps is normal and expected — it is not a sign you have done something wrong.
Face coordinates (u, v)
Section titled “Face coordinates (u, v)”Holes and pockets are positioned in a face’s own 2D frame, measured from the face’s minimum corner, so both numbers are positive and read the way a person would measure them with a tape.
| Face | u runs along |
v runs along |
|---|---|---|
top, bottom |
length | width |
left, right |
length | thickness |
start, end |
width | thickness |
- kind: hole on: top diameter: 8 depth: 12 at: { u: 100, v: center }u and v always run along the positive part axis. That means the triad (u, v, normal) is
right-handed on some faces and left-handed on others. It is a deliberate trade: predictable
authored numbers matter more here than a consistent handedness nobody writing a design would
notice.
Mirrored instances
Section titled “Mirrored instances”A part with instances named exactly left and right is mirrored about the design’s YZ
plane. Mirroring carries the faces and features with it, and there are two consequences to
hold onto.
The inner face swaps sides. The inner face of the left post is its right face; the inner
face of the right post is its left face. No {i} substitution can express that, which is
why rail-to-post joints are normally written out twice rather than with for_each:
- 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 }Features arrive flipped. The groove cut on the right face of the left post is on the
right face of the right post too — but upside down. A panel mating into both goes in
backwards on one side until you turn it over:
- id: panel_to_head_post_right mate: a: head_post@right:feature.panel_slot b: headboard_panel:face.end align: roll: 180 # ← the mirror, undoneIf a mirrored part comes out inverted, roll: 180 is almost always the fix.
Checking your work
Section titled “Checking your work”fcad inspect <file> prints where every part ended up — world bounds and transform, per
instance. It is the fastest way to find out whether an anchor said what you meant, and it
needs no CAD kernel:
fcad inspect fixtures/bed-queen/design.yamlIn the app, the workbench’s isolate and dimensions controls do the same job visually.