# Static workbench — assembling benches and frames

The **static workbench** is the non-kinematic half of the engine. Instead of gears that spin, you place structural parts — steel tube, sheet, wood panel, angle, bolts — and join them into one rigid body. Nothing moves each tick; the work is all geometry. Under the hood it is the *same* engine: static parts carry a **rigid** port (empty state, nothing propagates), and the layout solver + validators do the heavy lifting.

Everything is in **millimeters**, and every part's `transform.position` is the **center** of its bounding box. Meshes are authored centered, so a position reads as "where the middle of the part sits".

## The pieces

Components (category `static`, DSL namespace `statics`):

- `steelTubeSquare` — square tube ("metalon"). Legs and uprights.
- `steelTubeRectangular` — rectangular tube. Frame rails and stretchers.
- `steelTubeRound` — round tube. Handrails, light legs and frames.
- `steelChannelU` — open U (channel) profile. Stiff cross members.
- `steelFlatBar` — solid rectangular bar ("barra chata"). Braces, straps, feet.
- `steelSheet` — flat steel plate. Metal worktops, gussets.
- `woodPanel` — OSB / MDF / plywood / pine board. Worktops and shelves.
- `steelAngle` — equal-leg L-profile. Corner braces and stiffeners.
- `bolt` — metric hex bolt (M5–M12), simplified. Fastened joints and the BOM.
- `profile` — a custom 2D polygon extruded a thickness (sketch-lite). Gussets, mão-francesa braces, cut plates. See "Custom profiles" below.
- `genericSolid` — neutral bounding-box block. The fallback the scene3d importer emits for parts that have no dedicated kind yet; prefer a specific kind when one exists.
- `customPart` — a part you drew in the sketch/body editor (your parts library) placed into the bench. `statics.customPart({ id, partBodyId, bbox, weightKg, material })`: the viewer fetches the saved body and renders its real evaluated mesh, while the embedded snapshot (footprint, weight, material) keeps the bench self-contained. Author it fastest with "Usar na bancada" on a saved part, which copies a ready call to the clipboard. It exposes a rigid **body** port on every instance, so it joins welds/rests and IS counted by the connectivity / envelope / interference / BOM passes like any other static part. A **revolved** (lathe-turned) body additionally exposes a rotational **axis** port on its revolve axis (local Z through the centred origin): couple it to a shaft or gear with `mech.couple` and the turned part spins 1:1 with it (ω in = ω out, no ratio) — a flange or hub on a driven shaft. A prismatic part has no axis port and stays inert.

Accessories (also `static`):

- `casterWheel` — swivel caster (rodízio): plate + fork + nylon wheel. Weld/bolt its plate under a leg so the bench rolls; `brake: true` adds a lever.
- `levelingFoot` — adjustable foot (pé nivelador): base pad + threaded stud. Under a leg on an uneven floor.
- `hinge` — flat butt hinge (dobradiça). At a lid/door joint line.
- `handle` — tubular pull handle (puxador). On a drawer front or door.
- `drawerSlide` — telescopic drawer slide (corrediça). One per side of a drawer opening.

Junctions (also under `statics`):

- `weld(anchor, driven, params)` — rigid face-to-face contact. `params`: `face` (preferred), `align`, `gap`, `offset`, `rotationDeg` (legacy `axis`/`side` still accepted).
- `bolted(anchor, driven, params)` — a fastened joint. Same geometry as `weld`, but marks a bolted connection for the fastener BOM.
- `rest(anchor, driven, params)` — gravity rest: the driven part sits on the anchor's top surface (bottom face ↔ top face). `params`: `face` (default 'top'), `align`, `gap`, `offset`, `rotationDeg`.
- `inset(anchor, driven, params)` — an interlocking / snug-fit joint (a shelf let into a frame). v1 geometry is a flush face contact like `weld`.

## Placement hints — let the solver do the arithmetic

