<!-- read_docs topic "dsl", generated from app/src/dsl.ts -->

# parcad DSL reference

The parcad modelling language.

- Millimetres. Every primitive is centred on the origin; place it with
  `.at(x, y, z)`.
- Shapes are values: reusing one reuses the node.
- A script returns one shape, or named bodies, `return { base, lid }`,
  which are measured apart and against each other and never fused.
  Selectors, tags and treatments work inside one body.
- `checks: [...]` beside the bodies is a list of rules the build must
  hold, judged on every build and reported first; see `Check`.
- A shape may be a *surface*: faces with no inside and free edges where it
  ends. It reports area and free edges instead of a volume; booleans,
  fillets, wall thickness and STL refuse it until `.thicken(t)` makes it a
  solid. See `surfaceLoft`.

    const hole = cylinder(3, 40);
    return box(60, 30, 6).cut(hole.at(20, 0, 0), hole.at(-20, 0, 0));

A script may name its own variables after anything below; a `const box` hides the primitive for the whole script, above the declaration too. What the language cannot do, and what to write instead, is the `gaps` topic.

## Functions

### brief

`brief(declared: Brief): void`

What this part is for: the requirement it is judged against on every build.

Every reply carries a `brief` key — with none declared it says the part's
size and volume are measured against nothing, which is the whole nudge.
A brief never refuses a save; it is a sentence, in the place a reader
meets first.

- `envelope` is `[x, y, z]` in mm and is judged in the best of the six
  axis orientations, so the order does not matter.
- `budgetCm3` is plastic in cm³; `printer` is a bed name from `BEDS`.
- `holds`, `gesture` and `material` are prose, echoed and never judged.
- Call it once, anywhere in the script, before or after the geometry.

@example
    brief({ envelope: [95, 70, 16], budgetCm3: 12, holds: ["3 × 2€"] });
    return box(90, 60, 12).cut(box(80, 50, 10).at(0, 0, 2));

### sheet

`sheet(thickness: number, options: SheetOptions = {}): Process`

A sheet a body is cut from — plywood, MDF, acrylic — for `.made()`, so the
print check holds that body to a saw instead of to a printer.

- `thickness` is in mm; `material` is prose the reply echoes.
- Give each piece of the sheet its own body: one body is one piece.
- A sheet body gets no bed fit, overhang or thin-wall flags. It still
  fails for a wall under 0.3 mm or an edge sharper than a saw's 45° bevel,
  is flagged when it is in several pieces, and stays in `between_bodies`.

@example
    const ply = sheet(18, { material: "birch plywood" });
    const side = box(18, 300, 400).at(-141, 0, 200).made(ply);
    const top = box(300, 300, 18).at(0, 0, 409).made(ply);
    return { side, top };

### note

`note(label: string, value: number | string | number[]): void`

Report a value the script computed. It appears in the reply's `notes`,
marked "from the script, not measured", and changes nothing about the
part.

- `value` is a number, a string or a list of numbers; `label` names it.
- A note is what the script asked for or worked out, never what was
  built: quote a measurement for anything that can be measured.
- At most 40 notes and 2000 characters; the rest are counted as
  `dropped`.

@example
    const mouth = 23.25 + 0.5;
    note("mouth 2€", mouth);
    return box(mouth + 4, 30, 10).cut(box(mouth, 30, 8).at(0, 0, 1));

### partParams

`partParams<T extends Record<string, unknown>>(defaults: T): T`

Declare the values this part takes when another part imports it, and read them.

- Returns `defaults` with what an `importPart` call passed laid over them;
  run on its own, the part gets `defaults` unchanged.
- An importer may pass only keys in `defaults`, each of its default's type;
  anything else is refused, naming the keys this part takes.
- Call it once per part.

@example
    const p = partParams({ od: 8, bore: 3.4, height: 10 });
    return cylinder(p.od / 2, p.height).cut(cylinder(p.bore / 2, p.height + 1));

### importPart

`importPart(name: string, params: Record<string, unknown> = {}): ImportedPart`

Build another saved part inside this one, with its parameters set.

- `name` is the part's name in the parts list, e.g. `"Fasteners/spacer"`;
  its saved `part.js` runs, not an unsaved edit of it.
- `params` overrides what that part declares with `partParams`.
- Returns what that part returns: a shape, placed like any other with
  `.at()`, or a multi-body part's object of bodies.
- Its tags come along; tag the import itself to tell two copies apart.

@example
    const spacer = importPart("spacer", { height: 12 });
    return box(40, 20, 4).union(spacer.at(0, 0, 8));

### box

`box(x: number, y: number, z: number): Shape`

A rectangular block with the given full extents, centred on the origin.

### sphere

`sphere(r: number): Shape`

A ball of radius `r` — a radius, like every other primitive here.

### cylinder

`cylinder(r: number, h: number): Shape`

A cylinder along Z with the given radius and full height.

Centred like every primitive, in Z as well: `cylinder(3, 20)` runs from
z = -10 to z = +10, so a hole through a part that stands on z = 0 is placed
at the middle of its own length, not at the face it enters. `holeFor()` does
that arithmetic, and overshoots both ends.

### torus

`torus(major: number, minor: number, options: { sweep?: number } = {}): Shape`

A ring: a circle of radius `minor` swept round Z at radius `major`, with
`minor < major`. Both are radii: a 2 mm cord O-ring on a 20 mm ID is
`torus(20 / 2 + 2 / 2, 2 / 2)`. `sweep` in degrees, from +X anticlockwise,
makes part of a ring.

### revolve

`revolve(profile: SectionEntry[], options: { wall?: WallSpec } = {}): Shape`

A closed section in the (radius, z) half-plane, revolved a full turn about Z:
a turned part, drawn as the shape a lathe tool leaves. The section is a list
of `SectionEntry`; a radiused shoulder is `{ at: [r, z], round: 1 }`.

- Radius >= 0 everywhere on the boundary, control points included.
- A stepped (re-entrant) section is fine; a self-crossing one is not.
- `wall: t` hollows it to a turned wall `t` thick, open at both ends: a
  bell. `wall: { thickness, bottom: "closed" }` keeps a floor, `top:
  "closed"` a lid. An end that opens is a straight edge from the axis.

@example revolve([[0, 0], [10, 0], [10, 4], { at: [6, 4], round: 1 }, [6, 12], [0, 12]])
@example revolve([[0, 0], [10, 0], [10, 20], [0, 20]], { wall: { thickness: 1, bottom: "closed" } })  // a cup

### cone

`cone(r1: number, r2: number, h: number): Shape`

A cone or truncated cone along Z, centred on the origin like every other
primitive: `r1` at the bottom, `r2` at the top.

A cone *is* a revolved triangle, which is why this is four lines rather than
a kernel primitive. `cone(r, 0, h)` is a point; `cone(r, r, h)` is a cylinder
built the long way round, and `cylinder()` is the one to use for that.

### countersink

`countersink(head: number | string, includedAngle = 90): Shape`

A countersink cutter for a screw head: the frustum a drill leaves, sized by
the head diameter and the included angle.

90° is the ISO metric countersink and the default here; 82° is the imperial
one. The cutter is returned positioned so its wide end sits at z = 0 — cut it
where the hole breaks out, `.at(x, y, faceZ)`.

### tapDrill

`tapDrill(thread: string): number`

Drill diameter for a coarse-pitch tapped hole: `tapDrill("M6")` is 5.0.

### clearance

`clearance(thread: string, fit: Fit = "normal"): number`

Clearance hole diameter, ISO 273: `clearance("M6")` is 6.6, close fit 6.4.

### counterbore

`counterbore(thread: string): { diameter: number; depth: number }`

Counterbore for a socket head cap screw: the diameter to bore and how deep.

The bore is the head diameter plus 1 mm of drill clearance, and the depth is
the head height, which for these screws is the thread diameter itself.

### holeFor

`holeFor(thread: string, depth: number, options: { fit?: Fit; tapped?: boolean; through?: boolean } = {}): Shape`

A hole cutter along Z for a named fastener, entering at z = 0 going down:
place it at the face it enters. `holeFor("M6", 12)` is a blind clearance hole
12 mm deep; `tapped` drills for a thread a machinist will tap; `through`
also clears the far face. It already overshoots by 0.5 mm. A printed or
modelled thread is `threadedHole`.

@example box(60, 30, 6).cut(holeFor("M5", 6, { through: true }).at(20, 0, 3))  // entering the top face

### threadedRod

`threadedRod(size: ThreadSize, length: number, options: ThreadOptions = {}): Shape`

An external ISO 68-1 thread along Z, centred on the origin like `cylinder`,
squared off at both ends; union a head or shank onto it. `size` is `"M8"`
(coarse pitch) or `{ diameter, pitch }`.

- The tooth crosses +X at z = 0 of the rod's own frame. A rod and a
  `threadedHole` of the same size mate only when their frames are a whole
  number of pitches apart along Z.
