MyBenchLab Documentation Open editor

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

Accessories (also static):

Junctions (also under statics):

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.

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:

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

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

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:

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.

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

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

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

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

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