---
title: "Anchors"
description: "The placement model — part-local coordinates, the six faces, the twelve edges, face (u, v) positions, anchor path syntax, and why an anchor carries a secondary axis."
url: https://kaicad.barpom.xyz/docs/schema/anchors
schema: "0.1"
source: packages/spec/src/anchors.ts
---

# 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

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

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  ╱
   width
```

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

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_end
bottom_left  bottom_right  bottom_start  bottom_end
left_start  left_end  right_start  right_end
```

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

```yaml
features:
  - kind: chamfer
    edges: [end_*]
    size: 3
```

## Anchor paths

```
<part>[@<instance>]:<kind>.<name>[.<qualifier>]
```

```yaml
side_rail:face.start              # a part with no instances
side_rail@left:face.start         # one named instance
head_post@left:feature.panel_slot # a named feature on an instance
slat_cleat@left:edge.top_start
centre_rail:point.center
centre_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`](/docs/validation/error-codes#res-007), and the
finding lists what the part does have.

### 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:

```yaml
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.start
```

The 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

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 — `top` and
> `bottom` — 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`](/docs/schema/joints#align) overrides it. `roll` in
90° steps is normal and expected — it is not a sign you have done something wrong.

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

```yaml
- 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

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`:

```yaml
- 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:

```yaml
- id: panel_to_head_post_right
  mate:
    a: head_post@right:feature.panel_slot
    b: headboard_panel:face.end
    align:
      roll: 180     # ← the mirror, undone
```

If a mirrored part comes out inverted, `roll: 180` is almost always the fix.

## 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.yaml
```

In the app, the workbench's **isolate** and **dimensions** controls do the same job visually.