- A nut reading **interfering** is out of phase (move it a whole pitch) or
  its hole stops short (place the hole's frame on the face it enters).

@example
    const bolt = threadedRod("M6", 20, { clearance: 0.2 });   // frame at z = 0
    const nut = box(10, 10, 5).at(0, 0, 2.5)                  // z 0 to 5
      .cut(threadedHole("M6", 5, { through: true, clearance: 0.2 }).at(0, 0, 5));
    return { bolt, nut };                                      // 5 mm = 5 pitches: in phase

### threadedHole

`threadedHole(size: ThreadSize, depth: number, options: ThreadOptions & { through?: boolean } = {}): Shape`

The cutter for an internal screw thread along Z, entering at z = 0 going
down like `holeFor`: a nut, a cap, a threaded boss. Cut it, do not union it.
Its tooth crosses +X at the entry face, so a `threadedRod` mates a whole
number of pitches away along Z. A hole a machinist will tap is
`holeFor(size, depth, { tapped: true })`.

@example box(20, 20, 10).cut(threadedHole("M8", 10, { through: true }).at(0, 0, 5))  // a nut

### vesaPattern

`vesaPattern(size: 75 | 100 | 200): [number, number][]`

The square VESA mounting patterns, as four points for `repeat()`: MIS-D at
75 and 100 mm on M4, MIS-E/F at 200 mm on M6. Centred on the origin, the
way a monitor arm's plate is; pick rows off the list with `filter` when a
bracket only reaches one of them.

### line2d

`line2d(from: [number, number], to: [number, number]): Line2d`

A line through two points, or from a point in a direction at an angle in degrees.

### line2d

`line2d(from: [number, number], angleDegrees: number): Line2d`

### inset

`inset(outline: SectionEntry[], by: number): SectionEntry[]`

An outline stepped inward by `by` mm, as a section: the inside of a wall.
Any section will do: corners, arcs, curves, one closed `{ fit }`.

- An arc stays an arc and a fitted curve one curve; never offset the
  points yourself.
- Where the outline turns inward (an L, a notch, a step) the inset keeps
  the corner, `by / cos(turn / 2)` from the outline's: the wall thickens
  into the valley.
- An inset that splits, vanishes or comes nearer than `by` is refused with
  the distance that fits. Inset once by the sum, never an inset of an
  inset.
- A lofted wall is `loft`'s `wall` and a turned wall `revolve`'s, not a
  loft or revolve of insets.

@example
    // a 60 × 40 tray, 1.6 mm walls and a 2 mm floor
    const outline = [{ at: [-30, -20], round: 6 }, { at: [30, -20], round: 6 }, { at: [30, 20], round: 6 }, { at: [-30, 20], round: 6 }];
    return extrude(outline, 20).cut(extrude(inset(outline, 1.6), 20).at(0, 0, 2));

### spurGearOutline

`spurGearOutline(options: { module: number; teeth: number; pressureAngle?: number; profileShift?: number; addendum?: number; dedendum?: number; backlash?: number; tolerance?: number; }): SectionEntry[]`

The outline of an involute spur gear for `extrude`: centred on the origin,
a tooth on +X, every flank certified to lie within `tolerance` mm of the
true involute.

- `module` (mm) and `teeth` are required. Defaults: `pressureAngle` 20°,
  `addendum` `module`, `dedendum` `1.25 * module`.
- The part reports the flanks' proven bound as `curve_bound_mm`, with
  `curve_bound: "certified"`; `tolerance` (default 0.0001) is only a cap.
- `backlash` thins each tooth by that many mm at the pitch circle.
- Under 17 teeth at 20° is refused as undercut unless `profileShift`
  (x, in modules) is large enough; the refusal names the least.

@example
    // module 2, 20 teeth, 10 mm thick, 6 mm bore
    return extrude(spurGearOutline({ module: 2, teeth: 20 }), 10).cut(cylinder(3, 12).at(0, 0, 5));
@example extrude(spurGearOutline({ module: 2, teeth: 12, profileShift: 0.3 }), 8)  // a 12-tooth pinion, shifted

### spurGearPair

`spurGearPair(options: { module: number; teeth: [number, number]; profileShift?: [number, number]; pressureAngle?: number; backlash?: number; tolerance?: number; }): { centres: number; pressureAngle: number; turn: number; outlines: [SectionEntry[], SectionEntry[]] }`

Two full-depth involute spur gears that mesh, and where to put them.

- `teeth` is `[first, second]`, `profileShift` their shifts (default
  `[0, 0]`); `module`, `pressureAngle` and `tolerance` are as in
  `spurGearOutline`.
- `backlash` is the pair's play in mm: built as returned, the flanks are
  `backlash / 2` apart in `between_bodies`.
- Returns `{ centres, pressureAngle, turn, outlines }`: `centres` mm
  between the axes, `pressureAngle` the working one in degrees, `turn` the
  degrees to rotate the second gear before placing it at `[centres, 0]`.
- Undercut, interference and a contact ratio under 1 are refused.

@example
    // 12 and 30 teeth, module 2, the pinion shifted to avoid undercut
    const pair = spurGearPair({ module: 2, teeth: [12, 30], profileShift: [0.3, 0], backlash: 0.1 });
    const pinion = extrude(pair.outlines[0], 8);
    const wheel = extrude(pair.outlines[1], 8).rotate("z", pair.turn).at(pair.centres, 0, 0);
    return { pinion, wheel };

### hull

`hull(points: [number, number][]): [number, number][]`

The convex outline through a set of points, anticlockwise, ready for
`extrude`. What a fan, a flare or a gusset is: "the shape that joins these
corners", with the convexity `extrude` demands guaranteed by construction
rather than checked after. Points inside the hull are dropped; three
distinct points that are not collinear are the least it accepts.

### extrude

`extrude(profile: SectionEntry[], height: number, options: { draft?: number } = {}): Shape`

A closed outline in XY (a list of `SectionEntry`), given a thickness along Z.
Like every primitive it is centred on the origin in Z, from `-height / 2`
to `+height / 2`; the outline places it in X and Y.

- `draft` leans the walls in by that many degrees going up, and needs a
  convex outline of straight edges: fillet the vertical edges afterwards.

@example extrude([[-5, -5], [5, -5], { through: [10, 0] }, [5, 5], [-5, 5], { through: [-10, 0] }], 3)  // 20 × 10 slot

### ngon

`ngon(sides: number, size: number, height: number, options: { across?: "corners" | "flats"; draft?: number } = {}): Shape`

A regular polygon prism along Z, first corner on +X: hex stock, a square
drive. `size` is across the corners unless `{ across: "flats" }`, which is
how hex bar and spanners are sized.

### pipe

`pipe(points: PathPoint[] | HelixPath | SplinePath, diameter: number, options: SweepOptions = {}): Shape`

A round tube of `diameter` along a path of points, a `HelixPath` or a
`SplinePath`: hydraulic line, hose, wire, spring. Another profile is `sweep`.

- `bend` is the centreline bend radius at every corner. Without it corners
  are filled with a ball: fine for clearance, not a makeable tube.
- `taper` along a path of points needs a `bend`.

@example pipe([[0, 0, 0], [40, 0, 0], [40, 30, 0]], 6, { bend: 10 })

### loft

`loft(sections: LoftSection[], options: { smooth?: boolean; wall?: WallSpec; } = {}): Shape`

Skin a solid through two or more `LoftSection`s at rising `z`. Walls are
ruled (straight between sections) unless `smooth: true`.

- Walls pair section edges by index, so every outline needs the same edge
  count: a straight edge, arc or curve is one, a rounded corner adds one.
  A circle lofted to a square is four arcs between four corners.
- Listing an outline rotated twists the wall; the kernel never untwists it.
- `wall: t` makes a shell `t` mm thick inward from `{ fit }` sections: a
  vase, not a loft minus a loft. `wall: { thickness, bottom: "closed" }`
  gives it a floor, `top: "closed"` a lid.
- List each `{ fit }` section's points from the same start, same way round.

@example loft([{ z: 0, outline: [[-10, -10], [10, -10], [10, 10], [-10, 10]] }, { z: 30, point: [0, 0] }])  // a pyramid
@example
    // a vase 80 tall with a 1.6 mm wall and a floor
    const ring = (r) => Array.from({ length: 120 }, (_, i) => [r * Math.cos(i * Math.PI / 60), r * Math.sin(i * Math.PI / 60)]);
    return loft([0, 40, 80].map((z) => ({ z, outline: [{ fit: ring(30 + z / 4), tolerance: 0.01 }] })), { smooth: true, wall: { thickness: 1.6, bottom: "closed" } });

### sweep

`sweep(profile: SectionEntry[], path: PathPoint[] | HelixPath | SplinePath, options: SweepOptions = {}): Shape`

Sweep a `SectionEntry` profile along a path of points, a `HelixPath` or a
`SplinePath`: `pipe` with any section. A round one stays a `pipe`.

- The profile starts perpendicular to the path, its +Y as near global +Z as
  the path allows; on a helix its +X points away from the axis.
- A path of points that turns needs `bend`, which must fit the legs and
  clear the profile's own extent.

@example sweep([[-2, -1], [2, -1], [2, 1], [-2, 1]], [[0, 0, 0], [0, 0, 20], [30, 0, 20]], { bend: 8 })

### surfaceExtrude

`surfaceExtrude(curve: SectionEntry[], height: number, options: CurveOptions = {}): Shape`

A surface: a curve in the XY plane extruded `height` mm along Z, centred
on z = 0. Fusion's surface Extrude.

- The curve is open unless `closed: true` (see `CurveOptions`).
- The normal is to the right of the curve's travel, seen from +Z.
- A surface has area and free edges, no volume; `.thicken(t)` makes it a
  solid.

@example
    // an L-shaped sheet, 30 + 20 long and 40 tall: 2000 mm²
    return surfaceExtrude([[0, 0], [30, 0], [30, 20]], 40);
@example surfaceExtrude([[0, 0], { spline: [[10, 6], [20, -6]] }, [30, 0]], 40)  // a wavy sheet

### surfaceRevolve

`surfaceRevolve(curve: SectionEntry[], options: CurveOptions & { degrees?: number } = {}): Shape`

A surface: a curve in the (radius, z) plane revolved about Z. Fusion's
surface Revolve: a dome from a quarter arc, a shade from a line.

- Points are `[radius, z]`, radius >= 0. The curve is open unless
  `closed: true` (see `CurveOptions`).
- `degrees` (default 360) revolves part of a turn, anticlockwise from +X.
- The normal is to the right of the curve's travel: a curve drawn upward
  faces outward.

@example
    // a radius-40 dome, open at its rim: 2π · 40² = 10,053 mm²
    return surfaceRevolve([[40, 0], { through: [40 * Math.SQRT1_2, 40 * Math.SQRT1_2] }, [0, 40]]);

### surfaceLoft

`surfaceLoft(sections: LoftCurve[], options: CurveOptions & { smooth?: boolean } = {}): Shape`

A surface through two or more `LoftCurve`s at rising `z`: Fusion's surface
Loft, for a sheet, blade or shade that stays open. Ruled between curves
unless `smooth: true`.

- Curves are open unless `closed: true`, which makes a tube open at both
  ends (see `CurveOptions`).
- Pieces pair by index, as in `loft`: every curve needs the same count.
- For curves from points, give each curve one `{ fit }` over the same
  number of points, listed from the same end (or, closed, the same start
  and the same way round). Point `i` then lies on one line of the surface.
- A surface has no volume; `.thicken(t)` makes it a printable solid.

@example
    // a 60° strip of a radius-40 cylinder, 40 tall: 40 · π/3 · 40 = 1676 mm²
    const arc = (r) => Array.from({ length: 12 }, (_, i) => {
      const a = (Math.PI / 3) * (i / 11) - Math.PI / 6;
      return [r * Math.cos(a), r * Math.sin(a)];
    });
    return surfaceLoft([
      { z: 0, curve: [{ fit: arc(40), tolerance: 0.01 }] },
      { z: 40, curve: [{ fit: arc(40), tolerance: 0.01 }] },
    ], { smooth: true });
@example surfaceLoft([{ z: 0, curve: [[-20, 0], [20, 0]] }, { z: 30, curve: [[-20, 0], [20, 0]] }]).thicken(2)  // 2400 mm³

### surfaceSweep

`surfaceSweep(curve: SectionEntry[], path: PathPoint[] | HelixPath | SplinePath, options: CurveOptions & { bend?: number } = {}): Shape`

A surface: a curve swept along a path. Fusion's surface Sweep.

- The path is what `sweep` takes: points (`bend` rounds every corner),
  `{ helix: { radius, pitch, turns } }` or `{ spline: [[x, y, z], ...] }`.
- The curve is drawn in the plane square to the path's start, as `sweep`
  draws its profile, and is open unless `closed: true`.
- The normal is to the right of the curve's travel in that plane.

@example
    // a ribbon 10 wide along a path 30 + π/2 · 10 + 20 long: 657 mm²
    return surfaceSweep([[-5, 0], [5, 0]], [[0, 0, 0], [0, 0, 40], [30, 0, 40]], { bend: 10 });

### stitchSurfaces

`stitchSurfaces(...args: (Shape | { tolerance?: number; solid?: boolean })[]): Shape`

Surfaces sewn into one along the edges they share: Fusion's Stitch. A
result with no free edge left is a solid.

- The report's `kind` says `solid` only when the shell measured closed;
  otherwise `kind: "surface"`, its opening in `surface.free_edge_length_mm`.
- Edges closer than `tolerance` (default 0.01 mm, at most 0.5) are joined.
- `solid: true` refuses a result that does not close, listing its free
  edges: the way to insist on a watertight part.

@example
    // four sides and two patches: a closed 20 mm cube, 8000 mm³
    const sides = surfaceExtrude([[-10, -10], [10, -10], [10, 10], [-10, 10]], 20, { closed: true });
    const top = sides.edges({ role: "boundary", at: { z: "max" } }).patch();
    const bottom = sides.edges({ role: "boundary", at: { z: "min" } }).patch();
    return stitchSurfaces(sides, top, bottom, { solid: true });

### union

`union(...args: (Shape | BoolOptions)[]): Shape`

Fuse every shape given into one solid.

`union(a, b, { blend: 3 })` rounds the seam the join creates by 3 mm, which
is a fillet in the exact backend. Two solids that meet *exactly* on a face
abort the kernel when the union is blended, at any radius — overlap them
instead, and see the `gotchas` document before reaching for a blend.

### intersect

`intersect(...args: (Shape | BoolOptions)[]): Shape`

Keep only the volume every one of these shapes occupies.

### repeat

`repeat(shape: Shape, points: [number, number, number?][]): Shape`

Copies of `shape` at every point, unioned together.

### grid

`grid(cols: number, rows: number, dx: number, dy: number): [number, number][]`

A centred `cols` x `rows` grid of points. `dx` and `dy` are the
centre-to-centre pitch, not the span: `grid(3, 1, 20, 0)` is x = -20, 0, 20.

### polar

`polar(count: number, radius: number, options: PolarOptions = {}): [number, number][]`

`count` points spaced evenly around a circle of `radius`, as `[x, y]` for
`repeat`: a bolt circle,
`repeat(holeFor("M6", 8, { through: true }), polar(6, 40))`.

### around

`around(shape: Shape, count: number, axis: "x" | "y" | "z" = "z"): Shape`

`count` copies of `shape` spun evenly about an axis through the origin and
unioned: for a feature that must turn with its position, like a flute round
a knob. Place the shape once at its radius, then `around(flute, 12)`.

### scriptBudget

`scriptBudget(multiple: number): void`

Let this script do `multiple` times the default work: the first line of a
part that runs a simulation, a growth or a search.

- `multiple` is a whole number from 1 to 10; calling again only raises it.
- Work is counted in interpreter steps, not timed: the default is 600
  million, far more than a drawn part uses. A refusal says how much was
  allowed. `evaluate_part`'s `timeout_s` does not raise it.

@example
    scriptBudget(8);
    return box(10, 10, 10);

### simulateReactionDiffusion

`simulateReactionDiffusion(options: ReactionDiffusionOptions): { a: number[]; b: number[] }`

Run a reaction-diffusion field to the pattern it settles into (Turing
spots, stripes, lobes) and return both fields, `{ a, b }`, one value per
cell: what a generative part grows its outline from.

- `size: n` is a ring of cells, `size: [w, h]` a grid that wraps both
  ways; see `ReactionDiffusionOptions`.
- A `dt` too long to stay stable is refused naming the longest that is; a
  run that diverges is refused too.
- Seed the starting noise deterministically, never with `Math.random`.

@example
    const cells = 100;
    const noise = Array.from({ length: cells }, (_, i) => 1 + 0.01 * Math.sin(i * 7.3));
    const { a } = simulateReactionDiffusion({
      model: "gierer-meinhardt", size: cells, a: noise, b: noise.map(() => 1),
      diffusion: [0.3, 60], dt: 0.2 / 60, steps: 12000,
      kappa: 0.05, decay: [1, 1.2], source: [0.01, 0],
    });
    // a ring whose radius swells where the activator peaks
    const ring = a.map((v, i) => [(20 + v) * Math.cos((2 * Math.PI * i) / cells), (20 + v) * Math.sin((2 * Math.PI * i) / cells)]);
    return extrude([{ fit: ring, tolerance: 0.05 }], 5);

### outlineCrossings

`outlineCrossings(points: [number, number][]): [number, number][]`

Where a closed outline of points crosses or touches itself: every pair
`[i, j]`, `i < j`, of segments that meet without being neighbours, sorted.

- Segment `i` runs from point `i` to point `i + 1`, the last back to the
  first.
- Empty for a clean loop: the check a growth or offset loop makes after
  every move.

@example
    // a bow tie: its two diagonals, segments 1 and 3, cross at [5, 5]
    const bow = [[0, 0], [10, 0], [0, 10], [10, 10]];
    const pairs = outlineCrossings(bow); // [[1, 3]]
    return box(10, 10, 2 * pairs.length);

### outlineGaps

`outlineGaps(points: [number, number][], options: { ignoreWithin: number; upTo?: number }): number[]`

For each point of a closed outline of points, the width of the gap it
faces: the distance to the nearest part of the outline that is at least
`ignoreWithin` mm away along the outline.

- `ignoreWithin` leaves out the point's own stretch of curve; set it to
  about the narrowest gap you care about.
- Gaps wider than `upTo` (default: no limit), and points with nothing far
  enough along, come back as `Infinity`.

@example
    // a U: its two arms face each other across a 4 mm slot
    const u = [[0, 0], [14, 0], [14, 20], [9, 20], [9, 5], [5, 5], [5, 20], [0, 20]];
    const narrowest = Math.min(...outlineGaps(u, { ignoreWithin: 6, upTo: 10 })); // 4, at [9, 20] and [5, 20]
    return extrude(u, narrowest);

### build

`build(root: Part, treatments?: TreatmentSource[], stacks?: (string | undefined)[]): Doc`

Flatten a shape, or an object of named bodies, into the JSON graph. A
script does not need it: returning the part is enough.

## Shape methods

### Shape.tag

`tag(name: string): Shape`

Name this shape's faces, for selectors (`on`, `between`) and for
`tag_extents` in the report. Changes this shape in place and returns it;
a second tag replaces the first everywhere the shape is used.

### Shape.reference

`reference(): Shape`

Mark this body as a reference: a real object the part is checked
against — a stack of coins, a tipped coin at a mouth — that is not the
part. Call it last, on the shape the returned object names.

- Built, drawn in blue, measured alone (`named_bodies`, `reference:
  true`) and against every body (`between_bodies`), so a `clear` or
  `interferes` check can name it.
- Never exported, probed, or counted: not in `bodies`, `volume_mm3`,
  `size` or `stands_on`.
- At least one body must not be a reference.

@example
    const plate = box(60, 40, 3).at(0, 0, 1.5).tag("plate");
    const stack = cylinder(12, 20).at(0, 0, 13.5).reference();
    return { plate, stack, checks: [{ clear: ["plate", "stack"], atLeast: 0.2 }] };

### Shape.printedUp

`printedUp(up: PrintAxis | [number, number, number]): Shape`

Which way a body drawn in its assembled position prints, when printed on
its own: the axis that points up on the printer. Call it last, on the
shape the returned object names, like `.reference()`.

- `"+z"` is as drawn, the default; `"-z"` prints it upside down; `"x"`,
  `"-x"`, `"y"`, `"-y"` stand it on a side; `[x, y, z]` is any direction.
- `print_check` measures overhang and bed contact this way up; the
  export is as drawn.
- Bodies printed together — a print-in-place hinge, a drawer in its
  frame — share one orientation and take none of this: draw them as they
  print and let a `turns` or `slides` check move them (TurnCheck).
- A reference body has none.

@example
    // a pin drawn lying in its bracket, printed standing on its end
    const bracket = box(30, 12, 12).at(0, 0, 6).cut(cylinder(2.1, 14).rotate("y", 90).at(0, 0, 8));
    const pin = cylinder(2, 36).rotate("y", 90).at(0, 0, 8).printedUp("x");
    return { bracket, pin };

### Shape.made

`made(process: Process): Shape`

Say this body is made another way than printing: cut from a
sheet. The print check then holds it to that process instead of
to a printer. Call it last, on the shape the returned object names, like
`.reference()`; it changes the shape in place.

- A body is one of: printed (the default), made, bought
  (Shape.bought), or a reference.
- A sheet body is one piece of the sheet; give each piece its own body.

@example
    const ply = sheet(18, { material: "birch plywood" });
    const shelf = box(600, 250, 18).at(0, 0, 9).made(ply);
    const bracket = box(40, 30, 4).at(0, -100, -2);
    return { shelf, bracket };

### Shape.bought

`bought(name: string): Shape`

Say this body is bought, not made — a bearing, a magnet, an LED strip —
so its shape stands in for the real part and the print check leaves it
out. Call it last, on the shape the returned object names; it changes
the shape in place.

- `name` is what to buy, in your own words; the reply echoes it.
- It is measured and in `between_bodies` like any body, so a check can
  hold its fit: `{ clear: ["magnet", "holder"], atLeast: 0.1 }`.

@example
    const holder = cylinder(8, 6).at(0, 0, 3).cut(cylinder(5.1, 3.2).at(0, 0, 4.5));
    const magnet = cylinder(5, 3).at(0, 0, 4.5).bought("10 x 3 mm neodymium magnet");
    return { holder, magnet };

### Shape.material

`material(material: Material): Shape`

How this body looks in the window. Visual only: nothing measured reads it,
and an agent's render shows it only with `materials: true`. Changes this
shape in place and returns it.

- One material per solid: a body wears the outermost one in it, a union
  the first operand's, a cutter's never. Two colours are two bodies.

@example return { base: box(40, 40, 10).material({ color: "#8a9099", metalness: 0.9 }), lid: box(40, 40, 2).at(0, 0, 7) }

### Shape.translate

`translate(x: number, y: number, z = 0): Shape`

Move by `x`, `y`, `z` millimetres from where the shape currently sits.

### Shape.at

`at(x: number, y: number, z = 0): Shape`

Alias for translate that reads better when placing a feature.

### Shape.rotate

`rotate(axis: Vec3 | "x" | "y" | "z", degrees: number): Shape`

Rotate about an axis through the origin, in degrees. Positive turns
anticlockwise seen from the axis's + end: `.rotate("z", 90)` takes +X to
+Y, and `.rotate("x", 90)` takes +Z to -Y, so a Z cylinder lies along Y.

### Shape.mirror

`mirror(axis: Vec3 | "x" | "y" | "z"): Shape`

Reflect in the plane through the origin whose normal is `axis`:
`.mirror("x")` flips X, across the YZ plane.

- A symmetric part is `union(half, half.mirror("x"))`.
- Mirror only a half. A body spanning the plane refills a hole cut on one
  side from its own uncut reflection; mirror the features, or cut after
  the union.

### Shape.scale

`scale(x: number, y = x, z = x): Shape`

Resize about the origin: one factor uniformly, three per axis.
`sphere(10).scale(2, 1, 0.5)` is an ellipsoid with semi-axes 20, 10, 5.

- Factors must be positive; a reflection is `mirror`.
- Stretching turns circles into ellipses, which `curve: "circle"` no
  longer finds. Fillet after stretching.

### Shape.offset

`offset(distance: number): Shape`

Move the surface outward by `distance` (or inward, if negative).

Growing also rounds every convex edge by `distance`, which is the cheapest
way to break sharp corners on a printed part.

### Shape.shell

`shell(thickness: number, options: { corners?: "round" | "sharp" } = {}): Shape`

Hollow this out, leaving a wall of `thickness` inside the current surface.

- `corners` is what the cavity does at an inside corner of the part:
  `"round"` (default) rounds it by `thickness`, so the wall is
  `thickness` everywhere; `"sharp"` extends the walls until they meet,
  so the wall thickens into the corner, as Fusion's Shell does.
- A step or lobe narrower than the wall leaves a round no room; it
  shells only with `corners: "sharp"`.
- A turned part hollows exactly with `revolve`'s `wall`; a lofted one
  with `loft`'s.

@example extrude([[0, 0], [20, 0], [20, 5], [5, 5], [5, 20], [0, 20]], 10).shell(1, { corners: "sharp" })

### Shape.thicken

`thicken(thickness: number, options: { side?: "both" | "out" | "in" } = {}): Shape`

This surface made a solid `thickness` mm thick, measured square to the
surface: Fusion's Thicken. The way to print, fillet or cut a surface.

- `side`: `"both"` (default) centres the wall on the surface, `"out"`
  grows it on the normal side, `"in"` on the other.
- The normal is to the right of the curve's direction of travel (seen
  from +Z for an extrude or a loft): outward for an anticlockwise curve.