The junctions are backed by the **assembly solver** (plan §16): instead of computing the mating position by hand (a leg's top is at `y = L`, the rail is `h/2` thick, so the rail center is at `y = L + h/2`…), you declare **where the parts meet** and the solver derives the exact flush position from each part's dimensions. Hand arithmetic is exactly where a few-millimetre error creeps in — a top hovering over its rails, a leg passing through a shelf. Hints remove it.

- **`face`** — which face of the **anchor**, *in the anchor's own local frame*, the driven part attaches to: `'top' | 'bottom' | 'front' | 'back' | 'left' | 'right'`. The solver rotates that face normal by the anchor's rotation, so a **standing leg** (rotated `[-90, 0, 0]`) has its physical top on its local **`'front'`** (+Z) face. The driven part is placed **flush** against the face — zero gap by default.
- **`align`** — in-plane alignment: `'flush'` (default, centered on the face); `'center'` (concentric — the driven's center coincides with the anchor's center, ignoring `face`, e.g. a bolt run through a tube); or a numeric `[x, y, z]` extra world offset.
- **`offset`** — `[dx, dy, dz]` slide of the driven part along the joint face (mm).
- **`rotationDeg`** — orient the driven part; it does **not** inherit the anchor's rotation (structural members meet at 90°).

**Opt-in and back-compatible.** A junction with **no** `face`/`align` hint behaves exactly as before (legacy `axis`/`side` for `weld`, top-alignment for `rest`), so existing benches are untouched. New benches should prefer hints.

### Before / after

Placing a rail on a standing leg by hand — you must know the leg length, the rail section, and add them up:

```js
// BEFORE — arithmetic; a wrong term leaves the rail floating or sunk
const leg = statics.steelTubeSquare({ id: 'leg', width: 50, wallThickness: 2, length: 700,
  transform: { position: [25, 350, 25], rotation: [-90, 0, 0] } });
const rail = statics.steelTubeRectangular({ id: 'rail', w: 40, h: 60, wallThickness: 1.5, length: 700,
  transform: { position: [400, 730, 25], rotation: [0, 90, 0] } }); // 730 = 700 + 60/2 by hand
statics.weld(leg, rail);
```

The same joint with a hint — the rail has **no** transform; the solver places it flush on the leg's end face:

```js
// AFTER — declare intent; the solver computes the flush position
const leg = statics.steelTubeSquare({ id: 'leg', width: 50, wallThickness: 2, length: 700,
  transform: { position: [25, 350, 25], rotation: [-90, 0, 0] } });
const rail = statics.steelTubeRectangular({ id: 'rail', w: 40, h: 60, wallThickness: 1.5, length: 700 });
statics.weld(leg, rail, { face: 'front', rotationDeg: [0, 90, 0] }); // flush, gap 0
```

The **Mesa 800x500 (montagem por junção)** preset is authored entirely this way: only the root leg has an explicit transform; every other part is placed by hints. Load it to see a full four-leg table where every joint is flush by construction.

## Orientation convention

Profiles run along their **local Z**; panels lie flat with thickness along **local Y**.

- Stand a leg upright: `rotation: [-90, 0, 0]` sends its length to **+Y**.
- Run a rail along X: `rotation: [0, 90, 0]` sends its length to **+X**.
- Run a rail along Z: leave `rotation: [0, 0, 0]`.
- A panel with `rotation: [0, 0, 0]` spreads length → X, width → Z, thickness → Y.

## How to build a bench

Work bottom-up. This is the order that keeps every joint face-to-face and every part supported:

1. **Legs.** Place the vertical square tubes. A leg of length `L` standing on the floor has its center at `y = L / 2` (so its base sits at `y = 0`). Space the leg footprints around the bench outline. Anchor one leg with an explicit transform; the rest can follow from hints.
2. **Top frame.** Weld the rails onto the legs with **`face`** hints — `statics.weld(leg, rail, { face: 'front', rotationDeg: [0, 90, 0] })` places the rail flush on the standing leg's end face, gap 0, no height arithmetic. Weld a rail flush against a leg's **side** face (`face: 'right'`) to run it toward the next leg. Rails **stack** face-to-face; they never overlap.
3. **Worktop.** `rest` the panel on the top rails — `statics.rest(rail, top)` aligns the panel's bottom face with the rail's top face automatically.
4. **Lower frame + shelf.** Repeat lower down for a shelf: weld the shelf rails **beside** the legs (offset in Z so the rail face touches the leg face instead of crossing through it), weld the cross rails on top, then `rest` the shelf board.

Declare the outer bounds once, so the envelope validator can check the assembly:

```js
statics.envelope({ x: 2600, y: 900, z: 600 });
```

## Repetition — `array` and `grid`

Four identical legs, five identical shelves, a row of ten balusters: writing each one out by hand is long, error-prone, and — for the model — a runaway risk. The **repetition helpers** emit many parts from one call.

- **`statics.array(descriptor, options)`** — replicate one kind `count` times along a constant step vector. `descriptor` is `{ id, kind, params?, transform? }`: `kind` is a component kind's DSL name (a string, e.g. `'steelTubeSquare'`), `id` the base id (each replica becomes `${id}_${i}`), `transform` the base placement of replica 0. `options`: `{ count, dx, dy, dz, startIndex? }` — replica `k` is offset by `[k·dx, k·dy, k·dz]` (mm). Returns an **array of handles**, so you can weld/rest each one.
- **`statics.grid(descriptor, options)`** — replicate on a 2D lattice. `options`: `{ rows, cols, dx, dy, dz, startIndex? }` — replica `(r, c)` gets id `${id}_${r}_${c}` and offset `[c·dx, r·dy, r·dz]` (columns step along X, rows step along Y and/or Z). Returns handles in row-major order.

The handles combine with the placement hints above — array the legs, then weld a rail flush onto `legs[0]`:

```js
// Four standing legs, 760 mm apart along X, then a rail welded flush on the first:
const legs = statics.array(
  { id: 'leg', kind: 'steelTubeSquare',
    params: { width: 40, wallThickness: 2, length: 700 },
    transform: { position: [25, 350, 25], rotation: [-90, 0, 0] } },
  { count: 4, dx: 760 });
const rail = statics.steelTubeRectangular({ id: 'rail', w: 40, h: 40, wallThickness: 2, length: 2320 });
statics.weld(legs[0], rail, { face: 'front', rotationDeg: [0, 90, 0] });

// A 2 x 3 grid of MDF shelves:
statics.grid(
  { id: 'shelf', kind: 'woodPanel', params: { material: 'mdf', width: 400, length: 600, thickness: 15 } },
  { rows: 2, cols: 3, dx: 650, dy: 400 });
```

Both helpers cap the total at 512 parts and fail fast on anything larger — a guard against an accidental `count: 100000`.

## Custom profiles — `profile`

For a shape no stock kind covers — a triangular **gusset**, a **mão-francesa** shelf bracket, a cut plate — trace a 2D polygon and extrude it:

```js
// A right-triangle gusset, 120 x 120 mm, 5 mm steel, welded into a corner:
statics.profile({
  id: 'gusset',
  points: [[0, 0], [120, 0], [0, 120]], // [x, y] mm, counter-clockwise, at least 3
  thickness: 5,                          // extrusion depth along local Z
  transform: { position: [0, 0, 0], rotation: [0, 0, 0] },
});
```

Points are `[x, y]` pairs in millimeters, authored **counter-clockwise**; do **not** repeat the first point (the outline closes on its own). The polygon lies in the local X/Y plane and extrudes `thickness` along local Z; rotate the whole part into the joint plane with the transform. v1 has **no holes**, and a **self-intersecting** or degenerate outline is rejected with `E_STATIC_PROFILE` — so you get a concrete error, not a silently wrong shape. Weight is `area × thickness × steel density`.

## The three validators (your guardrail)

A static bench is checked at compile time, after layout, with three deterministic passes. Each finding is a `{ code, params }` object you can act on:

- **Connectivity** (`E_STATIC_DISCONNECTED`) — every part with a rigid port must belong to **one** connected body. Declared weld/rest junctions are the intended graph; when they alone already tie the assembly into one body, it validates clean. When they do not, bare **geometric contact** (bounding boxes touching within 30 mm) is used as a fallback so a bench placed purely by explicit transforms still connects — the "a fabricated body is one connected mass" rule the scene3d importer relies on. If, even with contact, a part floats away from the rest, it is reported. Fix: add a junction or move it into contact.
- **Contact without a junction** (`W_STATIC_CONTACT_NO_JOINT`, a **warning**, not an error) — when an **authored** bench relies on that bare-contact fallback (parts touch but nothing declares *how* they join), the compiler nudges you to add an explicit `weld`/`rest`. It never blocks the compile. **Imported** benches are exempt: the scene3d→mbl converter stamps `statics.importedFrom('scene3d')` into the source, and any bench carrying that machine-import provenance accepts contact-only connectivity silently.
- **Envelope** (`E_STATIC_ENVELOPE`) — with a declared envelope, every part's world bounding box must fit inside it (tolerance **25 mm**). The error carries the part id and its real bounds. Fix: reposition the part or grow the envelope.
- **Interference** (`E_STATIC_INTERFERENCE`) — no two parts may interpenetrate beyond the junction tolerance (**30 mm**). Parts that merely touch (a rail on a leg, a panel on a rail) have zero penetration and pass. The error names **both** parts and the depth. Fix: stack the parts face-to-face instead of overlapping them, or offset one aside.
- **Profile** (`E_STATIC_PROFILE`) — every `profile` part must trace a simple (non-self-intersecting), non-degenerate polygon of at least three points. The error carries the failing reason (`self-intersection`, `too-few-points`, `degenerate-area`). Fix: reorder the points into a simple outline.

Touching is fine; crossing through is not. Almost every "interference" is really "I put the rail's centerline where the leg's centerline is" — offset it by half of each part's width so the faces meet.

## Worked example

The **Bancada 260x60** preset is the reference assembly: 6 legs, a top frame of 2 longitudinal + 3 transversal rails carrying an OSB worktop, and a lower frame of 2 longitudinal + 5 transversal rails carrying an OSB shelf, all joined by `weld` and `rest`. It compiles clean and passes all three validators. The worktop is left **unanchored** and placed by the `rest` solver on the center transversal, so its top face lands **flush at exactly 900 mm** by construction. Load it from the presets list, or read its source to see the full coordinate table and the height stack (legs 809 + rail 40 + rail 40 + OSB 11 = 900 mm to the worktop top).

## Weight and the BOM

Every steel and wood kind reports a real weight from its cross-section and material density (steel 7850 kg/m³; OSB 640, MDF 750, plywood 600, pine 500 kg/m³). The compiler sums them onto `document.meta.static.totalWeightKg`, and `computeBom({ document })` groups identical parts (same kind + same params) into a formal **bill of materials** — quantity, unit weight, total weight — carried in `meta.bom` when you export a **.mbl**. See the [File format](/docs/file-format) page for import/export.

## Per-part descriptor and the materials list

Beyond the weight, every physical kind also answers `describeItem({ params })` with a **structured descriptor** — the buyable identity of the part, derived entirely from its params (never typed):

```js
{ material: 'steel',            // steel · wood-osb · wood-mdf · nylon · …
  format: 'rectangular-tube',   // square-tube · round-tube · channel-u · flat-bar · angle · sheet · panel · bolt · caster · …
  dims: { w: 30, h: 50, wall: 2, length: 1200 } }  // mm, only the applicable keys
```

The fields are canonical and in English so code and the training corpus stay simple; a pt-BR shop label ("Tubo metalon retangular 30x50x2mm — 1200mm") is generated from the descriptor for display. `computeBom` carries the descriptor on every line, and it rides along inside `meta.bom` when you export a **.mbl**.

The **Materiais** panel in the workspace shows the pt-BR labels and offers **Exportar CSV** and **Exportar JSON** — a client-side download of the whole list (quantity, label, material, format, dimensions, length and weight). The same structured BOM is available to agents through the `bench_bom` MCP tool, the bridge that turns a bench into a material list to quote and buy (the MyBenchLab → BuscaMetal funnel).
