---
skill-group: dev-conventions
skill-order: 4
---

# The Component Model

## Parts Are Components. Assemblies Are Composition.

ForgeCAD's multi-part design model follows one principle:

> **A part is a function from props to shape + connectors. It knows nothing about where it sits in the world. The assembly decides.**

This is how React changed UI: components render content, the layout system positions them. Components don't know their screen coordinates. Before React, jQuery code positioned elements in page-space. After React, components are local and composable.

ForgeCAD applies the same principle to mechanical design. Before: parts compute their assembly-space coordinates (translate to the right X, Y, Z, using a shared-dims file). After: parts build at origin in local space, declare connectors, and the assembly uses `connect()` to position everything.

## The Three Rules

### Rule 1: Parts Build at Origin

A part is built in its own local coordinate system. Origin at `[0, 0, 0]`. No knowledge of the assembly. No imports of sibling part files. No global coordinate offsets.

```js
// WRONG — part positions itself in assembly space
const rackPlaced = rack.translate(pinionPitchR, 0, layout.pinionZ);

// RIGHT — part builds at origin, connector declares the interface
const rack = lib.rackGear({ ... }).rotateZ(90);
// pitch line at X=0, teeth face -X, local coordinates only
return rack.withConnectors({
  teeth: connector("rack-slide", { origin: [0, 0, faceWidth/2], axis: [0, 1, 0] }),
});
```

### Rule 2: Connectors Are the Interface

Every part that participates in an assembly declares connectors. A connector says: "here's where I connect, what direction I face, and what roll I consider upright." It's the part's public interface — the only thing the assembly needs to know.

```js
return mount.withConnectors({
  flange: connector("bolt-face", {
    origin: [0, 0, 0],       // where the mating surface is
    axis: [0, 0, 1],         // direction it faces (outward from part)
    up: [1, 0, 0],           // roll reference when orientation matters
  }),
});
```

A connector is a small local frame:

- `origin` is the pivot, contact point, socket center, or mating-face point.
- `axis` is the primary direction: face normal, hinge line, or slide direction.
- `up` is the secondary direction that fixes roll around `axis`.

`up` is not world up. It is the connector's local roll reference and must not be parallel to `axis`. If `up` is omitted, ForgeCAD picks a deterministic perpendicular vector. That is fine when roll does not matter, but hinges, wheels, levers, and keyed parts should author `up` explicitly.

Connectors meet **face-to-face**. Both axes point outward from their respective parts. The system brings them together from opposite sides, then uses `up` to pin the rest orientation — like plugging in a keyed connector instead of a round peg.

### Rule 3: The Assembly Is Pure Composition

The assembly file does three things: instantiate parts with props, connect them, and define motion. Zero coordinate math. Zero translate calls. Zero shared-dims imports.

```js
assembly("Gripper")
  .addPart("Base", base)
  .addPart("Mount", mount)
  .connect("Base.mount_face", "Mount.flange", { as: "mount-fix" })
  .addPart("Pinion", pinion)
  .connect("Base.pinion_seat", "Pinion.bore", { as: "spin" })
  // ...
```

If you're writing `translate()` in an assembly file, something is wrong. Either the part should have a connector, or the prop should carry the dimension.

## Data Flow: Props Down, Metadata Up

Data flows in one direction: from parent (assembly) to children (parts).

```
Assembly (root)
  ├── passes { servo, wall, clearance } → Motor Mount
  │     └── returns { shape, boltPattern, flange connector }
  ├── passes { gears, boltPattern } → Base Body
  │     └── returns { shape, pinionZ, rack connectors }
  ├── passes { gears, rackZ, armSpan } → Jaw Unit
  │     └── returns { shape, armWidth, rack connector }
  └── passes { boltPattern, armSlot } → Cover Plate
        └── returns { shape, seat connector }
```

**Props flow down:** The assembly knows the cross-cutting decisions (base height, wall thickness, servo model) and passes them to parts as `require()` param overrides.

**Metadata flows up:** Parts return their shape + connectors + any computed data that siblings need. The assembly reads this metadata and passes relevant pieces to other parts.

**Siblings never import each other.** The cover plate doesn't import the motor mount to read bolt positions. The assembly reads the mount's bolt pattern and passes it to the cover plate. The parent mediates all sibling communication.