- Reports the measured thickness as `thickened_mm: { min, max }`; more
  than 1 % off anywhere is refused.
- Refused where the surface bends tighter than `thickness` on the growing
  side (the radius is in the message), and at a crease between faces.

@example
    // a radius-40 dome, 2 mm thick inward: 2/3 · π · (40³ − 38³) = 19,117 mm³
    return surfaceRevolve([[40, 0], { through: [40 * Math.SQRT1_2, 40 * Math.SQRT1_2] }, [0, 40]]).thicken(2, { side: "in" });

### Shape.trim

`trim(tool: Shape | { plane: { point: PathPoint; normal: PathPoint } }, options: { keep: TrimKeep }): Shape`

This surface cut by a tool, keeping the pieces on one side: Fusion's
Trim. `keep` depends on the tool (see `TrimKeep`).

- A solid tool: `keep: "inside"` or `"outside"`.
- A surface tool: `"front"` (its normal side) or `"back"`. It must reach
  right across this surface.
- A plane, `{ plane: { point: [x, y, z], normal: [x, y, z] } }`:
  `"above"` (where the normal points) or `"below"`.
- To trim by a curve, trim by `surfaceExtrude(curve, h)` or an extruded
  solid.
- A tool that misses, or keeps all or none of the surface, is refused.
  To keep every piece, use `.split(tool)`.

@example
    // a 40 × 40 sheet on y = 0, less the strip a radius-10 rod passes: 800 mm²
    return surfaceExtrude([[-20, 0], [20, 0]], 40).trim(cylinder(10, 100), { keep: "outside" });
@example surfaceExtrude([[-20, 0], [20, 0]], 40).trim({ plane: { point: [0, 0, 10], normal: [0, 0, 1] } }, { keep: "above" })  // 40 × 10

### Shape.split

`split(tool: Shape | { plane: { point: PathPoint; normal: PathPoint } }): Shape`

This surface cut along a tool, keeping every piece: Fusion's Split Face.

- The tool is a solid, a surface or a plane, as `trim` takes.
- The pieces stay joined along the cut, as separate faces that a
  selector or a later trim can tell apart.

@example
    // one 40 × 40 sheet as two 800 mm² faces, split at x = 0
    return surfaceExtrude([[-20, 0], [20, 0]], 40).split({ plane: { point: [0, 0, 0], normal: [1, 0, 0] } });

### Shape.offsetSurface

`offsetSurface(distance: number): Shape`

A new surface `distance` mm from this one along its normal: Fusion's
Offset Surface.

- Negative `distance` moves against the normal (see `thicken` for which
  way the normal points).
- Reports the measured distance as `offset_mm: { min, max }`.
- Refused where the surface bends tighter than `distance` on the side it
  moves toward.

@example
    // a radius-40 dome moved 5 mm out: 2π · 45² = 12,723 mm²
    return surfaceRevolve([[40, 0], { through: [40 * Math.SQRT1_2, 40 * Math.SQRT1_2] }, [0, 40]]).offsetSurface(5);