```js
// Assembly mediates sibling data flow
const mount = require('./motor-mount.forge.js', { Wall: wall });
const base  = require('./base-body.forge.js', {
  Height: baseH,
  BoltPattern: mount.boltPattern,  // parent passes mount's data to base
});
const cover = require('./cover-plate.forge.js', {
  BoltPattern: mount.boltPattern,  // same data to cover
  ArmSlot: jaw.armProfile,         // jaw's data to cover
});
```

## Why This Matters

### For Humans

Each part file tells a complete story. You open `motor-mount.forge.js` and see everything: its geometry, its connectors, its parameters, its verifications. No chasing imports across 8 files. No 266-line shared-dims.js to understand.

Modifying one part doesn't break siblings — connectors are the contract. Change the mount's internal cavity dimensions and nothing else changes, as long as the flange connector stays at the same position.

### For AIs

Each part file is self-contained context. An AI generates `base-body.forge.js` without reading `jaw-unit.forge.js`. The assembly file is a high-level plan — generate it from a natural language description. Props are the interface contract — the AI knows exactly what each part needs and provides.

### For Swap-ability

Change the servo model → the mount rebuilds with new cavity dimensions → its bolt pattern updates → the assembly passes the new pattern to base and cover → everything adapts. One change, cascading update, zero manual re-derivation.

## The Connector Convention

Connectors follow one convention: **face-to-face connector frames**.

Each connector's axis points **outward** from its part — the direction the connection faces. When two connectors mate, the system makes their origins coincide and, for fixed/revolute joints, negates one axis so they approach from opposite sides. The `up` vectors define the roll reference that makes the rest pose deterministic.

- **Fixed joints** (bolt flange): both faces point outward, system brings them together
- **Revolute joints** (hinge): both parts point outward along the hinge line, system opposes them, and `up` pins zero angle
- **Prismatic joints** (slider): both connectors point along the slide direction (co-directional exception — the slide axis IS the shared direction), while `up` still pins roll around the rail

Revolute joint values are still signed by the physical hinge axis. If a bilateral
mechanism mirrors a hinge axis, the mirrored pose uses the negated physical value
(`Right: +theta`, `Left: -theta`), and physical limits mirror as
`[min, max] -> [-max, -min]`. Prismatic joints do not have this angle-handedness
flip. Use a side-neutral link graph or explicit state mapping when equal semantic
pose values should drive both sides.

```js
// Base bottom face points down, mount flange points up → meet in the middle
base.withConnectors({ mount_face: connector("bolt-face", { origin: [0,0,0], axis: [0,0,-1], up: [1,0,0] }) });
mount.withConnectors({ flange: connector("bolt-face", { origin: [0,0,0], axis: [0,0,1], up: [1,0,0] }) });

// Hinge: axes oppose along the pin, up pins the closed/rest roll
frame.withConnectors({ hinge: connector("hinge", { origin: [0,0,40], axis: [0,0,1], up: [1,0,0] }) });
door.withConnectors({ hinge: connector("hinge", { origin: [0,0,40], axis: [0,0,-1], up: [1,0,0] }) });
```

## One File vs. Many Files

**Default: one file per assembly.** For a project-specific assembly (robot gripper, drone frame), put everything in one file. Parts are sections separated by comments. Shared data is just variables. This maximizes locality — you see the whole design by scrolling.

**Split when:** A part is reusable across projects (standard servo cradle, parametric gear pair), or the file exceeds ~300 lines. Split parts into separate `.forge.js` files that export component functions.

**Never split for "organization."** Eight files with 40 lines each is worse than one file with 300 lines. The context-switching cost of 8 files destroys locality.

## What This Forbids

1. **No shared-dims files.** If you're writing a file whose only job is to compute derived dimensions from component specs, the assembly should be doing that work (or the parts should derive their own internal dimensions from props).

2. **No sibling imports.** A part file never `require()`s another part file. Data flows through the parent.

3. **No assembly-space coordinates in parts.** A part doesn't know `pinionZ = 14`. It receives `rackZ: 14` as a prop, or it computes its own internal Z from its own props.

4. **No `translate()` in assembly files.** If you're translating a part to position it, use a connector instead. The exception: ghost/reference geometry that isn't structural.

5. **No console.log validation.** Use `verify.*` for spatial checks. It's structured, clickable, and fails visibly.

## Design Gate

**Every new multi-part example and every agent-built assembly must follow this model:**

> Can you understand each part file without reading any other file? Does the assembly file contain zero coordinate math? Do all inter-part relationships flow through connectors and props?

If the answer to any is no, refactor until it's yes.