### Shape.edges

`edges(selector: EdgeSelector): EdgeSelection`

The edges a selector matches, for a fillet, chamfer or patch to act on.

- The selector is required: `.edges()` alone is refused, not read as
  every edge. `{ dihedral: "convex" }` is every outside edge.
- A string measures against the whole part: `>Z` is the edges furthest
  in +Z, `|Z` the straight edges parallel to Z, joined by `and`.
- An `EdgeQuery` object narrows by feature (`on`, `between`), corner
  angle, curve kind or length.
- `.expect({ count })` turns a change in how many edges match into a
  build error.

@example
    // a 20 mm cube with all 12 edges rounded 2 mm: 16³ + 6·16²·2 + 3π·2²·16 + (4/3)π·2³ = 7,805 mm³
    return box(20, 20, 20).edges({ dihedral: "convex" }).expect({ count: 12 }).fillet(2);

### Shape.vertices

`vertices(selector: VertexSelector): VertexSelection`

The corners a selector matches, for a treatment of every edge meeting
there.

- The selector is required: `.vertices()` alone is refused, not read as
  every corner. To round every outside edge, use
  `.edges({ dihedral: "convex" })`.
- `>X and >Y and >Z` is the corner furthest in +X, +Y and +Z. Vertex
  strings take only `>` and `<` terms.

@example
    // one corner of a 20 mm cube rounded 2 mm: 8000 − 3·18·(4 − π) − (8 − (4/3)π) = 7,950 mm³
    return box(20, 20, 20).vertices(">X and >Y and >Z").fillet(2);

### Shape.fillet

`fillet(radius: number, selector: EdgeSelector, expectation?: EdgeExpectation, options?: FilletOptions, source?: SourceLocation): Shape`

Round the edges matched by an authored selector. Either order: `.fillet(">Z", 2)` is `.fillet(2, ">Z")`.

### Shape.fillet

`fillet(selector: EdgeSelector, radius: number, expectation?: EdgeExpectation, options?: FilletOptions, source?: SourceLocation): Shape`

### Shape.chamfer

`chamfer(distance: number, selector: EdgeSelector, expectation?: EdgeExpectation, options?: ChamferOptions, source?: SourceLocation): Shape`

Bevel the edges matched by an authored selector. Either order: `.chamfer(">Z", 1)` is `.chamfer(1, ">Z")`.

### Shape.chamfer

`chamfer(selector: EdgeSelector, distance: number, expectation?: EdgeExpectation, options?: ChamferOptions, source?: SourceLocation): Shape`

### Shape.expect

`expect(expectation: EdgeExpectation): Shape`

Check the fillet or chamfer just applied, as `.edges(sel).expect({ count })`
before it would: a build where its selector matches a different number
of edges fails aloud.

- Only straight after `.fillet`, `.chamfer`, `.smooth` or `.squircle`;
  anywhere else it is refused.
- `count` is `edges` on that treatment in an evaluate_part reply.
- One expectation per treatment: not here and on `.edges(...)` too.

@example
    // a 40 × 20 × 10 block with its 4 upright edges rounded 2 mm: 8000 − 4·10·(2² − π·2²/4) = 7,966 mm³
    return box(40, 20, 10).fillet(2, "|Z").expect({ count: 4 });

### Shape.union

`union(...rest: (Shape | BoolOptions)[]): Shape`

Fuse this shape with the others; `{ blend: r }` rounds where they meet.

### Shape.cut

`cut(...rest: (Shape | BoolOptions)[]): Shape`

Subtract each of `tools` from this shape, as one cut: the order they are
listed in does not matter. A cutter must pass through, not end on, the
faces it opens: make it at least 0.5 mm longer at each open end, or the
part keeps a sliver or fails to close.

### Shape.intersect

`intersect(...rest: (Shape | BoolOptions)[]): Shape`

Keep only what this shape and the others all occupy.

## EdgeSelection methods

What `.edges(...)` returns.

### EdgeSelection.expect

`expect(expectation: EdgeExpectation): EdgeSelection`

Require this selector to resolve to exactly `count` edges.

This turns a topology edit that changes the target set into a clear build
error instead of silently filleting a different number of edges.

### EdgeSelection.fillet

`fillet(radius: number, options?: FilletOptions): Shape`

Round the selected logical edges by `radius` millimetres.

- A fillet whose stripe runs over a hole cut before it is built before
  that cut, and the cut made again through it: the hole stays through.
  The reply's treatment lists the cuts as `before_cuts`.
- The default recipe is tangent, rolling-ball. Other valid recipes are
  kept in the intent graph but the exact backend refuses them until it
  can produce that geometry exactly.

### EdgeSelection.chamfer

`chamfer(distance: number, options?: ChamferOptions): Shape`

Bevel the selected edges by an equal distance in millimetres.

- A chamfer that runs over a hole cut before it is built before that cut,
  and the cut made again through it, as a fillet is.

### EdgeSelection.smooth

`smooth(radius: number, options?: Omit<FilletOptions, "continuity">): Shape`

Round the selected edges with a blend that is curvature-continuous (G2)
to the faces it joins: no crease and no change in curvature where it
meets them, where a `fillet()` leaves a kink in the reflection.

- It meets the faces along the same lines as `fillet(radius)`, at the
  same depth, so the two are interchangeable.
- Every edge must run face to face: three or more selected edges meeting
  at a corner are refused, naming the corner. `fillet()` those.
- The reply's `curvature_jump_per_mm` is the step in curvature measured
  where the blend meets the part: 0 here, 1/radius for a fillet.

@example return box(40, 30, 10).edges("|Z").expect({ count: 4 }).smooth(4);

### EdgeSelection.squircle

`squircle(radius: number, options?: Omit<FilletOptions, "continuity">): Shape`

The same blend as smooth, under the name a "squircle" corner is
asked for; a true squircle is a 2D superellipse, this is a 3D blend.

@example return box(40, 30, 10).edges("|Z").squircle(4);

### EdgeSelection.patch

`patch(options: { tangent?: boolean } = {}): Shape`

A new surface filling the closed loop of free edges selected: Fusion's
Patch.

- Select with `{ role: "boundary" }`, narrowed by `at` or `on`. Every
  selected edge must be a free edge, and together they must close.
- Returns the patch alone. `stitchSurfaces(surface, patch)` joins it.
- `tangent: true` makes the fill meet its neighbours smoothly.
- A fill more than 0.01 mm off its edges, or 1° off tangent, is refused.

@example
    // a radius-20 tube 30 tall, patched at both ends: a 37,699 mm³ cylinder
    const tube = surfaceExtrude([[20, 0], { through: [0, 20] }, [-20, 0], { through: [0, -20] }], 30, { closed: true });
    const lid = tube.edges({ role: "boundary", at: { z: "max" } }).patch();
    const floor = tube.edges({ role: "boundary", at: { z: "min" } }).patch();
    return stitchSurfaces(tube, lid, floor);

## VertexSelection methods

What `.vertices(...)` returns.

### VertexSelection.expect

`expect(expectation: EdgeExpectation): VertexSelection`

Require this selector to resolve to exactly `count` corner vertices.

### VertexSelection.fillet

`fillet(radius: number, options?: FilletOptions): Shape`

Round every incident edge at the selected corner vertices.

### VertexSelection.chamfer

`chamfer(distance: number, options?: ChamferOptions): Shape`

Bevel every incident edge at the selected corner vertices.

## Line2d methods

What `line2d(...)` returns.

### Line2d.direction

`direction(): [number, number]`

Unit direction from `from` toward `to`.

### Line2d.normal

`normal(): [number, number]`

Unit normal, the direction turned a quarter turn anticlockwise: left of travel.

### Line2d.pointAt

`pointAt(distance: number): [number, number]`

The point `distance` mm along the line from `from`; negative goes back.

### Line2d.length

`length(): number`

Length of the segment `from` to `to`.

### Line2d.offset

`offset(distance: number): Line2d`

The same line moved sideways by `distance`, to the left of its travel;
negative moves it right. A wall's inner face from its outer one, the
edge of a band from its centreline.

### Line2d.meet

`meet(other: Line2d): [number, number]`

Where this line crosses `other`, extended as far as needed in both
directions. Throws when they are parallel, which is the answer in that
case rather than a point far away.

### Line2d.yAt

`yAt(x: number): number`

The line's y at a given x, for a line that is not vertical.

### Line2d.xAt

`xAt(y: number): number`

The line's x at a given y, for a line that is not horizontal.

## Values

### METRIC_FASTENERS

`const METRIC_FASTENERS: Record< string, { pitch: number; tap: number; close: number; normal: number; free: number; head: number; csink: number } >`

ISO metric coarse fasteners by designation, M2 to M20 (`M2_5` is M2.5), in mm:
`pitch` (ISO 261 coarse), `tap` drill, `close`/`normal`/`free` clearance
(ISO 273), `head` of a socket head cap screw (ISO 4762), `csink` of a 90°
countersunk screw (ISO 10642). A size not listed is refused, never guessed.

## Types

### Vec3

`type Vec3 = { x: number; y: number; z: number }`

### BoolOptions

```ts
export interface BoolOptions {
  /**
   * Radius of the rounding applied where the shapes meet. Zero is a hard corner.
   *
   * Worth knowing: a blend pushes the surface outward by up to `blend / 4` near
   * the seam, so blending two shapes with coincident faces makes the part very
   * slightly larger than nominal on those faces.
   */
  blend?: number;
}
```

Options shared by the boolean operations.

### AxisDirection

`type AxisDirection = "+x" | "-x" | "+y" | "-y" | "+z" | "-z"`

The outward normal of a face, as `adjacentTo` spells it.

### EdgeQuery

```ts
export interface EdgeQuery {
  /** Match edges created by this named Boolean operation. */
  generatedBy?: string;
  /**
   * `"line"`, `"circle"` (any circular arc) or `"spline"` (anything else:
   * section curves, and the ellipses and intersection curves booleans leave).
   */
  curve?: "line" | "circle" | "spline";
  /**
   * `"hole"`: a circular hole rim, not the rim of an outside boss.
   * `"boundary"`: a free edge, where a surface ends (one face on it); what
   * `.patch()` fills and what a surface report measures as open. A closed
   * solid has none.
   */
  role?: "hole" | "boundary";
  /** Match an edge touching a face with this outward normal. */
  adjacentTo?: { faceNormal: AxisDirection };
  /** Match edge centres at the requested document extrema. */
  at?: Partial<Record<"x" | "y" | "z", "min" | "max">>;
  /**
   * `convex` (an outside corner), `concave` (inside) or `smooth` (no corner:
   * a fillet's boundary). Fillets and chamfers skip smooth edges unless asked
   * for them.
   */
  dihedral?: "convex" | "concave" | "smooth";
  /** A straight edge parallel to this axis: the object form of `|Z`. */
  parallel?: "x" | "y" | "z";
  /** Only edges at least this long, in mm: what keeps a sliver out of a cosmetic pass. */
  longerThan?: number;
  /**
   * Only edges of these tagged features. `{ on: "lip", at: { z: "max" } }` is
   * the lip's own top rim: `at` is then measured among the lip's edges. Tags
   * survive booleans, fillets, chamfers and moves, not offset, shell or
   * intersect.
   */
  on?: string | string[];
  /** Only edges with one face from each of two features: the seam where one meets the other. */
  between?: [string, string];
  /**
   * Every edge the rest of this query matches, except those this sub-query
   * matches. `{ dihedral: "convex", not: { parallel: "z" } }` is every
   * outside edge but the upright ones.
   *
   * - Taken away before `at` picks extrema, so an extremum is the highest of
   *   what is left rather than the highest edge if it survives.
   * - One level: a `not` inside a `not` is refused, and so is an empty one.
   * - Prefer a positive term where there is one — `{ dihedral: "convex" }`
   *   says more than `{ not: { dihedral: "concave" } }`.
   */
  not?: EdgeQuery;
}
```

A topology-aware alternative to the compact directional selector string.
Any other key is refused, naming the one it most likely meant.

### EdgeSelector

`type EdgeSelector = string | EdgeQuery`

A string query or an `EdgeQuery`, resolved anew on every build. `>Z` is
furthest in +Z, `<Y` furthest in -Y, `|X` parallel to X, joined by `and`:
`>Z and >Y and |X` is the top edge at +Y running along X.

### VertexQuery

```ts
export interface VertexQuery {
  /** Match vertices at the requested document extrema. */
  at?: Partial<Record<"x" | "y" | "z", "min" | "max">>;
}
```

A positional query over B-rep vertices for a corner treatment.

### VertexSelector

`type VertexSelector = string | VertexQuery`

A compact vertex selector such as `>X and >Y and >Z`, or a vertex query.

### EdgeExpectation

```ts
export interface EdgeExpectation {
  /** The exact number of selected edges or vertices the selector must match. */
  count?: number;
  /** The fewest the selector may match. */
  atLeast?: number;
  /** The most the selector may match. */
  atMost?: number;
}
```

What a selector must resolve to, checked on the shape the treatment runs
against, so a selector that drifts fails aloud instead of treating other
edges. At least one field.

- `count` is the number `edges` in an evaluate_part reply's `treatments`
  reports for that treatment: paste it from a reply, never count by hand.
- `atLeast` and `atMost` are for an expectation written before the count is
  known: `{ atLeast: 1 }` says the selector must find something.

@example box(40, 20, 10).edges("|Z").expect({ count: 4 }).fillet(2)

### Material

```ts
export interface Material {
  /** `#rrggbb` or `#rgb`. */
  color: string;
  /** 0 mirror-smooth to 1 fully diffuse. Defaults to 0.5. */
  roughness?: number;
  /** 0 plastic or paint to 1 bare metal. Defaults to 0. */
  metalness?: number;
  /** 1 solid, towards 0 see-through; above 0. Defaults to 1. Window only. */
  opacity?: number;
  /** A colour the surface glows with, `#rrggbb` or `#rgb`. Defaults to none. Window only. */
  emissive?: string;
  /** A clear glossy layer over the surface, 0 to 1. Defaults to 0. Window only. */
  clearcoat?: number;
}
```

A surface appearance for `Shape.material`, in glTF's terms. Glossy is low
`roughness`.

@example box(20, 10, 2).material({ color: "#c9ccd1", metalness: 1, roughness: 0.35 })  // aluminium

### FilletOptions

```ts
export interface FilletOptions {
  /** Tangent (G1) is `fillet()`; curvature (G2) is `smooth()`, which sets it. */
  continuity?: "tangent" | "curvature";
  /** Rolling-ball is available now; setback is reserved for the exact backend. */
  corner?: "rollingBall" | "setback";
}
```

How a constant-radius edge fillet meets its neighbouring faces.

### ChamferOptions

```ts
export interface ChamferOptions {
  /** Planar chamfer is available now; miter and blend are reserved for the exact backend. */
  corner?: "chamfer" | "miter" | "blend";
}
```

How equal-distance chamfers meet where selected edges share a corner.

### SheetOptions

```ts
export interface SheetOptions {
  /** The stock, in your own words: "poplar plywood B/BB". Echoed, never judged. */
  material?: string;
}
```

What sheet takes besides the thickness.

### Process

```ts
export interface Process {
  kind: "sheet";
  /** mm. */
  thickness: number;
  material?: string;
}
```

How a body is made when it is not printed; sheet makes one.

### Brief

```ts
export interface Brief {
  /** The box the part must fit inside, mm, in any of the six axis orientations. */
  envelope?: [number, number, number];
  /** The plastic it may use, cm³. */
  budgetCm3?: number;
  /** What it holds, in your own words. Prose: the measurement is a `Check`. */
  holds?: string[];
  /** How it is handled — "one hand, thumb only". Prose. */
  gesture?: string;
  /** The machine it is for, a bed name from `BEDS`. */
  printer?: string;
  /** The filament it is for. Prose. */
  material?: string;
}
```

What brief declares. Every key is optional; an empty brief is refused.

### ImportedPart

`type ImportedPart = Shape & { [body: string]: any }`

What a part returns, as `importPart` hands it back: a shape, or a multi-body part's object of bodies.

### EdgeSelection

`class EdgeSelection`

A selected edge set, ready for an edge-specific operation.

### VertexSelection

`class VertexSelection`

A selected corner-vertex set, expanded to incident edges by the exact backend.

### Shape

`class Shape`

A solid, or a step on the way to one. Every method returns a new shape, so a
shape can be placed twice or kept as a tool, except `tag` and `material`,
which change this one.

### SectionPoint

`type SectionPoint = [number, number]`

A point in a revolved section: `[radius, z]`, with radius measured off +Z.

### SectionEntry

```ts
export type SectionEntry =
  /** A corner, `[x, y]` (`[radius, z]` in a revolve). */
  | [number, number]
  /**
   * A corner rounded by a tangent arc of radius `round`, between two straight
   * edges. `at` is the sharp corner where the edges would meet, not where the
   * arc starts; the round trims both edges. A 40 × 20 plate with 5 mm corners
   * is exactly four entries, `{ at: [±20, ±10], round: 5 }`, anticlockwise,
   * with no plain corners.
   */
  | { at: [number, number]; round: number }
  /** A circular arc through this point, which lies on it. A full circle is two arcs between two corners. */
  | { through: [number, number] }
  /** The shorter arc of this radius, at least half the chord. Positive bulges out of the section, negative bends in. */
  | { radius: number }
  /**
   * A smooth curve through the points; `start` and `end` are its end
   * directions. `[{ spline: points }]` alone is one closed smooth curve.
   */
  | { spline: [number, number][]; start?: [number, number]; end?: [number, number] }
  /** Bézier control points: one is quadratic, two cubic. The curve does not pass through them. */
  | { bezier: [number, number][] }
  /**
   * A clamped B-spline with the corners as its end poles: how a STEP export's
   * poles copy in. `knots`, if given, is the full vector.
   */
  | { bspline: [number, number][]; degree?: number; knots?: number[] }
  /**
   * A smooth curve within `tolerance` mm (0.01 to 0.1) of every sampled
   * point: the entry for points that are data. The measured worst distance
   * is reported as `deviation_mm`. Alone, it is one closed smooth loop.
   */
  | { fit: [number, number][]; tolerance: number }
  /** A curve given by a formula, with its own ends: see `CurveEntry`. */
  | CurveEntry
  /** An outline stepped inward; write it as `inset(outline, by)`. */
  | { inset: SectionEntry[]; by: number };
```

One entry of a section, the outline `extrude`, `revolve`, `loft` and `sweep`
take: a list, anticlockwise, closing itself (never repeat the first corner).

- One boundary that does not cross itself; a hole is a second shape cut out.
- Two corners in a row are a straight edge; between two corners at most one
  entry draws the stretch (after the last, the stretch back to the first).
- Arcs are exact circles and curves exact B-splines: never approximate a
  curve with short lines.

@example
    // 60 × 30 plate, 4 mm corners: four rounded corners and nothing else
    const plate = [{ at: [-30, -15], round: 4 }, { at: [30, -15], round: 4 }, { at: [30, 15], round: 4 }, { at: [-30, 15], round: 4 }];
    // slot 24 long, 8 wide: straight sides end at x = ±8, each half circle
    // passes through its tip at x = ±12 (the chord's midpoint moved out by the radius)
    const slot = [[-8, -4], [8, -4], { through: [12, 0] }, [8, 4], [-8, 4], { through: [-12, 0] }];
    return extrude(plate, 3).cut(extrude(slot, 5));

### CurveEntry

```ts
export type CurveEntry = {
  curve: (t: number) => [number, number];
  /** Where `t` starts; `from > to` draws the curve backwards. */
  from: number;
  to: number;
  /** The most the drawn curve may be from the function anywhere, in mm. */
  tolerance: number;
  /** The exact derivative of `curve`, `(t) => [dx, dy]`; checked against `curve`, and refused when it disagrees. */
  derivative?: (t: number) => [number, number];
  /** `(a, b) => m`: at least the length of the fourth derivative anywhere in `[a, b]`; checked against the error the script sees. */
  fourth?: (a: number, b: number) => number;
};
```

A section entry drawn from a formula, `curve(t)` for `t` from `from` to
`to`: an involute, a cam law, a spiral.

- Its ends are `curve(from)` and `curve(to)`; it needs no corners round
  it, and a corner beside it is joined to that end by a straight edge.
- The drawn curve is within `tolerance` mm (0.001 is usual) of the function
  everywhere. The part reports that bound as `curve_bound_mm`;
  `curve_bound` is `certified` when `derivative` and `fourth` are given,
  else `estimated`. `deviation_mm` is a check at points, not a bound.
- `curve` must be smooth on the range: a cusp is two entries meeting at a
  corner.

@example
    // a quarter circle of radius 10, certified: its fourth derivative has length 10
    const arc = { curve: (t) => [10 * Math.cos(t), 10 * Math.sin(t)], derivative: (t) => [-10 * Math.sin(t), 10 * Math.cos(t)], fourth: (a, b) => 10, from: 0, to: Math.PI / 2, tolerance: 0.001 };
    return extrude([[0, 0], arc], 5);

### Fit

`type Fit = "close" | "normal" | "free"`

How loosely a clearance hole is drilled, per ISO 273.

### ThreadSize

`type ThreadSize = string | { diameter: number; pitch: number }`

A thread named from `METRIC_FASTENERS` ("M8", coarse pitch) or given outright.

### ThreadOptions

```ts
export interface ThreadOptions {
  /** `"right"` (the default, and nearly every screw) or `"left"`. */
  hand?: "right" | "left";
  /**
   * Radial allowance in mm, default 0 (the ISO basic profile). A rod shrinks
   * and a hole grows by it. A printed pair gives it to both: `c` on each sits
   * `c` apart across the flanks and `2c` at crest and root.
   */
  clearance?: number;
  /** A fine pitch in place of the coarse one a named size carries: `{ pitch: 1 }` on "M8". */
  pitch?: number;
}
```

Options shared by `threadedRod` and `threadedHole`.

### Line2d

`class Line2d`

A straight line in the XY plane through two points, with a direction.

### OutlinePoint

`type OutlinePoint = [number, number]`

A point in an extruded outline: `[x, y]`, in the plane the shape is drawn on.

### PathPoint

`type PathPoint = [number, number, number]`

A point on a routed path: `[x, y, z]`.

### HelixPath

```ts
export interface HelixPath {
  helix: {
    radius: number;
    pitch: number;
    turns?: number;
    height?: number;
    endRadius?: number;
    hand?: "right" | "left";
  };
}
```

A helical path for `pipe` and `sweep`: a spring, a coil, a spiral horn.
Axis +Z, centred on the origin: it starts at `[radius, 0, -height / 2]`.

- Give `turns` or `height` (= `pitch * turns`), not both.
- `endRadius` varies the radius linearly (a conical helix); `hand: "left"`
  reverses the default right-handed winding.
- A screw thread is `threadedRod` or `threadedHole`, not a sweep.

@example pipe({ helix: { radius: 10, pitch: 4, turns: 5 } }, 1.5)  // a spring

### SplinePath

```ts
export interface SplinePath {
  spline: PathPoint[];
}
```

A smooth path for `pipe` and `sweep` through at least three points, in
order: a hose, a cable, a handle. The section starts perpendicular to it at
the first point. A bend tighter than the section is refused, naming where:
spread the points there.

### SweepOptions

```ts
export interface SweepOptions {
  /** Centreline bend radius at every corner of a path of points. */
  bend?: number;
  /**
   * The section's size at the end relative to the start, above 0: `0.2` ends
   * at a fifth. For a point, end on something small like `0.05`.
   */
  taper?: number;
}
```

Options shared by `pipe` and `sweep`.

### LoftSection

```ts
export interface LoftSection {
  /** Height of the plane this section lies in. */
  z: number;
  /** Corners and curve entries, anticlockwise, first corner not repeated. */
  outline?: SectionEntry[];
  /** An apex in place of an outline, first or last section only. */
  point?: [number, number];
}
```

One loft section, lying flat at height `z`: an `outline` (a list of
`SectionEntry` — corners, arcs, curves), or, for the first or last section
only, a single `point: [x, y]` the wall closes onto — the tip of a vase or
a spire.

### WallSpec

`type WallSpec = number | { thickness: number; bottom?: "open" | "closed"; top?: "open" | "closed" }`

A wall for `loft` and `revolve`: a thickness, or one with each end open (the default) or closed.

### TrimKeep

```ts
export type TrimKeep =
  /** Inside a solid tool. */
  | "inside"
  /** Outside a solid tool. */
  | "outside"
  /** The side of a plane its normal points to. */
  | "above"
  /** The side of a plane its normal points away from. */
  | "below"
  /** A surface tool's normal side. */
  | "front"
  /** The other side of a surface tool. */
  | "back";
```

Which pieces a `trim` keeps; the tool decides which two words apply.

### CurveOptions

```ts
export interface CurveOptions {
  /** `true`: the curve closes back to its start. Default `false`. */
  closed?: boolean;
}
```

Options every surface-making function takes. The curve is a list of
`SectionEntry`, open unless `closed: true`.

- An open curve runs from its first entry to its last, and both are
  corners `[x, y]`: `[[0, 0], { through: [10, 5] }, [20, 0]]`.
- Or it is one `{ fit }` or `{ spline }` alone, which then runs open
  through its points from the first to the last.
- A closed curve is a section, as `extrude` takes, and makes a tube open at
  both ends. A full circle is two arcs between two corners.
- `inset` is refused in an open curve.

### LoftCurve

```ts
export interface LoftCurve {
  /** Height of the plane the curve lies in. */
  z: number;
  /** The curve, open unless the loft is `closed` (see `CurveOptions`). */
  curve: SectionEntry[];
}
```

One curve of a `surfaceLoft`, lying flat at height `z`.

### PolarOptions

```ts
export interface PolarOptions {
  /** Angle of the first point, in degrees anticlockwise from +X. Default 0. */
  start?: number;
  /**
   * Turn the circle half a step so no point sits on a centreline, as flange
   * standards (ASME B16.5) place bolt holes.
   */
  straddle?: boolean;
}
```

How a bolt circle is placed relative to the centrelines.

### ReactionDiffusionOptions

```ts
export interface ReactionDiffusionOptions {
  /**
   * `"gierer-meinhardt"`: `a` is the activator and `b` the inhibitor,
   * `a' = Da∇²a + rho·a²/(b·(1 + kappa·a²)) − decay[0]·a + source[0]` and
   * `b' = Db∇²b + rho·a² − decay[1]·b + source[1]`.
   * `"gray-scott"`: `a` is the substrate and `b` the reactant,
   * `a' = Da∇²a − a·b² + feed·(1 − a)` and `b' = Db∇²b + a·b² − (feed + kill)·b`.
   */
  model: "gierer-meinhardt" | "gray-scott";
  /** `n` cells around a ring, or `[width, height]` cells on a grid that wraps both ways, stored row by row. */
  size: number | [number, number];
  /** Starting values, one per cell. Seed the noise with the script's own seeded generator. */
  a: number[];
  b: number[];
  /** `[Da, Db]`, in cells² per unit time. */
  diffusion: [number, number];
  /** The explicit time step; refused when it is too long for the diffusion to stay stable. */
  dt: number;
  steps: number;
  /** Gierer–Meinhardt only: production, default 1. */
  rho?: number;
  /** Gierer–Meinhardt only: activator saturation, default 0; larger widens the peaks. */
  kappa?: number;
  /** Gierer–Meinhardt only: `[decay of a, decay of b]`, default `[1, 1]`. */
  decay?: [number, number];
  /** Gierer–Meinhardt only: `[basal production of a, of b]`, default `[0, 0]`. */
  source?: [number, number];
  /** Gray–Scott only: the feed rate. */
  feed?: number;
  /** Gray–Scott only: the kill rate. */
  kill?: number;
}
```

A two-species reaction-diffusion run: see simulateReactionDiffusion.

### PrintAxis

`type PrintAxis = "+x" | "x" | "-x" | "+y" | "y" | "-y" | "+z" | "z" | "-z"`

An axis that points up on the printer; see Shape.printedUp.

### Part

`type Part = Shape | Record<string, Shape | Check[] | Mechanism>`

What a script may return: one shape, or an object naming each body of a
part that stays in several — `return { base, lid }` — where two keys are
not bodies: `checks`, a list of Checks, and `mechanism`, a
Mechanism.

### Check

```ts
export interface Check {
  /** The two never touch, by `atLeast` mm. */
  clear?: [string, string];
  /** The two overlap: a catch that has to catch. */
  interferes?: [string, string];
  /** The two are flush: neither gap nor overlap. */
  touching?: [string, string];
  turns?: TurnCheck;
  slides?: SlideCheck;
  /** For a motion: bodies to stay clear of; default all others. */
  clearOf?: string | string[];
  /** For a motion: the body to bite into. */
  catches?: string;
  /** Over the {@link Mechanism}, these bodies or "all" stay `atLeast` mm clear. */
  moves?: "all" | string[];
  /** Over the {@link Mechanism}, the two stay `within` mm of contact. */
  pushes?: [string, string];
  /** For `pushes`, mm of gap or overlap. */
  within?: number;
  /** Nothing is thinner than `min` mm. */
  wall?: { min: number };
  /** The part is `is` [x, y, z] mm, or fits in `max`. */
  size?: { is?: number[]; max?: number[] };
  /** Round holes of Ø`diameter` along `axis` (default z): `count`, `through`, one per `at` [x, y]. */
  holes?: { diameter: number; count?: number; through?: boolean; at?: number[][]; axis?: "x" | "y" | "z" };
  /** The faces tagged `tag` span `size` centred at `center`; tag the cutter. */
  feature?: { tag: string; size?: number[]; center?: number[] };
  /** This fraction of the footprint reaches the bed, 0 to 1. */
  standsOn?: { atLeast: number };
  /** Exactly this many free-standing pieces. */
  bodies?: number;
  /** The mesh closes; `true` only. */
  watertight?: true;
  /** For `clear` the least gap, mm; for `interferes` the least shared mm³. */
  atLeast?: number;
  /** For `interferes`, `catches`: how deep the two must overlap, mm. */
  deeperThan?: number;
  /** For `touching`: the least shared surface, mm²; a graze is 0. */
  contactAtLeast?: number;
  /** For `wall`: tags left out, or `"feather"`/`"edge"` for that reading. */
  ignore?: string[];
  /** For `wall`: only material on these tags counts. */
  on?: string[];
  /** Free text, echoed when the check fails. */
  why?: string;
}
```

One rule the built part must hold, written as `checks: [...]` beside the
bodies. Judged on every build and reported first: a verdict, then each
failure with what differs, what was measured and where.

- Before modelling, write the request's numbers as checks — size, holes,
  where features sit — so the verdict says the part is what was asked.
- One head key per check: `size`, `holes`, `feature`, `clear`,
  `interferes`, `touching`, `turns`, `slides`, `moves`, `pushes`, `wall`,
  `standsOn`, `bodies`, `watertight`; the rest qualify it. Held to 0.01 mm.
- evaluate_part reports a failure; export_part and save_project refuse
  without `allow_failing: "<reason>"`.

@example
    // "80 × 50 × 6, four Ø6 holes 10 mm in, a 10 × 8 notch on a long edge"
    const at = [[-30, -15], [30, -15], [-30, 15], [30, 15]];
    const notch = box(10, 10, 10).at(0, 22, 0).tag("notch");
    let plate = box(80, 50, 6).cut(notch);
    for (const [x, y] of at) plate = plate.cut(cylinder(3, 10).at(x, y, 0));
    return { plate, checks: [
      { size: { is: [80, 50, 6] } },
      { holes: { diameter: 6, through: true, at } },
      { feature: { tag: "notch", size: [10, 8, 6], center: [0, 21, 0] } },
    ] };

### TurnCheck

```ts
export interface TurnCheck {
  /** The named body that turns. */
  body: string;
  /** The axis: `"x"`, `"-y"`, `"+z"` or a direction `[x, y, z]`. */
  about: PrintAxis | [number, number, number];
  /** A point the axis passes through, mm. */
  at: [number, number, number];
  /** From and to, degrees, in the order the motion runs; 0 is as drawn. */
  degrees: [number, number];
}
```

A body turned about an axis, for a `turns` check: a print-in-place hinge
proven clear at every angle of its swing on the exact solids.

- `about` is the axis, `at` a point on it (the pin); right-handed, the
  sign `.rotate("x", d)` gives, 0° as drawn, at most one revolution.
- Draw the hinge open and flat, the pin of one body through the other's
  knuckles with a 0.4 mm gap, and let the check fold it; never draw the
  lid shut and `.printedUp()` it.
- A lid is two checks: `clearOf` up to where the latch bites, `catches`
  with `deeperThan` over the last degrees.
- The reply's `checks.motions` has the least clearance and its pose, or
  where the bodies first meet.

@example
    // a box with a lid on a print-in-place hinge, drawn open and flat
    const AY = -28;                              // the hinge axis
    const AZ = 10.2;
    const base = box(60, 40, 10).at(0, 0, 5).cut(box(56, 36, 9).at(0, 0, 6))
      .union(cylinder(4, 20).rotate("y", 90).at(0, AY, AZ).cut(cylinder(1.9, 22).rotate("y", 90).at(0, AY, AZ)));
    const lid = box(60, 40, 10).at(0, -56, 5).cut(box(56, 36, 9).at(0, -56, 6))
      .union(cylinder(4, 18).rotate("y", 90).at(-19.5, AY, AZ), cylinder(4, 18).rotate("y", 90).at(19.5, AY, AZ))
      .union(cylinder(1.5, 58).rotate("y", 90).at(0, AY, AZ));
    return { base, lid, checks: [
      { turns: { body: "lid", about: "x", at: [0, AY, AZ], degrees: [0, -180] }, clearOf: ["base"], atLeast: 0.3 },
    ] };

### SlideCheck

```ts
export interface SlideCheck {
  /** The named body that slides. */
  body: string;
  /** The direction: `"x"`, `"-y"`, `"+z"` or `[x, y, z]`. */
  along: PrintAxis | [number, number, number];
  /** From and to, mm along `along`, in the order the motion runs; 0 is as drawn. */
  mm: [number, number];
}
```

A body slid along a direction, for a `slides` check: a drawer in its
frame, a phone into its sleeve, checked over the whole travel.

- `along` is the direction, `mm` the travel from the body as drawn (0).
- A `.reference()` body may be the one that slides: "does the phone go
  in" is `slides: { body: "phone", ... }, clearOf: ["sleeve"]`.
- The reply's `checks.motions` carries the least clearance and where it
  is, or where the bodies first meet.

@example
    const frame = box(60, 80, 40).at(0, 0, 20).cut(box(50, 82, 30).at(0, 0, 20));
    const drawer = box(49.4, 70, 29.4).at(0, 0, 20).cut(box(45, 66, 26).at(0, 0, 22));
    return { frame, drawer, checks: [
      { slides: { body: "drawer", along: "y", mm: [0, 60] }, clearOf: ["frame"], atLeast: 0.25 },
    ] };

### Mechanism

```ts
export interface Mechanism {
  /** The drive's range, which every law reads; default [0, 1]. */
  drive?: [number, number];
  [body: string]: Joint | [number, number] | undefined;
}
```

Bodies that move together on one drive, as `mechanism: {...}` beside the
bodies: a lever pushing a slider, a rack and its pinion. One slider plays it
in the chat.

- `drive: [from, to]` is the one input, default [0, 1].
- Each other key is a moving body: `turns: { about, at, degrees }` or
  `slides: { along, mm }`, as in a turns/slides check without `body`.
- A law is `[from, to]`, straight in the drive, or a function `t => value`.
- `on: "b"` beside a joint moves the body on b; `{ on: "b" }` alone rides b.
  0 is the body as drawn.
- Check it with `{ moves: "all", atLeast }` and `{ pushes: [a, b], within }`.

@example
    // a lever on a pin pushes a slider along x as it drops 18°
    const PIN = [0, 0, 40];
    const base = box(90, 30, 4).at(0, 0, 2);
    const lever = box(6, 10, 30).at(0, 0, 25);
    const slider = box(20, 10, 8).at(13.1, 0, 14);
    // how far the lever's lower corner, 3 mm right of the pin and 30 below, has moved in x
    const push = (t) => {
      const a = (18 * t * Math.PI) / 180;
      return 30 * Math.sin(a) + 3 * (Math.cos(a) - 1);
    };
    return { base, lever, slider,
      mechanism: {
        lever: { turns: { about: "y", at: PIN, degrees: [0, -18] } },
        slider: { slides: { along: "x", mm: push } },
      },
      checks: [{ moves: "all", atLeast: 0.3 }, { pushes: ["lever", "slider"], within: 0.15 }] };

### Joint

```ts
export interface Joint {
  /** A turn about an axis through `at`, `degrees` from as drawn. */
  turns?: { about: PrintAxis | [number, number, number]; at: [number, number, number]; degrees: Law };
  /** A slide along a direction, `mm` from as drawn. */
  slides?: { along: PrintAxis | [number, number, number]; mm: Law };
  /** The body this one is carried by: its joint moves with that body's. */
  on?: string;
}
```

How one body of a Mechanism moves with the drive.

### Law

`type Law = [number, number] | ((t: number) => number)`

A joint's value against the drive: from and to, straight, or a function of the drive.
