git:20261012.91cf7b3 to git:20261012.2503e83

8 added, 3 removed. Audit B to B.

---
name: kernelcad-authoring
description: kernelCAD model authoring API — primitives, transforms, booleans, sketches, path-builder, sketch-text, return-single-shape rule, conventions, CLI commands, and verification gates. Load this when writing or modifying .kcad.ts geometry.
---
# kernelCAD — authoring
- Author or modify kernelCAD models in TypeScript. Scripts live in `.kcad.ts` files; the kernelCAD CLI (`kernelcad evaluate <file>` and `kernelcad export stl|step|dxf|3mf|glb <file> -o <out>`) runs them.
+ Write or edit `.kcad.ts` TypeScript models; run `kernelcad evaluate <file>` or `kernelcad export stl|step|dxf|3mf|glb <file> -o <out>`.
- `.kcad.ts` scripts are single files: top-level `import`/`require` is refused with `feature.invalid-args` — define helpers in the same file.
+ Single-file scripts refuse top-level `import`/`require` with `feature.invalid-args`; define helpers in the same file.
## Agent authoring loop
Use this loop for every non-trivial model edit:
1. **Classify the job**: blockout, production-ish part, reference replication, assembly/mechanism, sheet metal, or standard-part integration. Load the matching specialty skill before editing. Words like *real*, *production*, *complex*, *enclosure*, *gearbox*, *bearing housing*, *robot arm* → production-ish / mechanism — `lookup_cookbook("multi-feature machined housing")` or `lookup_cookbook("multi-body mechanism real proportions")`, and prefer MCP `design_loop` until green (see `docs/agent/adam-quality-bar.md`). Do not ship stacked-primitive toys for those prompts.
2. **Write the design brief in plain engineering terms**: purpose, external dimensions, interfaces, materials/finish if relevant, moving parts, manufacturability assumptions, and what must be proven. If the request is ambiguous on load-bearing parameters — overall dimensions, units, symmetry, part count, fit/clearance targets — ask 1–3 targeted clarifying questions BEFORE generating geometry; if you proceed anyway, list each assumption in the brief and encode it as a named `param()` so the user can correct it without a rewrite. Do ParamRef arithmetic with methods, not operators: `w.add(5)`, `w.subtract(t)`, `w.multiply(2)`, `w.divide(4)`, `w.negate()`.
3. **Map words to geometry**: turn important prompt phrases into named source sections, parameters, parts, connectors, materials, or tests so the generated model stays traceable to the user's words.
4. **Plan parameters and artifacts**: identify the `.kcad.ts` source file, named parameters, imported STEP files, expected exports, and the smallest verification command set before writing geometry.
5. **Edit source only**: treat `.kcad.ts`, prompt/brief markdown, and provenance metadata as source. Do not hand-edit generated PNG, MP4, STEP, STL, score JSON, or capture metadata.
6. **Generate explicit targets**: run the exact render/export/capture command for the requested artifact. Avoid broad directory refreshes unless the task is a release/demo rebuild.
7. **Validate deterministically**: run `kernelcad evaluate`, relevant exports, assembly/review tools, interference checks, and scorer gates before accepting visual evidence.
8. **Inspect visual artifacts honestly**: when a PNG/MP4/render is produced, open/read it and report what is visible. If the image shows wrong proportions, floating parts, occlusion, unreadable details, or a bad camera crop, repair source and regenerate.
9. **Packetize visual evidence**: when visual evidence matters, run `kernelcad render inspect <file> <outDir>` to produce a deterministic inspection bundle: a manifest naming the source file, command, generated artifacts, and caveats, plus canonical RGB views. Add `--channels rgb,mask,depth,normals` when machine-readable object masks, depth, or view-space normals are needed. Use `--focus <names>` or `--hide <names>` to isolate feature ids or assembly part names when clutter would obscure the check. Keep richer channels in the same manifest packet; do not replace the canonical RGB views.
10. **Repair one cause at a time**: target the smallest source change that addresses the failing check, then rerun the same check. Do not loosen gates or silently skip failing evidence.
11. **Never present an unverified model**: if the latest `evaluate` failed, fix it or report the blocker — do not export, render, or hand over a script whose last check failed.
**Verify against geometry, not against your own summary.** Trust measured
evidence — exact bbox/volume from `inspect({ of: 'shape' })`/`inspect({ of: 'step' })`, interference
volumes, DFM findings, the rendered pixels — over the narrative of what the
script "should" have built. A green `evaluate` (`ok: true, featureCount: N`)
proves the script ran, not that the geometry is correct. When a check cannot
measure a thing (kernel error, unknown clearance status, an inconclusive
render), treat that uncertainty as a failure to resolve, not a pass to assume.
**Verify against the design intent, not just validity.** Diagnostics-clean is
necessary but not sufficient: a solid block is valid geometry and still fails
the design. Before finishing, re-read the request and confirm every must-have
feature is actually modeled — openings, cavities, wall thickness, separate
components, clearances, legroom, load paths, and the specified manufacturing
method. Fix the source until those are visibly satisfied.
**Check your work: render top, front and iso before calling a layout done.**
DFM checks walls, overhangs and clearance; it does NOT check whether text,
logos, slots or other decorations overlap holes, sit off the part, or collide
with each other. Only the pictures show that. Render
(`render_preview` / `kernelcad render`) the top, front and iso views and look
at each one before you report a layout as finished. For a whole part, check
the seven-view sheet (`render_preview({ file, sheet: true })`): done means
every tile matches the request, not that it compiles.
## Inner loop: render after every visible change
The authoring loop above is the *outer* loop. The *inner* loop runs after every feature you add that you expect to see in the rendered output. Skipping the inner loop is the single biggest cause of "tests green, output wrong" shipments.
After every one of these operations, render and look at the result:
- a new `subtract(...)` that should produce a visible hole, notch, or relief
- a new `union(...)` of shapes that should carry distinct materials
- a `material({...})` change on a body that should be visually distinct from its neighbors
- a new sketch + `extrude` whose orientation matters (i.e. anything front-facing)
- a parameter retune that should visibly move a feature
The minimum check:
```bash
node dist/cli/index.js render <file> -o /tmp/check.png --pose <scorer-pose> --hide-reference-images
```
Pick the pose the scorer uses (look in the task's `harness.ts` for `REFERENCE_POSE` — typically `30,15` for canonical 3/4 product shots). Render at default views (no `--pose` flag) only when you specifically want the front/right/top/iso composite — that view often hides defects visible at the scorer pose.
When you open the rendered PNG, ask three questions:
1. **Is the feature I just added visibly present?** If the subtract didn't punch through, the union didn't show a color delta, or the material change didn't render, the eval-harness JSON of `ok: true, featureCount: N` is lying about success. Fix the geometry, don't ship.
2. **Are features I didn't intend to change still where they were?** Material chaining, sketch reuse, and chained booleans regularly corrupt earlier features silently. Compare against the previous render.
3. **Does the silhouette match the reference at the same pose?** Not pixel-perfect — but the right number of openings, right proportions, right shape category.
If a render shows wrong proportions, hidden openings, floating parts, or unreadable details, repair `.kcad.ts` source and re-render. Do not pile features on top of broken geometry. Do not declare done until the most recent render shows the intent.
Common inner-loop traps are catalogued in the cookbook at
`kernelcad-nurbs/cookbook/snippets/your-first-real-build-anti-patterns.md`
(orientation defaults, sketch-axis sign flips, material-shadowing on union,
two-feature placement math, subtract-chain reliability, JSON-`ok`-is-not-visual-proof).
## Assembly and mechanism loop
`evaluate_script` on a model with mates runs only a shallow mechanism check.
It does NOT check joint-support intents (`jointSupport` / `mechanicalJoint`),
pose-envelope overlap at a declared mate limit, or a gravity drop / static hold,
so `ok: true` from it is not a mechanism verdict. When the result carries
`reviewHint`, run `review_cad` for the pose-envelope + gravity checks before you
call the mechanism done.
### Articulated-digit workflow (non-bypassable)
For an articulated digit, use the structural joint helper and complete every gate below:
<!-- ARTICULATED_DIGIT_EXAMPLE:START -->
```typescript
const arm = assembly('index-digit');
const minClearance = 0.1;
const parentMount = 'palm.index-mount';
const frame = { origin: [0, 0, 10], pinAxis: [0, 0, 1], forward: [1, 0, 0] };
const segments = [
{ name: 'proximal', lengthMm: 54, widthMm: 14, depthMm: 14, terminal: true },
];
const joints = [
{ name: 'mcp', limitsDeg: [0, 27], style: { knuckleR: 5.4, forkGapY: 10, pinCapThickness: 3 } },
];
arm.part('palm', box(20, 50, 20, true).translate(-10, 0, 0))
.connector('index-mount', {
type: 'frame',
origin: { kind: 'vec3', value: [0, 0, 10] },
});
joint.articulatedDigit(arm, {
name: 'index',
parentMount,
frame,
clearanceMm: minClearance + 0.1,
segments,
joints,
});
dfmSpec({ minClearance, includeArticulatedMates: true });
return arm.solvedModel({}, { validate: 'error' });
```
<!-- ARTICULATED_DIGIT_EXAMPLE:END -->
- Run the full `review_cad` review, including pose-envelope clearance and interference checks.
- Confirm visual fit separately: every digit link and moving mate must remain outside package keepouts across the reviewed pose envelope.
- Dynamics, load, and actuation claims are unverified without explicit evidence; articulated-digit geometry and clearance review do not certify payloads or actuation.
- These steps are mandatory. Do not bypass the helper, DFM declaration, pose-envelope review, keepout fit check, or evidence requirement by substituting raw offsets or a visual-only pass.
### Think in parts
- Every physically distinct component is a named `assembly().part(name, shape)` — one body per part unless the component is genuinely monolithic.
- Anonymous loose top-level bodies in a multi-body model are a defect smell: the review loop emits `assembly.structure.unstructured-bodies` (info) with the recovery hint. Wrap each loose body in a named part.
- Part names are the durable handles for `inspect --focus`, `inspect({ of: 'part-stats' })`, and Studio's hide / keep-whole / per-part validity — choose stable, descriptive names.
### Multi-member fabrications (frames, weldments, sheet on tubes)
A tube frame, weldment, rack, or tray is several members cut to length. Build it one of two ways:
1. **`assembly().part(name, member)` per member** — interference between parts is checked; every part's own unions get the same `union.*` checks below. Preferred when the members are bought or cut separately.
2. **One `union()` of members that touch but never overlap.** Members butt against each other: a cross member between two side rails is `span - 2 * railWidth` long and starts at `x = railWidth`, so its end faces sit on the rails' inner faces. The first and last cross members sit inside the frame, next to the front/back rails, not inside them. A sheet on top of the tubes sits at `z = tubeHeight`, on the tubes.
```ts
const crossLen = width.subtract(tube.multiply(2)); // fits BETWEEN the rails
const s1 = tubeX(crossLen).translate(tube, endInset, 0).finish('aluminium-brushed');
const sheet = box(width, depth, sheetT).translate(0, 0, tube).finish('aluminium-brushed'); // on the tubes
return union(sheet, front, back, left, right, s1 /* ... */);
```
`evaluate_script` / `kernelcad evaluate` gate every `union()` (errors make `ok: false`):
- `union.disconnected` — the union is more than one solid; the message names the floating operand, its bbox and the gap. Move it until it touches (shared face, zero gap) or add the member that carries it. This applies inside `assembly().part(...)` too: a part that is several floating pieces fails evaluate (the assembly validator's `assembly.mechanical.part-disconnected` is only a warning; it is not a substitute).
- `union.member-overlap` — two operands that each carry their own `.finish()`/`.material()` and the SAME one (two pieces of the same stock) share more than 1 mm³; the message names both, the volume and the overlap box. Most often this is one part finished piece by piece: fuse the same-material pieces first and call `.finish()` once on the result. If they really are separate members, cut one to fit (butt joint) instead of burying it in the other. This applies inside assembly parts too. Operands with DIFFERENT materials may overlap (inlay, over-mould, multi-material part).
Finish once, after the booleans: a boss or rib merged into a body BEFORE `.finish()` has no finish of its own, so ordinary overlapping modelling unions are not affected. Give a piece its own finish only when it is a different material (inlay, over-mould) or a separate member.
- If a model has moving parts, design the joint structure before styling: name the parent/child parts, joint type, axis/frame, limits, and editable pose parameters up front.
- If two parts are intended to touch, author the relationship with connectors and mates rather than relying on raw `translate()` offsets alone. Raw offsets are acceptable for free placement, but touching load-path geometry needs named interfaces the validator and Studio can inspect.
- Prefer `assembly().model()` for multi-part scenes so Studio receives per-part identity, material, mate, and transform metadata. Collapse with `.toCompound()` or `.toUnion()` only when a downstream export truly requires one body.
- For interactive joints, keep heavy CAD recompute as the committed source-of-truth path, but use viewport-side joint posing for drag feedback whenever transform metadata is available. Drag should move the existing part mesh immediately; release commits the parameter for exact recompute, review, and export.
- Final reports for mechanism/gallery work should include an artifact packet: source path, generated artifact paths, deterministic checks run, visual proof path, and unresolved caveats.
## Source and artifact policy
- Source of truth: `.kcad.ts`, task brief/prompt markdown, tests, and explicit provenance metadata that records where dimensions, vendor parts, prompts, and review decisions came from.
- Derived artifacts: rendered PNGs, MP4 demos, STEP, STL, 3MF, DXF, score JSON, generated topology dumps, generated capture-run metadata, and generated route/build outputs.
- Commit derived artifacts only when the repository already treats that directory as an artifact bundle (`docs/demos/**`, `examples/portfolio/**`) or the user explicitly requested a deliverable bundle.
- When committing derived artifacts, keep the source file and provenance beside them and prefer existing capture scripts so hashes/metadata stay reproducible.
- If an artifact changes but source/provenance did not, stop and explain why. Hidden artifact churn is not a valid CAD change.
- Required visual packet: for review-worthy visual work, keep an inspection bundle beside the generated artifact by running `kernelcad render inspect <file> <outDir>`. The v1 bundle writes `manifest.json` and canonical RGB views, with optional `--channels rgb,mask,depth,normals` and `--focus <names>` / `--hide <names>` object filters recorded in the manifest. Depth, normals, and mask channels extend the same packet as richer evidence; they do not replace the canonical RGB views.
## Standard parts and vendor geometry
Use `lib.fromSTEP(...)` for off-the-shelf components whenever physical fit matters:
- Good candidates: motors, servos, bearings, shafts, fasteners, hinges, sensors, PCBs, connectors, rails, and purchased enclosures.
- Store or reference the vendor STEP file deliberately; name the source, version, and license/terms in nearby metadata or README when the file is part of a demo/portfolio bundle.
- Before placing a vendor STEP, run `kernelcad inspect step <file.step>` (or the `inspect({ of: 'step' })` MCP tool) to read the solid tree, per-solid exact bbox + volume, and detected cylindrical holes (axis, diameter, depth, blind/through) — find mounting-hole positions and verify the part-local frame from exact geometry instead of measuring renders.
- Build modeled brackets, mounts, clearances, cable paths, and keepouts around the imported part rather than approximating the part with generic boxes/cylinders.
- Placeholder geometry is acceptable for early blockouts, but final review must label it as a placeholder or replace it with catalog geometry.
- Format choice when the vendor offers several: STEP (`lib.fromSTEP`) is the default and the most portable. Native `.brep` (`lib.fromBREP`) is lossless and the fastest to load. Use it to round-trip kernelCAD geometry. Use STEP to interchange with other CAD systems. Reach for `.stl` (`lib.fromSTL`) only when nothing better exists: it is a triangle mesh, so you keep bbox/volume/booleans but lose analytic surfaces, which means no reliable fillet/chamfer on curved edges, no canonical face refs, and no hole detection. If a mostly prismatic part is STL/OBJ/3MF-only and you need editable geometry (mounting holes, params), run `mesh_to_features` (CLI `kernelcad reconstruct`): it emits a feature-tree script and measures it against the mesh — use it when the verdict is faithful, otherwise model the interface yourself from the reported regions rather than trusting facet normals.
> For off-the-shelf fasteners, bearings, motors, headers, and connectors, prefer the bundled parts catalog: load the `kernelcad-parts` skill. The catalog exposes `lib.findPart`, `lib.fetchPart`, and a typed `lib.standard.*` namespace, plus four MCP tools for discovery. Bundled parts ship with pre-defined connector frames so they participate in mates without any `partRef.connector(...)` boilerplate.
## Installation
```bash
npm install -g kernelcad
kernelcad evaluate path/to/script.kcad.ts
```
Verify the install with `kernelcad --version` (should print `0.1.0` or higher).
## Coordinate System
- **Z-up**, right-handed.
- Lengths in millimetres, angles in degrees.
- Box: corner-anchored at the origin (spans `[0, x] × [0, y] × [0, z]`). `centered: true` (4th argument) anchors at the centroid.
- Cylinder: axis along Z, base at `z=0`, top at `z=h`.
- Sphere: centred at the origin.
## API Surface
### Before you say "kernelCAD can't make that"
NEVER tell a user a shape is impossible, or that it needs other software, without first calling `lookup_api` and `lookup_cookbook`. kernelCAD does NURBS freeform surfacing — lofts, sweeps, boundary-fill patches, G2 Hermite blends, and variable-section sweeps — so organic forms (body shells, panels, fairings, ergonomic curves, bottles, horns) ARE in scope. Its two real limits, state them precisely and do not overgeneralize past them:
- It is a **solid/NURBS** kernel, not a **polygon/subdivision sculptor** (Blender/Maya).
- Its viewer renders clean CAD, not **photoreal** images — for a photoreal hero shot, export STEP/GLB and finish in a render tool.
For an **organic** vehicle / berlinetta / fairing body, reach for `automotive-body-envelope` (`lookup_cookbook("automotive body envelope")`) — spline/rail loft or surfaceFromCurves + sew + thicken; glass/aero as separate parts. Reserve `loft-body-shell-from-profiles` (`lookup_cookbook("loft a stylized body shell")`) for stylized / mechanism-demo shells only.
### Top-level functions
```typescript
// Editable symbolic parameters. Returned value is a ParamRef accepted anywhere
// the API expects an editable number or boolean. Edit the param() default
// post-build with set_param via MCP / session.params.update in runtime code.
param<T extends number | boolean>(name: string, defaultValue: T, opts?: {
min?: number;
max?: number;
description?: string;
}): ParamRef<T>;
// ParamRef arithmetic uses methods, never JS operators (`+ - * /` throw):
// const w = param('w', 60), t = param('t', 2);
// box(w.add(5), w.subtract(t).multiply(2), w.divide(4)); // also .negate()
// Hex default ('#rrggbb') = colour param with a picker; pass the ref to
// .color(c) / .finish(t, { color: c }): param('BodyColor', '#3366cc')
params({ width: 60, addCablePort: true }): {
width: ParamRef<number>;
addCablePort: ParamRef<boolean>;
};
// Primitives. Each returns a Shape.
// box: default corner-origin, spans [0,x]×[0,y]×[0,z]. centered=true centres ALL
// THREE axes (Z spans -z/2..+z/2) — unlike cylinder, whose bottom stays on z = 0.
// Cavity from z = floor in a corner-origin base:
// base.subtract(box(x - 2*wall, y - 2*wall, z, true).translate(x/2, y/2, floor + z/2))
box(x: number, y: number, z: number, centered?: boolean, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery> }): Shape;
// cylinder: centred on the Z axis in X/Y, bottom on z = 0 (spans 0..h). No centered flag.
cylinder(h: number, r: number, segments?: number, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery> }): Shape;
sphere(r: number): Shape; // centred on the origin; faceLabels NOT accepted — sphere has no canonical faces
// Extrusion helpers — profile defined inline, extruded along Z. All four
// accept opts.twistAngle (deg; number or ParamRef): the profile rotates about
// the profile origin through the sweep. Unknown option keys throw
// feature.invalid-args.
extrudeRect(w: number, h: number, height: number, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery>; twistAngle?: Editable<number> }): Shape;
extrudeCircle(r: number, height: number, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery>; twistAngle?: Editable<number> }): Shape;
extrudePolygon(points: Array<[Editable<number>, Editable<number>]>, depth: Editable<number>, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery>; twistAngle?: Editable<number> }): Shape;
extrudeRoundedRect(width: number, height: number, radius: number, depth: number, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery>; twistAngle?: Editable<number> }): Shape;
// Path builder — chain moveTo / lineTo / arcs / .close() to get a Sketch. For
// revolved geometry (washers, donut bodies, mug profiles, etc.) build a profile
// via path() and call .revolve() on the resulting Sketch.
path(): PathBuilder;
// Boolean union of two or more shapes (top-level alternative to .union()).
union(...shapes: Shape[]): Shape;
// Inspectable mechanical assembly intent. Captures parts and joints as records.
// Call .model() to return one fused/exportable Shape of all placed parts.
assembly(name?: string): Assembly;
// Helix rail for Sketch.sweep. radius / pitch / turns / startAngle accept
// Editable<number>; the rail remembers them, so a sweep along it follows param
// changes and sweep(rail, { spine: 'helix' }) can build the exact helix.
helix({ radius, pitch, turns, axis?, pointsPerTurn?, startAngle? }): [number, number, number][];
// ISO metric threads (see "Threads" below). isoThread('M6') -> { spec: 'M6x1',
// diameter, pitch, minorDiameter, tapDrill, ... }; 'M6x0.75' / 'M70x4' set the pitch.
isoThread(spec: string): IsoThreadInfo;
// Real external thread along +Z, z = 0..length, ONE sweep (M6 x 20 ~2-3 s).
threadedRod({ spec, length, chamfer?: boolean | number, cosmetic?: boolean, clearance? }): Shape;
// 3D Bezier curve (3 points = quadratic, 4 = cubic) -> Curve3D.
bezier(points: [number, number, number][]): Curve3D;
// Edge selection — lowers the shape lazily (awaitable).
// selectEdges returns a ShapeList: still an EdgeSegment[] (fillet/chamfer accept
// it directly), plus the selector algebra described under "Selector algebra" below.
selectEdges(shape: Shape, query?: EdgeQuery): Promise<ShapeList<EdgeSegment>>;
selectEdge(shape: Shape, query: EdgeQuery): Promise<EdgeSegment>; // throws if zero or multiple match
// Selector algebra — wrap ANY array of topology query results (face summaries,
// q.face(...).evaluate(scene) results, a hand-built list) so the sort/group/filter
// methods apply. selectEdges already returns one.
select<T>(items: Iterable<T>): ShapeList<T>;
// Parts library — geometry import. Every from* call resolves the path relative
// to the calling .kcad.ts file (absolute paths also accepted) and returns the
// standard capture-proxy Shape — composes with translate/rotate/color and
// arm.part(...) like any primitive.
// STEP — the default for vendor catalog components.
lib.fromSTEP(path: string): Promise<Shape>;
// Native BREP — lossless (exact analytic surfaces and full topology, no
// schema translation, no tessellation). Every downstream op is valid on the
// result. Use STEP to interchange with other CAD systems.
lib.fromBREP(path: string): Promise<Shape>;
// STL (ASCII or binary) — THIS IS A MESH, NOT ANALYTIC B-REP. Triangles are
// sewn back into topology and promoted to a solid when they close, so
// booleans / volume / mass properties / bbox / export all work. But the faces
// stay planar facets: a hole that was a cylinder in the source CAD is now a
// fan of quads, so fillet/chamfer on a "curved" edge, canonical face refs and
// hole detection will NOT behave as they do on STEP/BREP input. Prefer
// fromSTEP/fromBREP whenever the source is available.
// A non-watertight mesh is REFUSED by default rather than silently returned as
// an open shell whose volume and booleans would be meaningless.
lib.fromSTL(path: string, opts?: {
tolerance?: number; // vertex-merge distance, mm (default 1e-6); raise for
// third-party meshes with non-bit-identical vertices
allowOpen?: boolean; // accept a non-watertight mesh as an open shell
maxTriangles?: number; // cost cap (default 200000; import is ~0.45 ms/triangle)
}): Promise<Shape>;
// Reference-image overlay — virtual node (no solid). The renderer draws
// the image on the chosen plane for tracing or design review. Path resolved
// relative to the calling .kcad.ts file. Supported formats: .png .jpg .jpeg .webp.
// Validation errors (missing file, bad format, invalid plane) are pushed as
// diagnostics on the returned handle rather than thrown.
referenceImage(path: string, opts: {
plane: 'xy' | 'xz' | 'yz' | { plane: 'xy' | 'xz' | 'yz'; offset?: number };
anchor?: 'origin' | [number, number, number]; // default 'origin'
scale?: 'fit-bbox' | number | { width?: number; height?: number }; // default 'fit-bbox'
opacity?: number; // [0, 1], default 0.5
flipU?: boolean; // default false
flipV?: boolean; // default false
}): ReferenceImageHandle;
// HDRI / image-based lighting for the rendered scene (W2). Pass either a
// built-in `preset` key or a custom .hdr `url` (mutually exclusive).
// `intensity` (default 1.0; clamped to (0, 100]) scales envMapIntensity on
// every PBR material; `rotation` (degrees, default 0) rotates the env map
// around the world Y axis. Virtual record — display only.
// Default behavior (script never calls this) is the existing three-light
// rig. Multiple calls register multiple records; the renderer applies the
// last one.
setRenderEnvironment(spec: {
preset?: 'studio' | 'softbox' | 'neutral' | 'outdoor' | 'warehouse';
url?: string;
intensity?: number; // (0, 100], default 1.0
rotation?: number; // degrees around Y, default 0
}): RenderEnvironmentHandle;
// Override the camera look-at target for `setRenderPose` and headless
// engineering renders. Default is the bbox centroid; that auto-fit skews
// when a build has tall asymmetric features (pocket-watch with pendant +
// bail above origin, scope with offset eyepiece, lamp with tall shaft).
// Pass an explicit (x, y, z) in the script's world frame to re-aim the
// camera; the renderer translates it into its recentered scene frame
// automatically. Virtual record — display only. Multiple
// calls register multiple records; the renderer applies the last one.
setCameraTarget(x: number, y: number, z: number): CameraTargetHandle;
// Override the camera framing distance (mm from target). Convenience wrap
// over setCameraTarget's optional `distance` field — inherits the most
// recent target (or world origin when no setCameraTarget call has
// happened) and pins the camera at the supplied distance along the pose
// direction. Use when the auto-fit extents-projection reads too tight or
// too loose at the chosen pose / aspect.
setCameraDistance(distance: number): CameraTargetHandle;
// Declare an animation timeline for offline kinematic-motion MP4 capture.
// Two author-surface forms — both normalize to the same stored track shape,
// captured by `kernelcad animate` / the `capture_animation` MCP tool (full
// authoring contract in the "Animation timelines" section below):
//
// 1. Legacy sweep — ONE previously-declared `param()` swept linearly from
// `from` to `to` over `durationMs` ms (normalizes to a two-key linear
// track):
animationView(spec: {
param: string;
from: number;
to: number;
durationMs: number;
fps?: number; // default 30
}): AnimationViewHandle;
// 2. Keyframe tracks — several params on one shared timeline. Each track
// animates one `param()` through `{ atMs, value, ease? }` keys. `ease`
// applies to the segment ENDING at that key (default 'linear'); the
// value HOLDS before the first key and after the last (clamp). Total
// duration is the max `atMs` across all tracks.
animationView(spec: {
name?: string; // optional human label, e.g. 'dispense cycle'
tracks: Array<{
param: string; // a NUMERIC param() declared earlier; one track per param
keys: Array<{
atMs: number; // >= 0, finite, unique within the track
value: number; // clamped to the param's declared min/max (warn on clamp)
ease?: 'linear' | 'step' | 'easeIn' | 'easeOut' | 'easeInOut'; // default 'linear'
}>;
}>;
fps?: number; // default 30
}): AnimationViewHandle;
// Virtual record — display only. Every animated param MUST be declared
// NUMERIC by a prior param() call; undeclared/non-numeric params, two tracks
// on the same param, and malformed keys (empty tracks/keys, non-finite or
// negative atMs, duplicate atMs, unknown ease) THROW KernelError
// (`animation.param.unknown` / `animation.track.duplicate-param` /
// `animation.keys.invalid`). Out-of-range key values clamp to the param
// boundary with an `animation.value.clamped` warn. Multiple animationView()
// calls register multiple records; capture uses the LAST one and the later
// record carries an `animation.view.shadowed` warn naming the shadowed ids.
// Declare printability (design-for-manufacture) gates for the model.
// Declaration-only: registers a virtual record (display only);
// enforcement runs on every `evaluate` / `evaluate_script` once a dfmSpec
// record is present. At least one of minWall / minClearance / channels is
// required. Malformed declarations THROW KernelError
// (`feature.invalid-args`) rather than stashing diagnostics — dfmSpec is
// an enforcement gate, and a silently-disabled gate is worse than a build
// failure. Multiple calls register multiple records; the last one wins
// (same convention as setRenderEnvironment). Full semantics: see the
// "DFM gates (print readiness)" section below.
dfmSpec(spec: {
minWall?: number; // mm — min printed wall thickness per non-excluded part
minClearance?: number; // mm — min distance between distinct parts
includeArticulatedMates?: boolean; // also measure non-fastened mates at rest; requires minClearance
ignore?: [string, string][]; // part-name pairs exempt from clearance (design-intent contacts)
exclude?: string[]; // non-printed parts (vendor STEP, electronics); trailing-'*' glob per entry
channels?: Array<{
part: string; // owning part name (single-shape scripts: 'shape')
name: string; // author-facing label, echoed in diagnostics
openings: number; // expected count of distinct mouth openings to the outside
sealed?: boolean; // intentionally sealed internal void (openings must be 0)
}>;
}): DfmSpecHandle;
```
### Selector algebra (ShapeList)
`EdgeQuery` / `FaceQuery` are flat predicate bags: they answer *"which entities
match these constraints?"*. They cannot answer *"the highest three faces"*,
*"the largest bore"*, or *"one group per Z level"*. `ShapeList` is the other
half — sort / group / filter over results the kernel has ALREADY resolved.
Compose the two, don't substitute one for the other: let the declarative query
filter in the kernel first, then rank or bucket the resolved list in TypeScript.
```typescript
// selectEdges returns a ShapeList; select(...) wraps any other result array.
const edges = await selectEdges(body, { convex: true });
edges.sortBy('Z').last // topmost edge
edges.sortBy('Z', { descending: true }).take(3) // highest three
edges.filterBy('CIRCLE').sortBy('radius').last // largest circular edge
edges.filterBy('Z') // edges running parallel to +Z
edges.filterByPosition('Z', 8, 10) // edges in the top 2 mm slab
edges.sortByDistance([0, 0, 20]).first // nearest to a point
edges.filterBy((e) => e.length > 5) // arbitrary predicate
const levels = edges.groupBy('Z', { tolerance: 0.01 }); // one group per Z level
levels.length; levels.keys; // 3, [0, 5, 10]
levels.at(-1); // top level (ShapeList)
levels.byKey(10); // same group, looked up by value
```
Contracts worth knowing:
- **Immutable.** Every method returns a NEW list; nothing sorts in place.
- **Identity survives.** The algebra reorders the SAME descriptor objects — it
never re-derives one — so `EdgeSegment.id` and the `@kc[...]` ref
handle on a `ResolvedEntity` are intact after any sort or group.
- **Float-noise tolerant.** Both `sortBy` and `groupBy` compare on a quantized
metric (1e-6 by default, or an explicit `tolerance` in mm) with the incoming
position as tiebreak, so kernel noise cannot flip an order or split one Z
level into two groups. `byKey` quantizes the request identically.
- **It is still an array.** `ShapeList` extends `Array`, so `.length`,
indexing, spread, `for..of` and `.map` / `.filter` all work, and a
`ShapeList<EdgeSegment>` passes straight into `fillet` / `chamfer`.
- **`radius` is CIRCLE-only.** `sortBy('radius')` needs `EdgeSegment.radius`,
which is populated for `curveType === 'CIRCLE'` and deliberately absent
otherwise; asking for it elsewhere throws rather than approximating.
### Shape methods (chainable)
```typescript
// Transforms (mutate-then-return-this; chain freely):
.translate(x: Editable<number>, y: Editable<number>, z: Editable<number>): Shape
.rotate(
axis: [Editable<number>, Editable<number>, Editable<number>],
degrees: Editable<number>,
pivot?: [Editable<number>, Editable<number>, Editable<number>],
): Shape
// Cardinal-axis aliases for .rotate — prefer these for the common cases:
.rotateX(degrees: Editable<number>, pivot?: [Editable<number>, Editable<number>, Editable<number>]): Shape // .rotate([1, 0, 0], ...)
.rotateY(degrees: Editable<number>, pivot?: [Editable<number>, Editable<number>, Editable<number>]): Shape // .rotate([0, 1, 0], ...)
.rotateZ(degrees: Editable<number>, pivot?: [Editable<number>, Editable<number>, Editable<number>]): Shape // .rotate([0, 0, 1], ...)
// Uniform (single positive finite number) or per-axis Vec3 (non-uniform sx/sy/sz).
// Non-uniform lowers via gp_GTrsf + BRepBuilderAPI_GTransform so face refs survive
// (topology preserved under any affine transform). All factors must be positive
// and finite; otherwise feature.invalid-args.
.scale(factor: number | [number, number, number]): Shape
// Orient this shape so its current +Z axis aligns with `axis`. Sugar over .rotate() —
// preferred for cross-axis cylinders/axles. Identity [0, 0, 1] is a no-op; antipodal
// [0, 0, -1] is a deterministic 180° around X. Zero vector throws feature.invalid-args.
.alongAxis(axis: [number, number, number]): Shape
// Bounding-box query + placement normalizers — ESSENTIAL for placing fetched
// catalog parts. A `lib.fetchPart(ref)` STEP arrives at its own arbitrary
// native origin; box(l,w,h) is corner-origin (min corner at 0,0,0). Without
// normalizing, .translate(x,y,z) just nudges a part from wherever the STEP
// authored it, so real parts scatter/overlap. These three fix that. All async
// (they lower the shape to read its real AABB) — `await` them.
//
// AABB in the CURRENT world frame (after every transform appended so far), mm:
.boundingBox(opts?: { exact?: boolean }): Promise<{ min: [number,number,number]; max: [number,number,number]; size: [number,number,number]; center: [number,number,number] }>
// Translate so bbox center → world origin; returns this Shape for chaining.
// After recenter(), a following .translate(x,y,z) places the part's CENTER at
// (x,y,z). Pass { z:false } etc to recenter only the named axes.
.recenter(opts?: { x?: boolean; y?: boolean; z?: boolean }): Promise<Shape>
// Seat on the z=0 floor (bbox min.z → 0), centered in x/y; returns this Shape.
// { center:false } drops onto z=0 without moving x/y.
.seatOnFloor(opts?: { center?: boolean }): Promise<Shape>
//
// // Place a fetched servo so its CENTER lands at (20, 0, 0):
// const servo = await lib.fetchPart('servo/sg90');
// (await servo.recenter()).translate(20, 0, 0);
// // Stand a part upright on the build plate:
// (await bracket.seatOnFloor());
// Tag this shape with a render-time role color (geometry unchanged). Booleans drop
// the color so identity lives at leaf parts: a `.color()` call applied to a
// composed/unioned shape silently has NO effect on the underlying leaf parts —
// each part must be colored BEFORE it enters a boolean. Coloring the post-union
// root is a no-op; don't infer from a uniform-grey render that "renderer ignores
// .color()". Tokens: 'servo' | 'gear' | 'beam' | 'shaft' | 'plate' | 'pin' |
// 'frame' | 'tool'. Hex escape hatch: any '#rrggbb'.
.color(name: 'servo' | 'gear' | 'beam' | 'shaft' | 'plate' | 'pin' | 'frame' | 'tool' | `#${string}`): Shape
// Booleans (each returns a NEW Shape that captures a 'boolean' feature record):
.union(...others: Shape[]): Shape
.subtract(...others: Shape[]): Shape
.intersect(...others: Shape[]): Shape
// Edge features:
// Simple form — single radius applied to matching edges:
.fillet(radius: number, edges?: EdgeSelector): Shape
// Variable-radius form — one group per blend region:
.fillet([{ edges: EdgeSelector, radius: number }, ...]): Shape
// Bevel edges (same selector shape as fillet):
.chamfer(distance: number, edges?: EdgeSelector): Shape
.chamfer([{ edges: EdgeSelector, distance: number }, ...]): Shape
// Face features:
.shell(thickness: number, { face: FaceSelector }): Shape // face REQUIRED
// Draft — taper faces for mold release (Slice E). angleDeg: 0–90.
// neutralPlane defaults to the selected face; pullDir defaults to face normal.
.draft(angleDeg: Editable<number>, opts: { face: FaceSelector | string; neutralPlane?: string; pullDir?: [number, number, number] }): Shape
// Symmetry operations:
// Pure reflection across a cardinal plane (no union — volume unchanged, handedness flipped):
.reflect(plane: 'xy' | 'xz' | 'yz' | { plane: 'xy' | 'xz' | 'yz'; offset: number }): Shape
// Boolean union of the source and its reflection — produces a symmetric part:
.mirror(plane: 'xy' | 'xz' | 'yz' | { plane: 'xy' | 'xz' | 'yz'; offset: number }): Shape
// Mechanical patterns:
.patternLinear({ count, direction, spacing }: { count: number; direction: [number, number, number]; spacing: number }): Shape
.patternGrid({ x, y }: { x: { count: number; direction: [number, number, number]; spacing: number }; y: { count: number; direction: [number, number, number]; spacing: number } }): Shape
.patternCircular({ count, axis, angleDeg? }: { count: number; axis: [number, number, number]; angleDeg?: number }): Shape
// Apply an SE(3) Transform (returned by SolvedKinematics.transform()) to a shape.
// Decomposes to translate + rotate via the existing transform pipes; no rebake.
.transform(t: Transform): Shape
// Eager lowering (for inspection; rarely called by agents directly):
.lower() — internal; do not call from a script
```
`EdgeSelector = EdgeQuery | EdgeSegment[] | { face: string | FaceQuery } | undefined`
`FaceSelector = CanonicalFace | string (label) | FaceQuery`
`CanonicalFace = 'top' | 'bottom' | 'left' | 'right' | 'front' | 'back'`
**Declare drawing review** with `shape.drawingReview(plan)` to bind required geometric callouts and engineering decisions to this shape and downstream mechanical drawings. The plan has `id`, `requirements: [{ id, featureId, annotationId: 'authored:<dimension id>', expectedKind: 'linear' | 'diameter' | 'radius' | 'angular', role: 'critical' | 'reference', reason }]` and `decisions: [{ id, kind: 'datum' | 'fit' | 'tolerance' | 'dimension' | 'other', featureId, prompt, resolution?: { note, provenance } }]`. Both arrays are required; at least one must contain an entry. Unknown fields, empty strings and duplicate IDs reject. This virtual declaration returns the same shape and creates no geometry. See `kernelcad-drawings` for export review options and statuses. Coverage proves declared callout presence; it does not certify fits, tolerances or manufacturing.
**Declare dimensions** the viewer should show with `shape.dimension(spec)`: optional nonempty `id` gives its stable `authored:<id>` drawing identity; a declaration only, it returns the same shape and adds no geometry. `{ kind: 'linear', from, to, label? }` measures between two anchors, each a point `[x, y, z]`, `{ edge: EdgeQuery }` or `{ face: FaceQuery }`; a circular edge anchor means the circle centre, so two holes give their centre distance. `{ kind: 'diameter' | 'radius', edge, label? }` calls out one circular edge (`Ø5`, `R3`). `{ kind: 'angular', from, to, label? }` measures between two edges. A label is a name, not a number: a linear dimension reads `<label> <value>` (`hole spacing 30`), so never type the number into the label. Declared dimensions turn the viewer's Dimensions toggle on by default, and a shared project link with `?dims=1` forces it on. `evaluate_script` returns them as `dimensions: [{ text, kind, source }]`, so check the text before you publish. Recipe: `dimension-callouts`.
```typescript
const holeAt = (x: number, y: number) => ({ edge: { ofCurveType: 'CIRCLE' as const, near: [x, y, 6] as [number, number, number] } });
const plate = box(60, 40, 6)
.holes('top', { positions: [{ u: -15, v: 0 }, { u: 15, v: 0 }], diameter: 5, depth: 'through' })
.threadedHole('top', { u: 0, v: 12, spec: 'M5', depth: 8 }) // ISO tapped hole by designation; modeled: true cuts the thread
.dimension({ kind: 'linear', from: holeAt(15, 20), to: holeAt(45, 20), label: 'hole spacing' }); // "hole spacing 30"
```
### Sketch methods
A `Sketch` is produced by `path()...close()`. All Sketch methods return a `Shape` (or another `Sketch` for `reflect`).
```typescript
// Extrude closed sketch normal to its plane by `depth` (mm; number or ParamRef).
// twistAngle (deg; number or ParamRef) is real twisted-extrude geometry: the
// profile rotates about the sketch origin through the sweep, so a profile that
// does not touch the origin changes its bounding box. Face-bound extrudes
// (projectCurve output) reject a non-zero twist with feature.invalid-args,
// as does any unknown option key.
.extrude(depth: Editable<number>, opts?: { faceLabels?: Record<string, CanonicalFace | FaceQuery>; twistAngle?: Editable<number> }): Shape
// Revolve around the Z axis (360 degrees unless angleDeg; number or ParamRef).
// Profile coords are (radial-X, axial-Z); all x >= 0.
.revolve(opts?: { angleDeg?: Editable<number> }): Shape
// Sweep this profile along a 3D rail.
// spine: 'polyline' (default) keeps real corners — pipe runs, L-bends;
// transitionMode ('right' | 'transformed' | 'round') picks how those corners are bridged.
// spine: 'smooth' builds ONE B-spline spine through the rail points and places the
// profile at the rail start — REQUIRED for rails that sample a smooth curve
// (helix(...), threads, organic paths). A polyline spine on a dense smooth rail
// produces per-segment tubes that do not sew and fail the watertight export verify.
// spine: 'helix' (rails from helix(...) only) sweeps along the EXACT helix by
// screw motion — the mode for threads, worms and helical grooves. The profile is
// read in the AXIAL plane through the rail start: profile x = radial offset from
// the helix point (+ = away from the axis), profile y = offset along the axis. A
// 60° ISO V thread section sweeps to a valid solid. Keep the profile's axial
// extent below the pitch and off the axis, or lowering fails with
// feature.invalid-args. frenet / transitionMode do nothing here.
// frenet: true rotates the profile with the rail curvature.
// section: 'transverse' (spine 'helix' only): the profile is the cross-section
// PERPENDICULAR to the axis, sketch origin on the axis — a twisted extrusion that
// turns once per pitch (how threadedRod builds a whole rod without a boolean).
.sweep(rail: [number, number, number][], opts?: { frenet?: boolean; transitionMode?: 'right' | 'transformed' | 'round'; spine?: 'polyline' | 'smooth' | 'helix'; section?: 'axial' | 'transverse' }): Shape
// Loft through one or more additional sections to produce a 3D solid.
// Use for nozzles (round-to-square), wings, fairings, transition pieces,
// twisted blades and staggered stacks.
// opts.spacing stacks sections on XY planes at z = 0, spacing, 2*spacing, ...;
// of the rotation controls only twistDeg/twistCenter apply there (per-plane
// rotationDeg requires opts.planes). opts.planes overrides with explicit
// per-section placement and each entry may add rotationDeg. Unknown option
// keys (top-level or per-plane) throw feature.invalid-args.
.loft(other: Sketch | Sketch[], opts?: {
spacing?: Editable<number>;
planes?: { plane: 'XY' | 'YZ' | 'XZ'; origin: [Editable<number>, Editable<number>, Editable<number>]; rotationDeg?: Editable<number> }[];
twistDeg?: Editable<number>;
twistCenter?: [Editable<number>, Editable<number>];
ruled?: boolean;
startPoint?: [Editable<number>, Editable<number>, Editable<number>];
endPoint?: [Editable<number>, Editable<number>, Editable<number>];
faceLabels?: Record<string, CanonicalFace | FaceQuery>;
rails?: Curve3D[];
}): Shape
// Reflect this sketch's path across an axis, returning a new Sketch.
// 'x' negates y-coords; 'y' negates x-coords; { axis, offset } reflects across a parallel axis.
// Arc winding is inverted automatically. Labels are preserved. Reflection stays
// symbolic: ParamRef coords (and a ParamRef offset) reflect to ParamRef
// expressions, so the reflected sketch tracks param edits.
.reflect(axis: 'x' | 'y' | { axis: 'x' | 'y'; offset: Editable<number> }): Sketch
```
**Twisted blades / staggered sections.** Two ways to twist a loft. `twistDeg` distributes an even total twist across the sections — first section at 0°, last section at `twistDeg`. A per-plane `planes[].rotationDeg` overrides the distributed angle for that section only (`rotationDeg ?? twistDeg * i / (N - 1)`). Both rotate in section space about `twistCenter` (default `[0, 0]` — the sketch-local origin of each section, NOT the 3D plane `origin` you passed in `planes`), which is how a turbine blade profile is rotated about its stacking axis. In non-rail lofts, sections built from NURBS path segments (`.spline` / `.nurbsSegment` / `.hermiteG2`) honor `plane`, `origin`, and rotation exactly like ordinary sections. The twist option name differs by operation: extrude twists with `twistAngle`, loft with `twistDeg`.
```ts
// Twisted blade: two sections, the tip rotated 30° about its stacking center.
const root = path().moveTo(-6, -1).lineTo(6, -1).lineTo(6, 1).lineTo(-6, 1).close();
const tip = path().moveTo(-3, -0.5).lineTo(3, -0.5).lineTo(3, 0.5).lineTo(-3, 0.5).close();
const blade = root.loft(tip, {
planes: [
{ plane: 'XY', origin: [0, 0, 0], rotationDeg: 0 },
{ plane: 'XY', origin: [0, 0, 40], rotationDeg: 30 },
],
twistCenter: [0, 0],
});
```
**Rail-guided loft limitation.** `opts.rails` currently neither supports section rotation (any non-zero `twistDeg` or `rotationDeg` is rejected with a `feature.invalid-args` lowering error) nor applies `planes[].origin` to NURBS sections — use line/arc section sketches or drop the rails if you need either.
### PathBuilder methods
`path()` returns a `PathBuilder`. Chain these calls; finish with `.close()` to get a `Sketch`.
```typescript
.moveTo(x: Editable<number>, y: Editable<number>): PathBuilder // Required first call — sets the start point.
.lineTo(x: Editable<number>, y: Editable<number>): PathBuilder // Straight segment to (x, y).
.tangentArc(x: Editable<number>, y: Editable<number>): PathBuilder // Arc continuing tangent from prior segment.
.threePointsArc(x: Editable<number>, y: Editable<number>, midX: Editable<number>, midY: Editable<number>): PathBuilder // Arc through start, mid, end.
.sagittaArc(x: Editable<number>, y: Editable<number>, sagitta: Editable<number>): PathBuilder // Arc by chord + perpendicular bulge. Sign chooses side.
.bulgeArc(x: Editable<number>, y: Editable<number>, bulge: Editable<number>): PathBuilder // Arc by chord + DXF bulge factor (tan(angle/4)).
.radiusArc(x: Editable<number>, y: Editable<number>, radius: Editable<number>): PathBuilder // Arc by chord + explicit radius; sign chooses side.
.smoothSpline(x: Editable<number>, y: Editable<number>): PathBuilder // C1-smooth single-segment spline to (x, y); inherits start tangent.
.spline(points: Array<[Editable<number>, Editable<number>]>, opts?: { tension?: Editable<number> }): PathBuilder // (Slice D) N-waypoint B-spline interpolation through every point. See kernelcad-nurbs.
.nurbsSegment(controlPoints: Array<[Editable<number>, Editable<number>]>, opts?: { degree?: number; weights?: number[]; knots?: number[] }): PathBuilder // (Slice D) Explicit B-spline control polygon — controlPoints[0] must match current pen. See kernelcad-nurbs.
.bezierTo(c1: [Editable<number>, Editable<number>], c2: [Editable<number>, Editable<number>], end: [Editable<number>, Editable<number>]): PathBuilder // Exact cubic Bezier (SVG C) from the pen.
.quadraticTo(c: [Editable<number>, Editable<number>], end: [Editable<number>, Editable<number>]): PathBuilder // Exact quadratic Bezier (SVG Q) from the pen.
.hermiteG2(a: HermiteEndpoint2D, b: HermiteEndpoint2D): PathBuilder // (Slice D) 2D quintic-Hermite G2 transition. See kernelcad-nurbs.
.tangentCircle(entities: TangentEntity2D[], opts?: { radius?: Editable<number>; near?: [Editable<number>, Editable<number>] }): Sketch // Circle TANGENT to other geometry. 2 entities + opts.radius = sketch-fillet; 3 entities, no radius = through-three. Terminal, like circle().
.tangentLine(a: TangentEntity2D, b: TangentEntity2D, opts?: { near?: [Editable<number>, Editable<number>] }): PathBuilder // Segment along the line tangent to two circles — the belt/pulley move. Both entities must be circles.
.label(name: string): PathBuilder // Tag the prior segment for fillet/chamfer/shell by name.
.close(): Sketch // Close path; returns a Sketch.
```
`tangentCircle` / `tangentLine` take entities of the form `{ kind: 'line', from: [x,y], to: [x,y] }` (an INFINITE line; `from`->`to` sets direction) or `{ kind: 'circle', center: [x,y], radius }`, each with an optional `side: 'outside'` (default) `| 'enclosed' | 'enclosing' | 'unqualified'`. Points are not supported. These constructions have SEVERAL solutions (a radius-r circle tangent to two perpendicular lines has four, one per quadrant). `side` filters first, `opts.near: [x,y]` then picks the closest solution, and if more than one still survives the build FAILS with `sketch.tangency.ambiguous` listing every candidate rather than guessing. No such construction existing fails with `sketch.tangency.no-solution` naming the geometric reason.
Every PathBuilder coord and scalar accepts `Editable<number>` (`number | ParamRef<number>`), so symbolic params survive into capture and the dispatcher's pre-resolve substitutes them at lower time. That includes `.circle(cx, cy, r, segments?)` (each vertex is captured as `cx + r·cos θ`; `segments` stays a plain integer), `tangentCircle` / `tangentLine` entity coordinates and radii, and the `extrude` / `revolve` / `loft` / `reflect` scalars above. Capture-time checks read the param's current value, not a placeholder. Raw `sweep` rail arrays stay numeric; a rail from `helix(...)` is the parametric one. Build derived dimensions with the ParamRef arithmetic methods (`.add`, `.subtract`, `.multiply`, `.divide`, `.negate`).
### 2D text (sketch.text)
Drop a string of glyph outlines into a sketch as a single closed-region primitive. The bundled font is an industry-standard sans-serif default; pass `font: fontPath('/abs/path/to/your.ttf')` to load any TTF.
```typescript
// Engraved label: cut text into a plate.
const plate = box(80, 30, 3);
const label = sketch.text("KERNEL", { size: 12, align: 'center', position: [40, 15] }).extrude(1.5);
return plate.subtract(label.translate(0, 0, 1.5));
// Raised logo: extrude text upward as a protrusion.
const base = extrudeRect(60, 60, 2);
const logo = sketch.text("KC", { size: 20, align: 'center', position: [0, 0], rotation: 15 }).extrude(1.5);
return base.union(logo.translate(0, 0, 2));
```
Options:
- `size` (required, mm): glyph cap height.
- `align` (`'left' | 'center' | 'right'`, default `'left'`): horizontal alignment relative to `position`. Vertical alignment is always baseline.
- `position` (`[x, y]`, default `[0, 0]`): anchor point in the sketch's local plane.
- `rotation` (degrees CCW, default `0`): rotation around `position`.
- `font` (default = bundled): logical name (must be previously loaded) OR `fontPath('/abs/file.ttf')`.
Returns a single `Sketch` covering the whole string — chain `.extrude(depth)` to land 3D text.
### Constrained sketches (v0.4 MCP)
The script `path()` API remains the way to author production geometry. The v0.4 constrained-sketch MCP tools are for side-effect-free sketch solving and agent discovery: pass explicit `POINT`, `LINE`, and `CIRCLE` entity records plus a constraint list, then use the returned coordinates to author or adjust a script.
Supported constraint types:
`COINCIDENT`, `DISTANCE`, `HORIZONTAL`, `VERTICAL`, `PARALLEL`, `PERPENDICULAR`, `EQUAL_LENGTH`, `TANGENT`, `RADIUS`, `ANGLE`, `CONCENTRIC`, `SYMMETRIC`
Minimal tool flow:
- `inspect({ of: 'constraints', constraints? })` — discover the supported types and echo the current constraint list.
- `add_constraint({ constraints?, constraint })` — validate one constraint and return a new list; no session state is mutated.
- `solve_sketch({ entities, constraints })` — solve a 2D constraint set and return `{ ok, entities, constraints }` or validation errors; no script is modified.
Entity and selection recovery:
- If a constraint references a missing id, list the entity ids you are passing and fix the `entities` array before solving.
- If a `LINE` references non-POINT endpoints or a `CIRCLE` references a non-POINT center, replace the referenced id with a `POINT`.
- If a constraint reports the wrong entity count, check the type arity: most types use 2 entities; `RADIUS` uses 1, `ANGLE` uses 1 or 2, and `SYMMETRIC` uses 3.
- If `DISTANCE`, `RADIUS`, or `ANGLE` reports a missing value, add a numeric `value`.
- If the type is unsupported, call `inspect({ of: 'constraints' })` or `lookup_api({})` and choose one of the supported types above.
## Threads
ISO metric threads.
- Designations: `'M6'` = coarse pitch from the ISO 261 table (M1–M64); `'M6x0.75'` sets the pitch and works for any size (`'M70x4'` jar neck). `isoThread(spec)` returns the numbers (pitch, minor/pitch diameter, tap drill) for callouts and clearance holes.
- External thread: `threadedRod({ spec, length })` — real 60° profile along +Z from z = 0, one helical sweep, valid solid, STEP-exportable. `chamfer: true` for 45° ends (one more boolean), `cosmetic: true` for an instant major-diameter cylinder with the spec in metadata, `clearance` (≤ pitch/10) for printed fits. Union a head for a bolt; union onto a body for a neck.
- Internal thread: `body.threadedHole(face, { u, v, spec, depth })` — same frame and refs as `hole`. Default cosmetic (minor-diameter bore + recorded thread, fast); `modeled: true` cuts the real thread (seconds), with `clearance` (≤ pitch/8) of play.
- Mating in phase: the rod's ridge crosses +X at every z = k·pitch and a modelled hole's groove crosses its face u-axis at the entry face, so put the hole's entry face on a whole pitch of the rod (e.g. lift the nut by k·P). Verify with `verify({ check: 'dfm' })` / interference — a cosmetic hole on a modelled rod reports interfering, as it should.
- Example: `examples/threads/threaded-jar-and-lid.kcad.ts` (M64 × 4 neck + tapped lid).
## Labels — naming faces at creation time
Declare a label on a creating op via the `faceLabels` option. The value map accepts two kinds of entries:
**Canonical alias** — give a custom name to a canonical face:
```typescript
box([10, 10, 5], { faceLabels: { lid: 'top', base: 'bottom' } })
.fillet(2, { face: 'lid' });
```
**Query-based label** — name a face that has no canonical name, via `FaceQuery`:
```typescript
extrudeRect(20, 10, 5, { faceLabels: { rim: { atZ: 5, parallelTo: 'XY' } } })
.shell(1, { face: 'rim' });
```
Labels survive transforms (`.translate`, `.rotate`, `.scale`, `.reflect`, `.mirror`) and unambiguous booleans (`.subtract`, `.union`, `.intersect`) — the same lineage rules as canonical face refs. Splitting booleans emit `feature.face-ref.ambiguous-after-split`.
`sphere` does not accept `faceLabels` (no canonical face names; query targets undefined). Use a different primitive if labels are needed.
Discover labels on a script with the `inspect({ of: 'face-labels' })` MCP tool — it surfaces both `faceLabels`-declared labels (creating-op metadata) and sketch-segment labels (`path().label('rim')`).
## When something fails
Every kernel-running tool returns `diagnostics[]`. Each entry has `code`,
`message`, `severity`, optional `feature_id`, and a `hint` — a one-sentence,
imperative recovery instruction. Read the `hint` first; if your feature
failed because an upstream feature failed (`code` is
`recompute.input.missing`), call `why_did_this_fail` to walk the chain
and find the root cause.
The full code catalogue is enumerated by the
`lookup_diagnostics` MCP tool. Call it once at session start if you
want to pre-populate retry strategies.
### Recurring failure classes
These root causes account for most non-converging repairs; fix the cause, not the symptom.
- **No-op booleans** — a `subtract`/`cutout` whose tool never intersects the body reports `feature.subtractive-noop`. Check the tool's coordinates and axis against the body before retrying.
- **Non-finite numeric arguments** — every numeric argument must be a finite number or a numeric `ParamRef`; `NaN`/`undefined` fails capture with `feature.invalid-args`. Derive values with ParamRef methods rather than uninitialized variables.
- **Open or degenerate sketches** — `extrude`/`revolve` need a single closed loop: close the path and remove zero-length segments or duplicate points, or the kernel fails with a sketch-construction error.
- **Undeclared connectors** — declare a connector on the part before referencing it in a mate, and give it a finite `[x, y, z]` origin.
- **Over-tight fillets/chamfers** — keep the radius below half the local wall/edge thickness; larger radii fail in the kernel.
- **Members run into each other or float** — `union.member-overlap` / `union.disconnected` on a frame or tray: cut cross members to `span - 2 * railWidth` and place them at the rail width, keep end members inside the frame, and seat sheets on the tubes (see Multi-member fabrications).
- **ParamRef arithmetic** — never use JS operators on a `param()` result; use `.add/.subtract/.multiply/.divide/.negate` (see the Cookbook entry for worked examples).
## CLI Commands
```bash
# Run a script and report features + diagnostics
kernelcad evaluate path/to/script.kcad.ts
# Same, but JSON output (machine-readable)
kernelcad evaluate path/to/script.kcad.ts --json
# Export to a file in the chosen format
kernelcad export stl path/to/script.kcad.ts -o /tmp/out.stl
kernelcad export step path/to/script.kcad.ts -o /tmp/out.step
kernelcad export dxf path/to/script.kcad.ts -o /tmp/out.dxf # flat part / cut list / sheet metal; laser / waterjet / CNC
kernelcad export dxf path/to/script.kcad.ts -o /tmp/plan.dxf --options '{"section":{"axis":"z","at":25}}' # cross-section of any part at z = 25
kernelcad export 3mf path/to/script.kcad.ts -o /tmp/out.3mf # slicer mesh with per-part colors
kernelcad export glb path/to/script.kcad.ts -o /tmp/out.glb # web / AR viewer; PBR materials
kernelcad export stl path/to/script.kcad.ts --part lever -o /tmp/lever.stl # one solved-assembly part (world-frame); repeat --part with -o <dir> for a subset
kernelcad export stl path/to/script.kcad.ts --parts all -o /tmp/parts/ # every assembly part as <dir>/<part>.stl
kernelcad export stl path/to/script.kcad.ts -o /tmp/out.stl --no-verify # skip the default-on watertight verify gate
kernelcad parts path/to/script.kcad.ts --json # list assembly parts with bbox, volume, surface area, triangle count
# Render a 4-view PNG (front/right/top/iso) for visual review.
# Tiles are framed to the requested --width/--height: the camera fits the
# visible geometry to the output aspect and the capture is center-cropped,
# so the default 1024×1024 square tiles frame correctly.
kernelcad render path/to/script.kcad.ts -o /tmp/out.png
# All seven views (adds back, left, bottom) in one labelled inspection sheet.
kernelcad render path/to/script.kcad.ts -o /tmp/sheet.png --sheet
# Clip the model with a section plane to expose interiors in headless captures.
# Keeps the negative-axis side by default; --section-flip keeps the positive side.
kernelcad render path/to/script.kcad.ts -o /tmp/cut.png --section z=10
# Share-ready studio hero shot (same preset as render_preview preset: 'publish'):
# one 3/4 PNG, 1600×1200 by default (--width/--height up to 2048). --background
# white|light|dark|black|transparent|#rrggbb and --no-shadow apply only with
# --preset publish; --separate / --pose render those views in the publish look.
kernelcad render path/to/script.kcad.ts --preset publish -o /tmp/hero.png
kernelcad render path/to/script.kcad.ts --preset publish --background transparent --no-shadow -o /tmp/hero.png
# Interrogate an external STEP file before placement: solid tree, per-solid
# exact bbox + volume, cylindrical holes (axis, diameter, depth, blind/through)
kernelcad inspect step path/to/part.step
# Rebuild an editable feature-tree script from an STL / OBJ / 3MF mesh, verified
# against the mesh (volume IoU + surface deviation; --strict exits 1 unless faithful)
kernelcad reconstruct path/to/part.stl -o /tmp/part.kcad.ts
# Detect BREP interferences between Scene parts (industry-standard clash check)
kernelcad interference path/to/script.kcad.ts
# Validate the assembly: floating parts, orphan clusters, interferences (v0.5 MVP)
kernelcad validate path/to/script.kcad.ts
# Run the print-readiness gates declared by the script's dfmSpec()
# (exit 0 pass / 1 gate failed / 2 no dfmSpec declared; --json for the full report)
kernelcad dfm path/to/script.kcad.ts
# Capture the script's animationView({...}) timeline to MP4 (ffmpeg), verifying
# the sampled poses for part interference (exit 0 clean / 1 collisions / 2 could
# not capture). Requires a running studio dev server (npm run dev).
kernelcad animate path/to/script.kcad.ts -o /tmp/out.mp4
kernelcad animate path/to/script.kcad.ts --frames /tmp/frames # PNG sequence; no ffmpeg
kernelcad animate path/to/script.kcad.ts --no-verify # skip the pose-interference gate
kernelcad animate path/to/script.kcad.ts --hide wall,cap # hide parts in the frames (cutaway); --focus shows only the named parts (mutually exclusive; verification still runs on the full model)
# Seamless 360° turntable of the model (same engine as capture_animation turntable: true);
# .gif out → GIF, otherwise MP4; --frames keeps a transparent backdrop. Publish look by
# default; --width/--height (1080), --fps (30), --duration-ms (6000), --elevation (22),
# --background, --no-shadow, --preset default, --environment are turntable-only flags.
kernelcad animate path/to/script.kcad.ts /tmp/spin.gif --turntable --width 640 --height 640 --fps 15
# Run the MCP server (stdio transport)
kernelcad mcp
```
+ For static renders and turntables, set `PW_CDP_URL` to an already-running
+ Chrome's CDP endpoint after verifying it is available. The capture uses that
+ browser's existing context and closes its own tab on completion. An explicit
+ attachment failure is reported; it does not launch a second browser.
+
## Animation timelines (animationView)
`animationView({...})` declares a kinematic-motion timeline that
`kernelcad animate` (or the `capture_animation` MCP tool) renders to an MP4 or
a PNG frame sequence. Display only —
and the captured artifact is evidence of the motion, not a design source.
Use the keyframe-track form for a mechanism cycle: each track animates one
NUMERIC `param()` through `{ atMs, value, ease? }` keys on one shared timeline.
```typescript
const drumDeg = param('drumDeg', 0, { min: 0, max: 360 });
const swingDeg = param('swingDeg', 0, { min: 0, max: 40 });
// ... build the assembly so drumDeg / swingDeg drive its mates ...
animationView({
name: 'dispense cycle',
tracks: [
// Drum eases in to 60 deg, dwells (hold) to 1400 ms, then eases back to 0.
{ param: 'drumDeg', keys: [
{ atMs: 0, value: 0 },
{ atMs: 800, value: 60, ease: 'easeInOut' },
{ atMs: 1400, value: 60 }, // dwell: value repeated → no motion
{ atMs: 2200, value: 0, ease: 'easeInOut' },
] },
// Meter swings out while the drum dwells, then snaps closed (step).
{ param: 'swingDeg', keys: [
{ atMs: 800, value: 0 },
{ atMs: 1400, value: 35, ease: 'easeOut' },
{ atMs: 1600, value: 0, ease: 'step' }, // step: holds, then jumps at arrival
] },
],
fps: 30,
});
```
**Ease semantics.** `ease` applies to the segment ENDING at the key ("arrive
with this easing", default `linear`); the value holds (clamp) before the first
key and after the last. `step` holds the previous value until it jumps exactly
at the key. A dwell is two consecutive keys with the same value.
**Validation (throws at capture time).** Every track param must be a NUMERIC
`param()` declared earlier (`animation.param.unknown`); a param may appear in at
most one track (`animation.track.duplicate-param`); empty tracks/keys, non-finite
or negative `atMs`, **duplicate `atMs` within a track**, and unknown `ease`
throw `animation.keys.invalid`. Key values outside the param's declared min/max
clamp to the boundary with an `animation.value.clamped` warn. Multiple
`animationView()` calls keep the LAST record and warn `animation.view.shadowed`.
**Capture (`kernelcad animate <file> [out.mp4]`).** MP4 via ffmpeg by default;
`--frames <dir>` writes a PNG sequence and skips ffmpeg entirely (use when
ffmpeg is unavailable). `--fps <n>` overrides the record's fps. `--focus <names>` /
`--hide <names>` isolate parts in the rendered frames (comma-separated feature ids
or assembly part names, mutually exclusive — same semantics as `kernelcad render`;
use for cutaways like an internal drum behind a frosted wall). Visibility is
render-only and does NOT affect the pose-interference verification, which always
runs against the full model. Exit codes:
`0` = captured + verification clean (or skipped), `1` = captured but the pose
gate found collisions (the MP4/frames ARE still written as evidence), `2` =
could not capture at all (bad args, build error, no `animationView` record, an
unsolvable pose, ffmpeg missing, browser bootstrap failure). Like
`kernelcad render`, capture drives a headless browser against a **running
studio dev server** (`npm run dev`; honors `VITE_PORT` / `PW_CDP_URL`).
**Motion verification.** By default, before paying any browser/ffmpeg cost,
capture verifies the sampled poses for part interference at every keyframe time
plus each segment midpoint (`--verify-every <n>` additionally samples every
n-th frame time of the fps schedule). `verified: true` means verification ran
AND every sampled pose was collision-free using the mechanism-validity 20 mm³
threshold (touching ≠ interference; declared `solvedModel({ ignore })` pairs are
honored). Each collision is a `{ tMs, a, b, volumeMm3 }` row with an
`animation.collision` diagnostic naming the pair and time. `--no-verify` skips
the gate and sets `verified: false` (`verify_skipped` on the MCP envelope) — do
not ship a mechanism animation with `--no-verify` standing in for a clean pass.
**Studio Animation tab (live review).** The same `animationView` timeline plays
live in Studio's Inspector Animation tab — scrub or play it (with loop /
reciprocate modes and a speed control). The timeline is baked once (every frame
solved server-side into per-part transforms), then interpolated and played
client-side at full rate, so playback is smooth; on pause the kernel pose is
synced to the displayed frame so Export/Validate agree with the viewport. Live
drive needs a server-pool session (the model opened via `?script=`); the
editor-only mode previews sampled values. Offline `kernelcad animate` remains
the full-fidelity, verified MP4 capture.
<!-- OUT-OF-SCOPE:START -->
## Out of Scope
These are not available today; do not generate code that uses them. Anything not listed here is fair game — call `lookup_api` / `lookup_cookbook` before concluding kernelCAD lacks a capability.
- Asymmetric chamfer (only symmetric 45° supported) — deferred
- Param-bound (auto-updating) dimensions on `svg-drawing` — deferred; dimensions DO ship derived from the geometry (`options.autoAnnotate`: datums, hole callouts with position tolerances, positions, radii, chamfers, flatness, ISO 2768 note), authored (`options.annotations`), and as section views on any plane (`options.sections`)
- NURBS surface extend/untrim/blend, lattice/quilt — deferred (surface-surface intersection ships as `surfaceIntersection`)
- Tracked face/edge refs (only canonical refs and inline queries work) — deferred
<!-- OUT-OF-SCOPE:END -->
<!-- COOKBOOK:START -->
## Cookbook (snippet index)
When you need a canonical pattern, call MCP tool `lookup_cookbook(query, k?)` to fetch the full body of a snippet. The IDs and triggers below are the full v1 inventory; query by intent, not by ID.
| ID | Trigger |
|---|---|
| 4dof-release-evidence-pack | Prompt asks to turn a production 4-DOF open-chain arm (base yaw, shoulder, elbow, wrist, bridged yokes, named materials) into a release/evidence package with an animationView reach cycle and declared actuator capacities. This script evaluates standalone: param() sits beside animationView, and kinematic.checkStaticHold runs against body-tree revolutes (mates are not in that joint list). Prefer design_loop until evaluate_script reports mechanism=real. Follow-up MCP, not more geometry: inspect/export BOM, exploded PDF/SVG drawing, and usd-isaac with explicit drives on fastened/revolute mates only. run_fea only when CalculiX and gmsh are present — otherwise report FEA UNVERIFIED. No joint.scissorLift and no closed-loop FK. |
| assembly-connector-and-revolute-mate | The model has multiple mechanical parts (not a single fused body) that need a named pivot between them. Declare an axis connector on each part with partRef.connector(name, { type: 'axis', origin: { kind: 'vec3', value: [...] }, axis: [...] }), then join the connectors with arm.mate(name, 'partA.conn', 'partB.conn', 'revolute', { limitsDeg: [min, max] }). This is the canonical assembly-topology vocabulary for hinges, elbows, and any revolute joint — connector origins must use the tagged { kind: 'vec3', value: [...] } form, not a bare [x, y, z] array. |
| async-sketch-await | You called shape.sectionSketch(...), .faceSketch(...), or .silhouette(...) and chained a Sketch method (.extrude, .revolve, .sweep, .loft, .reflect) directly on the result. All three producers are async and return Promise<Sketch> — chaining without awaiting throws feature.async-result.missing-await instead of silently doing the wrong thing. |
| automotive-body-envelope | You need a car / van / berlinetta / fairing body that reads as a REAL vehicle, not a child-like extruded side outline with a box roof. Copy this ported hatchback (station tables measured from an open CC-BY model) and re-param it; for a different car, measure its stations from an open reference instead of free-handing. Degree-3 nurbsSegment sections lofted in short runs, greenhouse as a second loft, glass/lamps as thin separate skins, wheels in arches. |
| blind-pocket-from-top | You want a pocket cut into the top face only — the cylinder is shorter than the plate so it does not reach the bottom face. |
| bom-ready-assembly | Before calling inspect({ of 'bom' }) or export({ format 'bom-csv' \| 'bom-json' }), declare material/density on assembly.part(...) so mass rows are real numbers instead of bom.material.unassigned gaps; instances placed in a loop with distinct names group into one BOM row by real quantity. |
| chained-revolute-arm | You are building a serial robot arm (shoulder, elbow, wrist) as an assembly. Each link carries a tongue + pin at its proximal pivot and a clevis at its distal pivot (disjoint axial ranges, see revolute-joint-knuckle), so every joint is collision-free at any angle; the joint limits and link lengths keep non-adjacent links clear of each other and of the base at every combination of limits. |
| chamfer-rotated-face | You rotated a primitive and now want to chamfer one of its canonical faces by name (face-name semantics survive transforms). |
| clamshell-enclosure-release-pack | Prompt asks for a clamshell enclosure release pack: two walled shells on a clevis spine, a lidDeg swing with animationView, named materials, and a drawing/BOM/USD checklist. Declare lidDeg with param() in the same block as animationView — tracks must name that live param. The lid is modeled open, side by side with the base; limits stop it before the shells fold onto each other. A gasket-sealed close is not modeled. No latch mate, no joint.scissorLift, no closed-loop FK. After evaluate_script reports mechanism=real, export BOM and a PDF or SVG drawing, and usd-isaac with an explicit drive on the hinge only. Report FEA UNVERIFIED unless CalculiX and gmsh are present. Prefer design_loop until green. |
| clamshell-hinge-two-part-assembly | You are modeling a laptop-lid-style clamshell hinge, or any assembly where one rigid body swings about a fixed pivot line on another rigid body. Declare a matching axis connector on both bodies along the physical hinge line, then join them with a revolute mate and limitsDeg to bound the swing angle. Relevant for hinge, revolute joint, or assembly with connectors and mates. |
| clearance-hole-through-plate | You need a through-hole sized for a bolt with a small clearance margin. Use `hole(face, { diameter, depth: 'through' })` (or `holes` for a pattern), not a subtracted cylinder; for a tapped hole see `threaded-hole-tap-drill`. |
| countersunk-flat-head-screw | You need countersunk holes whose cone matches a flat-head screw so the head sits flush: hole or holes with countersink { diameter, angleDeg } cut a real cone widest at the entry face, and a revolved 90° head of the same diameter fills it with no gap on the cone and no overlap (M3–M6 ISO 10642 table). |
| diff-two-model-versions-by-material | You edited a script and need to know what physically changed, not just that something did. A signed volume delta is ambiguous — a boss that grew and a pocket that deepened report the same magnitude, and a body that only moved reports zero. Call diff_geometry({ baseFile, params }) to re-lower the SAME script with different param() values, or diff_geometry({ baseFile, file }) to compare separate scripts. Per matched body you get addedMm3 = volume(revised-base), removedMm3 = volume(base-revised), commonMm3, exact bbox/face/edge/hole-count deltas, a surface deviation, and a verdict — identical \| moved \| resized \| topology-changed. Branch on the verdict; quote the numbers. Author the model with named params and named features (as below) so both the override form and the report read cleanly. Reach for this when diff_scripts sets deeperDiffAvailable, instead of re-rendering and eyeballing. |
| dimension-callouts | You want the viewer to show a named measurement on the model, for example the spacing between two holes or a hole diameter, by declaring it with shape.dimension() in the script. Labels are names, the viewer adds the value; open the shared link with ?dims=1 to force the callouts on. |
| drawing-auto-dimension-gdt | You need a quotable engineering drawing without writing every callout. Export svg-drawing with autoAnnotate to derive datums, grouped bore callouts with position frames, positions from the datums, overall size, radii, chamfers, flatness and an ISO 2768 note from the geometry, and declare the datums or tolerances that are design intent with shape.datum and shape.tolerance. |
| drawing-hole-and-gdt-callouts | You exported a svg-drawing sheet and need standard fabrication callouts — ⌀ hole diameters with THRU/blind/counterbore/countersink, a pattern count, a fillet radius, a chamfer size, a datum letter, and a feature-control-frame tolerance — instead of only the automatic bounding-box dimensions. |
| drawing-pdf-sheet | You need a drawing to send to a shop or publish — a vector PDF on an ISO A0–A4 or ANSI A–E sheet with orthographic views, hidden lines, the automatic dimensions, hole callouts and GD&T, and a title block carrying title, part name, material, scale, units, projection, sheet size, date and revision. Export format pdf-drawing; the sheet content is the svg-drawing sheet. |
| enclosure-lid-with-screw-bosses | Prompt asks for an electronics enclosure with a removable screw-on lid/cover (not a hinged clamshell). Build a walled base with corner bosses + lid with matching clearance holes. Prefer this over `clamshell-hinge-two-part-assembly` when the user wants fasteners, not a revolute hinge. |
| engineering-material-presets-mass | You want engineering material presets by grade name (mild-steel, aluminum-6061, pla, petg, abs, nylon) to drive the mass, default finish and recorded material of identical geometry. One registry serves arm.part({ material }), inspect({ of: 'mass', material }), feaStudy({ material }) and .finish(), so the grade name works everywhere; steel / aluminum / pet are aliases for the same grades. |
| exploded-assembly-drawing | You need an exploded isometric of a multi-body assembly — bodies pulled apart along mate axes or radially from the centroid — with BOM-numbered balloons and a bill table on a svg-drawing sheet, or the same explode on render_preview. |
| export-dxf-flat-parts | You need a 2D cut file — a flat blank with holes and slots, a panel, an extruded profile, or a whole cut list of flat panels — or a section of any 3D body at one height (plans, floor plans, profiles). Export format dxf finds the blank's own plane in any orientation and writes the outline and holes with exact arcs and circles. |
| export-two-link-arm-to-usd-isaac | The consumer is a GPU physics / robot-learning stack that imports a UsdPhysics stage directly instead of parsing URDF or SDF. Author parts with a named material (it seeds both mass and appearance), axis connectors, and fastened / revolute / prismatic mates exactly as for urdf, then call export({ target: 'model', format: 'usd-isaac', output_path: 'robot.usda' }). You get an articulation root, one rigid body per link at its solved pose with mass and principal inertia, one PhysicsRevoluteJoint / PhysicsPrismaticJoint / PhysicsFixedJoint per mate with limits, UsdPreviewSurface materials, and a meshes/<link>.usda layer per link. Declare actuator gains with options.drives { <mate>: { stiffness, damping } } — none are invented. Planar, cylindrical, pin_slot and ball mates fail closed with export.usd.joint-unsupported. |
| extrude-rounded-rect-plate | You want a flat plate with rounded corners; use the dedicated rounded-rect extrude rather than building corners by hand. |
| fdm-fit-clearance-by-fit-type | Two printed parts must fit together (lid lip in a box, pin in a hole, slider in a slot, snap-fit catch). Pick the per-side clearance from the fit type as a param, size the female feature with it (hole diameter + 2c, lip width - 2c), and declare `dfmSpec({ process: 'fdm', printer, minWall, minClearance })` so every evaluate measures the real gap and the bed fit. Do not hard-code an unexplained 0.2. |
| fdm-printability-and-print-orientation | You are about to slice or send a model to an FDM printer and want to know first whether it prints cleanly — no unsupported overhangs, no over-long bridges, walls at least 2 x the nozzle, no pins or holes too small for the nozzle, enough bed contact, fits the printer bed. Declare dfmSpec({ process: 'fdm' }) and evaluate; the dfm.fdm.* diagnostics name the overhang area and locus, and the hint gives the axis-aligned rotation that minimises unsupported area ("rotate -90° about Y ... drops from 938.2 to 0.0 mm²"). Apply it by declaring that buildDirection, which the gcode export also slices in. Full report per orientation via verify({ check: 'dfm' }) or kernelcad dfm. |
| fea-study-safety-factor-gate | The design has to carry a real load and you need evidence, not a guess: peak von Mises stress, peak displacement, and a safety factor against the material's yield. Declare the study on the shape with shape.feaStudy({ material, fixed, loads, meshSize?, minSafetyFactor? }) — `fixed` and `loads[].faces` take the same FaceQuery / @kc[...] selectors as the rest of the API, and `force` is the TOTAL newtons on those faces. Declaring minSafetyFactor makes it an enforcement gate: evaluate_script fails with fea.safety-factor.below-min and names the governing region. Then run run_fea for the full summary, per-region hot spots and heatmap PNGs. Needs the external CalculiX + gmsh toolchain; check with fea_summary({}). Use this instead of verify({ check: 'load-capacity' }) when the geometry is not a plain cantilever beam or when you need to know WHERE it is overloaded. |
| fillet-face-after-subtract | After subtracting a hole or pocket, you want to round only the rim of the resulting opening — not every edge in the part. |
| fillet-translated-shape | You translated a primitive and now want to fillet one of its canonical faces by name (canonical face refs survive translate). |
| freight-trailer-roof-solar-batteries | Prompt asks for a freight box / dry-van semi-trailer with solar-battery modules, or solar-panel-plus-battery cabinets, mounted on the roof and an optimal or balanced location for them. Pack the maximum grid that fits inside the front-fairing, rear-door, and side-walkway keep-outs, then center that grid on the kingpin/bogie static-reaction station. The rule is closed-form packing plus a reaction split — not an FEA solve and not a mass-properties query. Doors are modeled closed and fastened; they are not a swinging mechanism. |
| g2-bridge-and-intersection-curve | You need a curvature-continuous blend between adjacent 3D curves, or the exact intersection curve of crossing faces (a weld seam, a trim spine) as a Curve3D you can sweep. |
| gcode-export-and-print | You have a finished, watertight kernelCAD part and want to go straight to a physical print — slice it to G-code with a real slicer (no manual GUI step), then upload it to a network-connected printer and start the print, without a human touching a slicer or a printer's touchscreen. |
| gridfinity-bin | You need a Gridfinity-compatible storage bin: X × Y grid units on the 42 mm pitch (0.5 mm clearance), height in 7 mm units, one profiled foot per cell (0.8 / 1.8 / 2.15 mm = 4.75 mm), optional Ø6 × 2 mm magnet holes at ±13 mm, optional dividers, and a stacking lip (0.7 / 1.8 / 1.9 mm). The script throws a clear error when magnets do not fit the foot or the floor, or when the bin is too shallow for a cavity under the lip. |
| gt2-shop-release-actuator | Prompt asks for a compact manufacturable GT2 belt-driven rotary actuator, not a stack of boxes: a machined housing with cavity walls, a bearing-seat bore, floor bosses, filleted edges, and a four-hole mounting pattern; two separate 20-tooth 2 mm-pitch pulleys; a closed belt; a shaft and 608-style bearing as separate bodies; a removable cover; named materials. The belt is a geometric cord and the pulley teeth are rounded nubs — not tooth meshing, pretension, or power transmission. Do not export USD Isaac: the cylindrical shaft/bearing mate fails with export.usd.joint-unsupported. After a full evaluate_script (not dry-run), inspect BOM and export a PDF or SVG drawing. Auto-GD&T in the script is generated annotation, not a manufacturing certification. Prefer design_loop until green. |
| gt2-timing-belt-drive | You need a GT2 timing-belt drive: two toothed pulleys at a center distance, with belt length computed from pitch diameters and center distance then rounded to a real GT2 tooth count (2 mm pitch). |
| heat-set-insert-pilot | A printed part takes brass heat-set (melt-in) threaded inserts. Cut the pilot with `hole()` / `holes()` (blind, insert length + 1 mm deep, with a small countersink lead-in), sized from the insert maker's table — not the screw's tap drill and not a subtracted cylinder. Keep >= 1.6 mm of wall around the pilot. |
| hermite-g2-blend | You have a pair of existing NURBS curves whose tangents and curvatures match at the join point and you want a G2-continuous compound spine (so a downstream variableSweep does not kink at the join). Author the flanks via nurbsCurve, then drop a hermiteG2 between them with matching endpoint tangents and curvatures. |
| hex-nut-trap-bolt-clearance | A printed or machined part needs a captive hex nut under a bolt. Cut the through clearance hole with `hole()` and the hexagonal pocket with `cutout(hexPath, { face: 'bottom', depth })`, sized from the nut's across-flats (AF) + 2 x clearance and nut height + clearance. Do not subtract a cylinder or a hand-placed prism. |
| involute-spur-gear-pair | You need a meshing involute spur gear pair (module, tooth counts, 20° pressure angle, face width, bore, backlash). Use the `spurGear(...)` primitive: it generates true involute flanks, a trochoid root fillet and correct undercut as ONE manifold solid. Center distance is m(z1+z2)/2. |
| iso-metric-bolt-and-nut | You need an ISO metric hex bolt threaded into its hex nut (M3–M12) whose pitch, hex across-flats, and head height come from a params table. The bolt thread is a real 60° V profile swept with sweep(helix(...), { spine: 'helix' }); the nut's internal thread is the thread option of its bore (pitch, modeled, play), seated in phase so the two parts never interfere. A cosmetic thread (modeled: false) keeps only the minor-diameter bore for speed. |
| loft-body-shell-from-profiles | Stylized / mechanism-demo shell ONLY — recognizable printable boxy loft from polyline stations (toy car, boat hull demo, fuselage blockout). NOT for berlinetta / sports-car / organic automotive likeness. For organic car bodies use automotive-body-envelope instead (lookup_cookbook("automotive body envelope")). |
| mated-inspection-head-routed-tube-animation | Prompt asks for a production 2-DOF pan/tilt inspection head: grounded base, yaw stage, tilt yoke with cheeks and a servo shelf fused into the parent solid, two revolute mates with limits, mechanicalJoint intent, and a rigid swept service tube fastened to the moving head (NOT a flexible hose). Declare yawDeg and tiltDeg with param() in the same block as animationView — tracks must name those live params; dropping param() while keeping tracks throws animation.param.unknown. Prefer design_loop until evaluate_script reports mechanism=real, then open_in_studio and capture_animation from a file with pose-interference verification. Closed-loop FK and joint.scissorLift are unsupported — stop and say so. |
| mirror-half-part | The part is symmetric across a cardinal plane; build only one half and call mirror to produce the complete symmetric part. |
| multi-body-mechanism-real-proportions | Prompt asks for a real / complex robot arm, cobot, open-chain mechanism, or multi-body machine with joints. Bodies must read as plates, towers, yokes — not stick figures. Reuse supported-arm lessons: connectors + revolute mates with limits, jointSupport / mechanicalJoint intent, grounded base-frame, and a tendon or mechanicalJoint so gravity-hold passes. If you add animationView, its tracks must name live param() declarations in the same script — do not drop those param() calls when rewriting (animation.param.unknown). Prefer design_loop until review is green. |
| multi-dof-robot-arm-4axis | Prompt asks for a production multi-body robot arm with at least 4 revolute DOF (base yaw, shoulder, elbow, wrist), seated servos, mechanicalJoint chain, mechanism=real, optional gravity/actuator check, and animationView reach cycle. CRITICAL: fuse every yoke / servo-shelf / cheek into its parent link with .union() so each part is ONE solid load path — floating yoke fragments fail design_loop / review_cad (floating-body geometry gate + attachment-plausibility). Pitch joints are joint.clevis with the link tube seated in the tongue and the parent collar stopped just short of that disk. Yaw is a spigot in a bored collar (sub-millimetre running fit, no shared volume). Prefer design_loop until green. See also multi-body-mechanism-real-proportions and examples/robot-arm/compact- supported-arm.kcad.ts. |
| multi-feature-machined-housing | Prompt asks for a real / production / complex mechanical housing, bearing housing, gearbox-ish enclosure, or multi-feature machined part with walls, fillets, bosses, pockets, and hole patterns. Prefer this over union-of-stacked-primitives — Adam-level bar requires manufacturing-intent BREP, not a toy stack of boxes. |
| nema-motor-mounting-plate | Prompt asks for a NEMA mounting plate, stepper motor mount, or motor flange plate with the standard four-bolt pattern (±15.5 mm, Ø3.2 clearance). Prefer catalog `lib.standard.nema17()` when the parts catalog is available; this snippet uses a BREP stand-in motor so evaluate stays offline/CI-green. Declare matching `bolt-holes-N` frames on the plate (`.holes()` auto-connectors stay on the holes feature id) and mate with the 4-arg form. |
| nema-shaft-spur-drive-stack | Prompt asks for one manufacturable drive stack: a NEMA-style mounting plate, a motor stand-in, two shafts in bearing seats, and an involute spur pair under the plate. Prefer catalog nema17 and bearing608 when those parts are available; this script uses BREP stand-ins so evaluate stays offline. Spur flanks are a static geometric mesh at m(z1+z2)/2 — not tooth-contact dynamics, not coupleMates, not a gear-pair transmission. Do not animate the pair. Do not export USD Isaac: the cylindrical drive-shaft mate fails with export.usd.joint-unsupported. The idler shaft is fastened. Only one of the four bolt-hole frames is mated — four fastened mates on that pair plus the cylindrical mate are a loop the solver refuses. After a full evaluate_script, inspect BOM and export a PDF or SVG drawing. Datums are annotation, not a certification. Report FEA UNVERIFIED unless CalculiX and gmsh are present. Prefer design_loop until green. |
| network-body-panels-via-sew | You need a network-style organic body / multi-guide fairing and loft accepts at most 2 rails. Do NOT pass >2 rails to Sketch.loft — panel with surfaceFromCurves / surfaceFromBoundary, sew closed, thicken, then G2 fillet. Pair with automotive-body-envelope for car proportions and verify body-likeness before publish. |
| non-overlapping-l-bracket | You're building two perpendicular plates joined at a right angle; both plates have the same thickness; volumes must not overlap at the joint. |
| parametric-bolt-pattern-skeleton | You want a compact bolt-hole part with an editable bolt-diameter parameter that can be changed later. |
| path-hermite-g2-blend-2d | You're authoring a freeform 2D outline that should transition from one prescribed point + tangent (+ curvature) to another with G2 continuity (no visible curvature crease where adjacent neighbours meet). Drop a single .hermiteG2(a, b) call into the chain; a.point must match the current pen position. Tangent magnitude is the first derivative (typical ~ chord length, NOT unit length). |
| path-nurbs-segment-explicit | You have an explicit B-spline control polygon (programmatic generation, round-tripping from external CAD, when precise shape control beats waypoint convenience) and want a 2D path segment authored from the control net directly. The first control point must match the current pen position within 1e-6 mm; the pen ends at the last control point. |
| path-spline-organic-outline | You need a freeform 2D outline (eyewear brow, ergonomic grip silhouette, sneaker midsole) authored as a sequence of measured waypoints, and arc primitives + smoothSpline are too rigid. Drop a single .spline([...]) call into the path() chain after moveTo; the path interpolates through every waypoint at degree 3. |
| pipe-route-swept-tube | You need a pipe or tube that follows 3D waypoints, with a specified bend radius at each corner, by sweeping a circular profile along a rail whose sharp corners are replaced by sampled arcs of that radius. |
| planetary-gearbox-shop-release | Prompt asks for a shop-release planetary gearbox, not a bare gear set: a machined housing with a ring pocket, floor, mounting holes and a fillet; a carrier with planet pins; a sun shaft; a cover; named materials. Teeth stay true involutes placed so the pitch circles meet. That is a static assembled pose — not tooth-contact dynamics, backlash simulation, or torque through the flanks. Do not coupleMates, do not declare a gear-pair transmission, and do not animate rotation; the kernel will not keep teeth in mesh. Every mate in this script is fastened so usd-isaac is not blocked by a cylindrical joint. After a full evaluate_script, inspect BOM and export a PDF or SVG drawing. Datums are annotation, not a certification. Report FEA UNVERIFIED unless CalculiX and gmsh are present. Prefer design_loop until green. |
| planetary-stage-internal-ring | You need a coaxial planetary gear stage (sun + planet(s) + internal ring) with true involute teeth, not cylinders. Use `spurGear` for the sun and planets, `internalSpurGear` (aliases `ringGear` / `internalGear`) for the annulus, and `planetaryToothCompatibility({ sunTeeth, planetTeeth, ringTeeth })` so Zring = Zsun + 2·Zplanet. Carrier pins sit at centre distance m(Zsun+Zplanet)/2. |
| publish-hero-render-and-turntable | The design is done and you need pictures to share it — a gallery tile, a README image, a social post or a product page — not an engineering review. Colour every part (or give it a material) in the script, then call render_preview({ file, preset: 'publish' }) for ONE studio 'hero' PNG (3/4 front-right, soft contact shadow, white backdrop, 1600×1200; width and height up to 2048; background: 'transparent' for a PNG with alpha, or 'light' / 'dark' / '#rrggbb'). For a seamless 360° loop call capture_animation({ file, turntable: true, output_path: 'x.gif' }) (or .mp4; frames_dir keeps alpha). The camera centres and frames the silhouette itself, so do not add setCameraTarget for these shots. The output is deterministic: re-rendering the same file gives the same pixels. Use the default preset (no preset) while you still iterate on geometry. |
| rebuild-editable-model-from-mesh | You were handed an STL, OBJ or 3MF of a mostly prismatic mechanical component and need named params and real features, not the faceted solid lib.fromSTL gives you. Call mesh_to_features({ file, out }) (CLI: kernelcad reconstruct scan.stl -o rebuilt.kcad.ts). It measures the mesh, emits a script like the one below — a revolve or extruded profile, drilled and counterbored bores placed on face queries relative to the entry face centroid — then evaluates that script and compares it with the mesh. Rely on the dimensions only when fidelity.verdict is faithful; otherwise read unmatchedRegions and the ledger. Snapped values are ledger facts whose ids are the param names, so resolve_assumptions turns a correction straight into set_param overrides. |
| rebuild-model-from-drawing-pdf | You were handed a vector PDF of an engineering drawing (orthographic views, lettered dimensions, a title block) and need an editable model, not a trace of a picture. Call drawing_to_cad({ path, out }): it reads the linework and dimension text, identifies the views by projection alignment, lets stated dimensions win over drawn lengths, extrudes the silhouette of the one non-rectangular view (or revolves a turned shaft), cuts ⌀ callouts along the axis of the view they are drawn in, and writes a script like the one below plus a ledger. Branch on fidelity.verdict, then settle the open ledger facts (mirrored positions, measured values, disagreements) with resolve_assumptions before relying on the model. A scanned page fails with reference.drawing.raster-only; use trace_from_image for that. |
| repair-draft-angle | evaluate_script reported feature.draft.failed because the draft angle was geometrically degenerate (typically 90 degrees) and you want a mould-release angle derived from the lowerer, not a guess. |
| repair-emboss-over-hole | evaluate_script reported feature.emboss-text.boolean-noop because the glyph prism was parked over a void and you want the UV anchor moved onto remaining solid. |
| repair-oversized-fillet | evaluate_script reported feature.edge-feature.short-edges-skipped (every target edge is shorter than twice the radius) and you want a bounded fix instead of guessing a new radius. |
| repair-revolve-crosses-axis | evaluate_script reported feature.revolve.crosses-axis (a path x-coordinate was negative) and you want the profile clamped onto x >= 0 instead of rewriting the sketch by hand. |
| repair-shell-too-thick | evaluate_script reported feature.kernel-failed on a .shell() whose wall is thicker than half the thinnest bounding-box dimension, and you want a feasible thickness derived from that bbox. |
| repair-tangency-radius | evaluate_script reported sketch.tangency.no-solution because a tangentCircle radius was smaller than (centre-distance - r1 - r2) / 2 between existing circles, and you want the radius derived from that gap. |
| resolve-photo-trace-assumptions | You want geometry from a photo or picture. Call MCP `trace_from_image` (do not type polygon points by eye), then use this when you want to know which returned waypoints are directly measured vs guessed before you feed them into path().spline() — and how to lock down a real-world scale before committing geometry. `referenceImage(path, { plane, scale })` only overlays the photo in Studio for visual checks; it does not trace. |
| revolute-joint-knuckle | You are building the physical hinge of a revolute joint (robot-arm elbow, lid, gripper finger). Knuckles built as boxes that straddle the pivot collide at every angle. Split the pivot along the ROTATION AXIS instead: the parent's clevis cheeks and the child's tongue occupy disjoint axial ranges (a gap between them), both are round about the pivot, and a pin on the child runs through clearance bores in the cheeks. Then no angle can make them overlap, and the mechanism gates pass at every limit. |
| revolve-rectangular-profile | You want a thin cylindrical wall, ring, or tube — author the rectangular profile via path() with the inner radius as the x offset, then call .revolve() to sweep it around Z. |
| scissor-lift-closed-loop | Prompt asks for a scissor-lift / scissor-jack table with base, platform, X-pattern links, mechanism=real, and animationView raising the platform. IMPORTANT: articulated closed-loop FK (true revolute + pin_slot scissor that solves both slider ends from one lift-height DOF) is UNSUPPORTED — it emits assembly.solver.did-not-converge / kinematic.solver.unsupported-config. Do NOT invent joint.scissorLift(...) or close the loop with pin_slot. Use this open-chain production pattern: one prismatic platform DOF (+ mechanicalJoint) driven by platformZ, industrial X-links fused into base-frame at a representative mid pose, animationView raising the platform. Keep the actuator-mount fastener on solid (center pad under the actuator) so evaluate_script mechanism=real — dry-run / cookbook:evaluate alone miss joint-mesh-gap. Prefer design_loop until green. Gallery examples/gallery/scissor-lift.kcad.ts is fastened pose-only (not mechanism-driven). |
| section-sketch-gasket | You have a 3D part (an imported STEP or a boolean result) and need a 2D profile from its cross-section to extrude a gasket, spacer, or shim — or to re-dimension the section. sectionSketch returns a real Sketch whose arcs stay exact, with holes as inner loops, so one extrude reproduces the cross-section. |
| shaft-and-bearing-cylindrical-mate | Prompt asks for a shaft in a bearing, bearing seat with shaft, or rotating shaft on a deep-groove ball bearing. Prefer catalog `lib.fetchPart('shaft-d8-l50')` + `lib.standard.bearing608()` when the parts catalog is available; this snippet uses BREP stand-ins so evaluate stays offline/CI-green. Mate with the 4-arg form `arm.mate(name, 'shaft.axis', 'bearing.inner-bore', 'cylindrical')` — do not Boolean-union them into one solid. |
| sheet-metal-l-bracket-bend | Prompt asks for a folded sheet-metal L-bracket, U-channel, flange, or service panel from a flat blank. Use `sheetMetal(profile, { thickness, kFactor })` then `.bend(...)` — not a solid union of two plates — when fabrication intent is sheet + folds. After evaluate/lower, recover the blank with `flattenPattern()` (MCP `flatten_pattern` / inspect bend-table) for laser/CNC. |
| slicer-ready-3mf | You want a model to open in a desktop slicer ready to print — every assembly member a named object with its colour and material, either packed on the bed at Z=0 without overlap or kept together as one multi-colour object (an inlay), each colour on its own filament slot — instead of a bare STL or already-sliced G-code. |
| static-hold-actuator-torque-check | A revolute or prismatic joint drives a downstream mass against gravity (a robot arm shoulder, a lift stage) and you need to know whether the declared actuator torque/force is sufficient — not just whether the mechanism is collision-free. Declare actuator: { torqueNm } (revolute) or actuator: { forceN } (prismatic) on the joint, then call kinematic.checkStaticHold(arm, opts). Real mass properties come from the part's geometry + declared density/material; the worst pose across the joint's declared range is reported alongside the margin. Fires assembly.joint.static-hold.exceeded when the actuator is undersized, assembly.joint.static-hold.margin-low when it clears but under the requested safety margin. |
| stress-graded-infill-fdm | A 3D print carries a load and the user wants it strong where it matters and light everywhere else. Declare the load case with shape.feaStudy({ material, fixed, loads }) — fix the mounting holes, load the face the force acts on, force in TOTAL newtons — then export a 3MF with options.infill { fromFea: '<study name>' \| true }. The export solves the study, splits the volume into stress bands relative to yield (default < 15 % -> 10 %, 15-40 % -> 25 %, > 40 % -> 60 % gyroid; override with infill.bands [{ name, fromYield, densityPercent }]) and writes one modifier volume per dense band with its own infill density (Orca/Bambu format by default; options.slicer 'prusa' writes PrusaSlicer's format). The result carries infill { bands (volume % per band), saving (filament + time vs uniform infill at the high density), fea (peak stress, safety factor), images { heatmap, bands, cutaway } }. Requires the LOCAL CalculiX + gmsh toolchain (check with fea_summary({})); without it the export fails with fea.solver.unavailable and writes nothing. The two slicer formats are not interchangeable: pick the slicer the file opens in. The FEA treats the print as solid, so the safety factor is an upper bound. |
| subtract-then-fillet-rim | You want a parametric plate, drill a through-hole, and round the rim where the hole meets the top face. |
| surface-quality-fillet-vs-g2 | You filleted or blended a freeform panel and need numbers, not a guess: is the join G0, G1, or G2? inspect({ of: 'continuity' }) samples each shared edge for position gap, normal jump and curvature difference. inspect({ of: 'curvature' }) reports per-face Gaussian and mean curvature (sphere 1/r², cylinder Gaussian 0 and \|H\|=1/(2r)). render_preview overlay zebra / curvature / continuity is the picture of those numbers. |
| sweep-tolerance-envelope-check | A design has a param() whose real-world value varies (a printed hole that comes out oversized, a clearance gap that shrinks under tolerance) and you need to know whether the mechanism stays buildable across that range, not just at the nominal value. Call kinematic.sweepTolerance({ code\|file, params, gates }) with one or more param names as { values: [...] } or { min, max, steps }; it re-evaluates the script per cartesian-product combo (capped at 64) and runs the interference / mounting-hole / joint-axis gates (default on) plus reachability when declared, returning a pass/fail table and the first failing combo per gate. |
| sweep-tolerance-hole-diameter | A fastened pair of parts has a param()-driven bore diameter and you need to know at which value the mounting-hole gate flips from pass to fail, rather than only checking the nominal. |
| tab-slot-flush-joint | You are joining flat stock (laser/CNC plywood, acrylic, sheet) with an interlocking tab-and-slot. The through-tab must span the full mating wall thickness — flush or slightly proud, never recessed — and the fit clearance belongs on the slot, not on the tab. |
| threaded-hole-tap-drill | The request names a metric screw size (M3, M4, M5, M6...) or says threaded, tapped, tap drill or internal thread. Use `shape.hole(face, { diameter: <nominal>, thread: { pitch } })` / `holes(...)`: the kernel drills the ISO 68-1 minor diameter (d - 1.0825*P, the tap drill) and records the thread for drawings and BOM. Do NOT subtract a plain cylinder of a guessed tap size. `modeled: true` cuts the real helical groove (slow; for printed threads). |
| tslot-extrusion-and-bracket | You need a 20×20 B-type T-slot extrusion (slot 6, 6 mm slot opening) trimmed to a length param, plus a matching 90-degree corner connector that fastens on the T-slots. |
| tslot-frame-fastener-bom | Prompt asks for a T-slot frame corner as a manufacturable stack: two rails, a gusset on the slot faces, and cap screws with named materials so a BOM has mass. Screws are cylinder-head stand-ins with clearance shanks, not a swept ISO V-thread — use the metric bolt snippet when the prompt needs that thread. T-nuts are not modeled; each shank stops in the slot void. After a full evaluate_script, inspect BOM and export a PDF or SVG drawing. Report FEA UNVERIFIED unless CalculiX and gmsh are present. |
| tube-bend-table-line-report | You need the data a tube bender or a weight and flow estimate works from: corner points, length / rotation / angle rows, cut length, mass and a pressure-drop estimate. Give the tube a density (or a known material) and a flow, evaluate, and read tubes.lines in the result; export bend-table writes one CSV per tube. The ends here are plain points in free space; an end that sits on an assembly part also names it, { point, axis, part }. |
| tube-between-two-ports | You need one rigid tube from a fitting on one part to a fitting on another (a hydraulic, fuel, oil or coolant line). Declare each fitting as an axis connector with port data, pass part.port(name) as the tube ends, and give the corner points in between. The first corner goes on the first port's axis and the last corner on the second port's axis. |
| tube-branch-off-another-tube | One line has to feed two places: two tubes from the same port overlap and fail tube.clearance.overlap. Route one tube from the port (the trunk) and start the second on it with from: { tube, at, axis }. at is the distance along the trunk centreline from its from end, through straights and bends; axis is square to the trunk leg. The branch is cut to the trunk wall and the trunk is not drilled. |
| tube-clearance-between-fluid-lines | Two or more tubes of different fluid systems run close together and the systems must keep a set distance apart (fuel from hydraulic, fuel from bleed air). Name the system on each port, declare the distance in tubeSpec({ clearance: { pairs } }), and run the lines side by side at a pitch that keeps the gap. The gate measures surface to surface and names both lines when the gap is too small. |
| twisted-tapered-thin-wall-vase | You need a hollow, open-top vessel whose cross-section twists and tapers with height (a twisted hex vase, a faceted planter, a lamp shade) with an even wall. Build the outer body with variableSweep through per-station profiles, then hollow it by subtracting the same sweep built from profiles inset by the wall thickness — a boolean hollow that works where shell() cannot close the offset (curved faces that meet at sharp edges: multi-station sweeps, ruled lofts, unions of stacked lofts). |
| typed-params-boolean-choice-string | A `.kcad.ts` script needs an editable value that isn't a plain number: a feature on/off switch (`param('HasLid', true)`), a closed set of named options like a fastener size (`param('Screw', 'M4', { choices: [...] })`), or free text such as a nameplate/label (`param('Label', 'KCAD', { maxLength })`). All three resolve eagerly — read `.value` in script logic (`if`, object-key lookup, `sketch.text`) instead of the numeric ParamRef's symbolic `.add()`/`.multiply()` chain. |
| union-of-stacked-primitives | Simple blockouts only — compose primitives by translate+union without overlap. NOT for real / production / complex / enclosure / gearbox / bearing housing / robot-arm prompts (use multi-feature-machined-housing or multi-body-mechanism-real-proportions instead). |
| welded-tube-frame-tray | A frame, rack, tray, table base or weldment built from tube / bar members, optionally with a sheet or deck on top. Members are cut to fit BETWEEN each other (butt joints) so they touch but never overlap, and the sheet sits on the tubes. Use one union() when the frame is one welded body; use assembly().part() per member when members are separate BOM lines. |
| wood-joinery-dado-rabbet-mortise | You are cutting a dado groove, a rabbet rebate, and a mortise-and-tenon in lumber, with a named fit-clearance param widening the receiving cuts (groove, rebate, mortise) while the male tenon stays nominal. |
| wrap-texture-can-label | You need a bitmap texture (label, decal, logo) wrapped onto a cylinder without hand-authoring UVs. Call `shape.wrapTexture(imageRef, { type: 'cylinder', axis })` so UVs are projected from final world-space vertices; `{ type: 'flat' \| 'sphere' \| 'box' }` cover planar and other wraps. |
<!-- COOKBOOK:END -->
## Conventions
- Always declare params at the top of the script with units; the kernel evaluates them and surfaces them as live sliders to the studio.
- Never use JS arithmetic on a `param()` result — `param('w', 18) + 4` coerces the branded ParamRef and throws `feature.invalid-args`. Build derived dimensions with the ParamRef methods: `.add(n)`, `.subtract(n)`, `.multiply(n)`, `.divide(n)`, `.negate()` — e.g. `w.add(4)`; they re-evaluate when the param changes.
- Prefer `hole` / `holes` / `cutout` over `subtract(cylinder)` (see the table below): they emit named refs (`'wall'`, `'floor'`, `'wall-back'`, `'counterbore-*'`, `'countersink-cone'`) that `.fillet()` / `.shell()` can address.
- Apply transforms AFTER edge/face features when face refs matter.
- - Always `return` a single shape from the top of the script. Only the returned shape is exported, probed and measured; "the last thing I created" is NOT a fallback. If a probe reports the same bbox no matter what you edit, check what the script returns.
+ - Always `return` one shape at script top level. Only it is exported, probed and measured; the last created shape is NOT a fallback. If edits leave a probe's bbox unchanged, check the returned shape.
- Symmetric parts: `.mirror(plane)` (source + reflection); `.reflect(plane)` gives the reflection only.
- In booleans, prefer ≥0.1 mm of overlap (unions) or offset (cuts) over exact tangency or coincident faces.
- Helical features: rail from `helix(...)`. Threads, worms, helical grooves: sweep with `spine: 'helix'`. Round-wire coils and springs: `spine: 'smooth'` (the default polyline spine is not watertight). Internal threads: `hole({ thread })`, not a sweep.
## Manufacturing intent → API
Intent words → call this API; do not hand-build it (subtracted cylinders, typed involute or polygon points, unioned plates). Recipe: `lookup_cookbook(<id>)`.
| Intent words | API | Cookbook id |
|---|---|---|
| bolt hole, clearance hole, M3–M6 | `hole(face, { diameter, depth })`, `holes` | clearance-hole-through-plate |
| thread, tapped, tap drill | `hole(face, { diameter: <nominal>, thread: { pitch } })` | threaded-hole-tap-drill |
| counterbore, countersink | `hole` + `counterbore` / `countersink` option | countersunk-flat-head-screw |
| heat-set insert | blind `hole` at the insert pilot Ø | heat-set-insert-pilot |
| nut trap, captive nut | `hole` + `cutout(hexPath, { face, depth })` | hex-nut-trap-bolt-clearance |
| gear, involute, module | `spurGear({ module, teeth, faceWidth, backlash })`, `ringGear` | involute-spur-gear-pair |
| sheet metal, bend, flat pattern | `sheetMetal(profile, { thickness, kFactor }).bend(...)`, MCP `flatten_pattern` | sheet-metal-l-bracket-bend |
| fit, clearance, tolerance, printer | clearance param + `dfmSpec({ process: 'fdm', printer, minClearance })` | fdm-fit-clearance-by-fit-type |
| photo, trace, reference image | MCP `trace_from_image` (`referenceImage` only overlays) | resolve-photo-trace-assumptions |
| infill, weak spots, stronger, will it hold, stress, lightweight print | `feaStudy` + `export` 3mf `infill: { fromFea }` (local ccx + gmsh) | stress-graded-infill-fdm |
| screw, nut, bearing, motor | `lib.standard.*`, `lib.findPart`, MCP `find_part` | kernelcad-parts skill |
## Interlocking joinery (flat-pack / laser / CNC)
Tab-and-slot and finger joints in flat stock follow a fixed discipline; getting it backwards produces joints that read as empty slots and carry no load.
- **Through-tabs sit flush or slightly proud — never recessed.** Model the tab to span the FULL thickness of the mating wall; add 0.2–0.5 mm of proud allowance when the face will be sanded or flush-trimmed after assembly. A tab whose end face stops short of the mating surface — even by 0.2 mm — looks like an empty slot and leaves the glue/bearing area undersized.
- **Fit clearance goes on the slot/pocket, not the tab.** Keep the tab at nominal width and thickness; widen the slot by a per-side clearance. Shortening or thinning the tab destroys both the flush face and the joint's reference geometry.
- **Clearance is process-dependent — encode it as a named param (`tabFit`)** so it can be retuned per machine and material without touching geometry:
- Laser: the beam removes a kerf (~0.1–0.3 mm depending on material and thickness) that already loosens nominal-drawn joints by roughly one kerf width; `tabFit` of 0–0.1 mm per side usually yields a snug press fit.
- CNC router: `tabFit` 0.1–0.25 mm per side, plus dog-bone / T-bone corner relief at internal slot corners (relief radius ≥ endmill radius) so square tab corners can seat fully.
- Mating 3D-printed parts: `tabFit` 0.15–0.3 mm per side.
- **Finger joints (box joints):** finger width 1–2× material thickness; finger depth equals the mating wall thickness exactly (plus the same proud allowance) so finger ends finish flush with the outer face; prefer an odd finger count for a symmetric edge.
- Canonical pattern: cookbook snippet `tab-slot-flush-joint` — `lookup_cookbook("tab and slot flush joint")`.
## Hardware, belts, and wood joinery
These are cookbook recipes over existing primitives — call `lookup_cookbook`, do not invent a generator tool.
- Involute spur gear pair (module, tooth counts, 20° pressure angle; center distance `m(z1+z2)/2`) — `lookup_cookbook("involute spur gear pair")`
- ISO metric hex bolt threaded into its nut, M3–M12 table: 60° V-thread via `helix` + `sweep` with `spine: 'helix'`, nut thread via `hole({ thread })` — `lookup_cookbook("ISO metric hex bolt and nut")`
- Countersunk holes seating ISO 10642 flat-head screws flush (`countersink` on `hole` / `holes`) — `lookup_cookbook("countersunk hole flat head screw flush")`
- 20×20 B-type T-slot extrusion, slot 6, plus a corner connector — `lookup_cookbook("20x20 T-slot extrusion slot 6")`
- GT2 timing-belt drive (belt length from pitch diameters, rounded to a 2 mm tooth count) — `lookup_cookbook("GT2 timing belt drive")`
- Wood dado, rabbet, and mortise-and-tenon with fit clearance — `lookup_cookbook("wood dado rabbet mortise")`
- Pipe route through 3D waypoints with a bend radius — `lookup_cookbook("pipe route bend radius")`
- Engineering material grades (`mild-steel` / `aluminum-6061` / `pla` / `petg` / `abs` / `nylon`; aliases `steel` / `aluminum` / `pet`) — one name drives mass, finish, the recorded part material and FEA — `lookup_cookbook("mild-steel aluminum-6061 nylon material mass")`
- Shop-release GT2 rotary actuator (housing + pulleys + shaft/bearing + cover, one script) — `lookup_cookbook("shop-release GT2 rotary actuator")`. Cord and rounded nubs are not tooth meshing; do not USD-export the cylindrical mate.
- Pan/tilt inspection head with a rigid routed service tube and `animationView` — `lookup_cookbook("pan tilt inspection head routed service tube")`. Keep `param()` beside the tracks. The tube is rigid.
- 4-DOF arm release/evidence pack (materials, reach animation, static hold, then BOM/drawing/USD/FEA checklist) — `lookup_cookbook("four DOF release evidence pack")`. Report FEA UNVERIFIED when the solver is absent.
- Shop-release planetary gearbox (machined housing, carrier, cover, named materials, fastened pitch pose) — `lookup_cookbook("shop-release planetary gearbox stage")`. Do not animate tooth mesh or declare a gear-pair transmission.
- Clamshell enclosure release pack (walled shells, clevis spine, `lidDeg` + `animationView`, BOM/drawing/USD checklist) — `lookup_cookbook("clamshell enclosure release pack")`. Keep `param()` beside the track. The gasket-sealed close is outside the limits.
- NEMA shaft/bearing/spur drive stack (plate, motor stand-in, two bearings, involute pair) — `lookup_cookbook("NEMA shaft spur drive stack")`. Geometric mesh only; do not USD-export the cylindrical mates.
- T-slot frame corner with gusset and cap-screw BOM — `lookup_cookbook("T-slot frame corner fastener BOM")`. Shanks are clearance stand-ins, not a swept V-thread.
## Sample
### Parametric bracket with hole
```typescript
const w = param('width', 60, { min: 30, max: 200 });
const h = param('height', 40, { min: 20, max: 120 });
const t = param('thickness', 5, { min: 2, max: 15 });
const holeRadius = param('holeRadius', 4, { min: 1.5, max: 10 });
const base = box(w, h, t);
const hole = cylinder(8, holeRadius).translate(30, 20, -1);
return base.subtract(hole);
```
### Sketch builder + extrude pipeline
```typescript
// Arbitrary 2D profile via path builder, then extruded.
const depth = param('depth', 10, { min: 1, max: 40 });
const profile = path()
.moveTo(0, 0)
.lineTo(30, 0)
.lineTo(30, 20)
.sagittaArc(15, 30, 5)
.lineTo(0, 20)
.close();
return profile.extrude(depth);
```
### Variable-radius blend
```typescript
// Different fillet radii on different edge selections.
const body = box(40, 30, 15);
const topEdges = await selectEdges(body, { face: 'top' });
const bottomEdges = await selectEdges(body, { face: 'bottom' });
return body.fillet([
{ edges: topEdges, radius: 5 },
{ edges: bottomEdges, radius: 1 },
]);
```
### Mirror — symmetric part
```typescript
// Build one half and mirror across the YZ plane.
const half = box(20, 40, 10)
.subtract(cylinder(10, 6).translate(10, 20, -1))
.fillet(2);
return half.mirror('yz');
```
### Surfaces → solid (Slice E finishing ops)
Two Coons patches trimmed to a shared edge, sewn into a closed solid, drafted for mold release, then exported as STEP.
```typescript
// Build two complementary 4-boundary patches that share the bottom edge.
const sharedBottom = nurbsCurve([[0, 0, 0], [25, 0, 1], [50, 0, 0]]);
const right = nurbsCurve([[50, 0, 0], [50, 12, 0.5], [50, 25, 0]]);
const top = nurbsCurve([[50, 25, 0], [25, 25, 1], [0, 25, 0]]);
const left = nurbsCurve([[0, 25, 0], [0, 12, 0.5], [0, 0, 0]]);
const bottom2 = nurbsCurve([[0, 0, 0], [25, 0, -1], [50, 0, 0]]);
const right2 = nurbsCurve([[50, 0, 0], [50, 12, -0.5], [50, 25, 0]]);
const top2 = nurbsCurve([[50, 25, 0], [25, 25, -1], [0, 25, 0]]);
const left2 = nurbsCurve([[0, 25, 0], [0, 12, -0.5], [0, 0, 0]]);
const patchA = surfaceFromBoundary([sharedBottom, right, top, left]);
const patchB = surfaceFromBoundary([bottom2, right2, top2, left2]);
// Trim each patch at their shared boundary curve so the sewn edges align.
const trimmedA = patchA.trimTo(sharedBottom);
const trimmedB = patchB.trimTo(sharedBottom);
// Sew into a closed solid (requireClosed catches authoring mistakes early).
const solid = sew([trimmedA, trimmedB], { requireClosed: true });
// Apply draft to one face for mold release.
const molded = solid.draft(3, { face: 'top' });
return molded;
```
Export as STEP after verifying:
```bash
kernelcad evaluate surfaces-to-solid.kcad.ts # exits 0, no open-shell diagnostic
kernelcad export step surfaces-to-solid.kcad.ts -o out.step
```
## Verification gates
After authoring, run before reporting done:
| Gate | Pass criterion |
|------|----------------|
| G-eval | `kernelcad evaluate <script>` exits 0, zero diagnostics |
| G-return | Script returns a single `Shape` or `Scene` (not undefined, not an array) |
| G-no-overlap | `kernelcad interference <script>` reports zero overlapping volumes |
| G-no-floaters | Every named part appears at the intended position in `kernelcad render` output — nothing hovers in empty space |
| G-conventions | Units mm + degrees, Z-up, all transforms after edge/face features when face-ref names matter |
For visual / reference-driven tasks the gate set extends — see `kernelcad-from-reference`.
## DFM gates (print readiness)
`dfmSpec({...})` declares printability gates in the script; the check engine
enforces them at evaluate time. Three gates ship: **part-pair clearance**
(exact BREP minimum distance), **minimum wall thickness** (inward ray sampling
over the export-grade mesh), and **void/channel topology** (voxel flood-fill:
undeclared sealed voids + channel mouth counting).
```typescript
dfmSpec({
minWall: 0.8, // thinnest wall the DESIGN intends to print
minClearance: 0.45, // fit gap between distinct parts
includeArticulatedMates: true, // moving links must also clear at the rest pose
ignore: [['lid', 'hinge-pin']], // design-intent contact: clearance-exempt pair
exclude: ['servo-*', 'pcb'], // not printed: skips minWall + voids
channels: [
{ part: 'base', name: 'cable-duct', openings: 2 }, // through-channel
{ part: 'float', name: 'air-pocket', openings: 0, sealed: true }, // intentional sealed void
],
});
```
Semantics that matter when authoring the declaration:
- **Opt-in, then always-on.** Scripts without a `dfmSpec` are untouched. Once a
record is present, EVERY `evaluate` / `evaluate_script` run enforces the
gates — there is no flag to skip them. A failing gate exits 1 with
error-severity `dfm.*` diagnostics.
- **Moving mates are opt-in.** By default, all declared mate pairs are exempt
from the clearance check so seated/fastened interfaces do not create false
failures. Set `includeArticulatedMates: true` on a mechanism to measure every
non-fastened mate pair at its declared rest pose. Fastened mates remain
exempt because their physical contact is checked by the mechanical
plausibility gate. This is a rest-pose screen, not a swept-motion proof.
- **Malformed declarations THROW at capture** (`feature.invalid-args`
KernelError) instead of stashing warnings — dfmSpec is an enforcement gate,
and a silently-disabled gate is worse than a build failure. At least one of
`minWall` / `minClearance` / `channels` is required; `sealed: true` requires
`openings: 0` (and vice versa); `exclude` globs are trailing-`*` prefix only.
- **`exclude` and `ignore` are different exemptions.** `exclude` marks parts as
NOT PRINTED (vendor STEP imports, electronics): they skip minWall and void
checks but STILL participate in clearance — a vendor part 0.2 mm from a
printed part is a real assembly problem. `ignore` is a per-PAIR clearance
exemption for design-intent contacts. Pairs joined by a declared mate are
clearance-exempt automatically. Overlapping pairs are tagged `interfering`
and emit the shared `assembly.interference.overlap` error — overlap analysis
belongs to the interference gate, but the DFM gate never passes over it.
- **Choose `minWall` from the design's intended wall floor**, not a generic
printer guideline. Declaring `minWall: 1.5` against a design whose thinnest
intentional wall is 0.8 mm just fails everything and tells you nothing about
real defects. Also expect near-tangent authoring slivers (a boss grazing a
rim, a blend tangent to a bore) to surface as sub-0.1 mm wall findings —
those are honest measurements of sliver geometry; fix the tangency or read
past them deliberately.
- **Channel detection limits.** ONE non-sealed channel per part — a second
declaration emits `feature.invalid-args` and the mouth count binds to the
first. Channels wider than ~16 mm escape the morphological closing and
report `found: 0` — that is a detection limit, not a blockage. The declared
channel binds to the LARGEST detected internal component, which can misbind
on parts with multiple internal voids; the reported `channelSeed` location
shows which component was picked (binding the declaration by location is a
recorded follow-up).
- **`'unknown'` clearance status means the measurement FAILED** (kernel error
on that pair), not that the pair passed. Unknown pairs and kernel-failed
parts stay warn severity: they surface in the report and the CLI summary
(`, N unknown`) but never flip the exit code.
- **Diagnostic locations are world-frame.** Every xyz in a `dfm.*` diagnostic
message is mapped through the part's world transform, so findings compose
across the parts of a posed assembly. The raw `walls[]` / `voids[]` report
structs keep part-local coordinates.
Surfaces: automatic on `kernelcad evaluate` / MCP `evaluate_script`; standalone
report via `kernelcad dfm <file>` (`--json`) and MCP `verify({ check: 'dfm' })`.
## Materials
There are three ways to set surface character. Reach for them in this order:
1. **`.color(token | '#hex')`** — HUE ONLY. Cheapest. Role tokens (servo, gear,
beam, shaft, plate, pin, frame, tool) or a literal hex. Right for schematic
assembly renders where photo-accuracy is not the point.
2. **`.finish(name, { color? })`** — a NAMED material finish: hue plus surface
character (metalness / roughness / clearcoat). This is the primary verb when
you know the material. You name "brass" or "matte ABS", not BRDF floats.
3. **`.material({...})`** — the raw-PBR escape hatch. Only for glass, clearcoat,
anisotropy, or image textures that no finish token covers (see below).
**How they compose on one leaf:** a `.finish()`/`.material()` PBR record
overrides a `.color()` hue at render time. `.finish()` and `.material()` write
the same slot, so on one leaf it is last-write-wins between them. To TINT a
finish, pass `{ color }` to `.finish()` — do not layer `.color()` under it, the
hue would be shadowed.
**Critical rule:** apply `.finish()` / `.material()` to leaf parts BEFORE they
enter a boolean. A call on a post-union root is a no-op — the kernel cannot
retroactively assign material to the input leaves of a boolean.
### `.finish(name, opts?)` — named finishes
```typescript
bracket.finish('anodized-black'); // matte black anodised alu
crownGear.finish('brass'); // brass, intrinsic colour
housing.finish('abs', { color: '#c0392b' }); // red ABS housing
knob.finish('paint-gloss', { color: '#0a3d62' }); // glossy navy paint
lens.finish('glass-tinted'); // real refraction, no floats
bezel.finish('brass', { face: 'ring' }); // per-face, like .material({ face })
```
The vocabulary (small and curated — pick the closest, override the hue if you
need a different colour). This table is the source the finish-drift gate checks
against `FINISH_TOKENS`; keep the two in sync.
<!-- FINISH-VOCABULARY:START -->
| group | finishes |
|---|---|
| metals | `aluminium`, `aluminium-brushed`, `anodized-black`, `anodized`, `steel`, `stainless`, `brass`, `copper`, `titanium` |
| plastics | `abs`, `pla`, `nylon`, `delrin`, `polycarbonate`, `rubber`, `plastic-glossy` |
| glass | `glass`, `glass-tinted`, `acrylic` |
| paint / neutral | `paint-matte`, `paint-gloss`, `default` |
<!-- FINISH-VOCABULARY:END -->
`opts.color` overrides the hue while keeping the finish. It is meaningful for
the paints, `abs`, `plastic-glossy`, and `anodized` (dyed anodising); for raw
metals the colour is intrinsic, so an override is accepted but rarely wanted.
An unknown finish name throws `feature.finish.unknown-token` and lists the valid
finishes — it does not fall back to a default.
### Advanced: raw PBR via `.material()`
`Shape.material(opts)` is the escape hatch when no finish token fits — glass with
a specific attenuation, brushed anisotropy, a bespoke clearcoat, or image
textures. The fields are three.js `MeshPhysicalMaterial` floats; a finish token
expands to exactly this record, so anything a finish does, `.material()` can do
by hand. Prefer the named finish unless you need one of the fields below.
```typescript
// Bespoke glossy acetate — clearcoat + ior a finish token does not name exactly
part.material({
baseColor: '#0a0a0a',
metalness: 0.0,
roughness: 0.15,
clearcoat: 0.8,
clearcoatRoughness: 0.05,
ior: 1.55,
});
```
### Glass, brushed metal, textured surfaces
Three additional `PBRMaterial` fields cover the common photoreal archetypes:
**Glass (volume absorption).** `transmission > 0` is light passing through;
`thickness` (mm) and `attenuationColor` + `attenuationDistance` (mm) together
shade the colored absorption through the body. The renderer auto-loads a
neutral studio HDRI when any material in the scene has `transmission > 0`, so
glass renders with realistic refraction without needing `setRenderEnvironment`.
```typescript
// Dark sunglass lens
lens.material({
baseColor: '#ffffff',
transmission: 0.85,
ior: 1.5,
thickness: 3,
attenuationColor: '#1a1a2a',
attenuationDistance: 8,
roughness: 0.0,
});
```
**Brushed / anisotropic metal.** `anisotropy` (0..1) stretches the specular
highlight; `anisotropyRotation` (degrees, normalized to [0,360)) aligns the
brush direction with a face axis.
```typescript
// Brushed aluminum hinge
hinge.material({
baseColor: '#b0b0b0',
metalness: 1.0,
roughness: 0.3,
anisotropy: 0.8,
anisotropyRotation: 90,
});
```
**Image-texture maps.** `textures` attaches up to six maps — `albedo`,
`normal`, `roughness`, `metalness`, `anisotropy`, `emissive`. Paths resolve
relative to the script file (mirrors `referenceImage()`); `https://` URLs are
fetched once and sha256-cached at `~/.cache/kernelcad/textures/`.
```typescript
// Matte acetate frame with normal-mapped grain
frame.material({
baseColor: '#1a1a1a',
roughness: 0.55,
textures: {
albedo: { path: './acetate-color.png' },
normal: { path: './acetate-normal.png' },
roughness: { path: './acetate-rough.png' },
},
});
```
Supported texture formats: `.png`, `.jpg`, `.jpeg`, `.webp`. Maximum dimension
8192px (hard error); textures over 2048px on the longest side surface a
console warning. Each `TextureRef` accepts optional `repeat: [u, v]`,
`offset: [u, v]`, and `rotation: <degrees>`.
**For reproducible hero builds:** download URL textures and check the files
into the script's directory before committing. Agent-fetched URLs land in
`~/.cache/kernelcad/textures/` but that cache is not source-controlled.
### Per-face materials
For parts where different faces need different materials (eyewear rim vs. lens
vs. temple-hinge boss; brushed body + polished crown; etc.), pass a `face`
field referencing a face label declared on a creating op:
```typescript
const frame = box(140, 50, 6, false, {
faceLabels: { front: 'front', back: 'back', top: 'top' },
});
frame.material({ face: 'front', baseColor: '#0a0a0a', clearcoat: 1, roughness: 0.1 }); // glossy acetate front
frame.material({ face: 'back', baseColor: '#1a1a1a', roughness: 0.7 }); // matte interior
frame.material({ baseColor: '#cccccc', roughness: 0.5 }); // default for unlabeled faces
```
Rules:
- `face` must be a label declared upstream via the creator's `faceLabels` option
(or via `path().label(...)` for sketch-derived shapes).
- Calls accumulate on the shape — multiple `.material({ face: ... })` calls
build up per-face entries. A second call with the same `face` overwrites.
- A call **without** `face` sets the shape-level default (applies to any face
not covered by a per-face entry).
- If a label fails to resolve at mesh time (typo, transform stripped lineage,
no upstream `faceLabels` entry), the build continues and a soft
`feature.material.face-label-no-match` warning is emitted; the affected faces
fall back to the shape-level default.
- Per-face identity dies at boolean operations (same as `.color()` and the
whole-shape `.material()`). Apply per-face materials AFTER all booleans.
## Reference images
`referenceImage(path, opts)` places a reference photo as a plane overlay in the
Studio viewport. Display only, and the
image is hidden during scoring (`--hide-reference-images`).
```typescript
// Front-view overlay (XZ plane) — typical for flat products facing the camera
referenceImage('./reference.jpg', {
plane: 'xz',
anchor: 'origin',
scale: 'fit-bbox', // auto-scales to match the model's bounding box
opacity: 0.4, // ghost behind the model; adjust to taste
});
// Top-down overlay (XY plane) — for PCBs, floor plans, plate layouts
referenceImage('./top-view.jpg', {
plane: 'xy',
anchor: 'origin',
scale: 'fit-bbox',
opacity: 0.3,
});
// Side overlay (YZ plane) — for profiles, silhouettes from the right
referenceImage('./side.png', {
plane: 'yz',
anchor: 'origin',
scale: { width: 130 }, // explicit width in mm; height auto-computed
opacity: 0.5,
flipU: true, // mirror horizontal if the reference is from the left
});
```
Multiple `referenceImage()` calls are allowed — one per view plane. Path is
resolved relative to the calling `.kcad.ts` file. Supported formats: `.png`,
`.jpg`, `.jpeg`, `.webp`. Validation errors (missing file, bad format, invalid
plane) are pushed as diagnostics on the returned handle rather than thrown.
## Related skills
- `kernelcad-features` — load when adding fillets, chamfers, shells, holes, or cutouts.
- `kernelcad-params` — load when the model needs editable `param()` / `params()` values or live slider support.
- `kernelcad-assemblies` — load for multi-part models with joints, mates, or connectors.
- `kernelcad-nurbs` — load for freeform NURBS surfaces that primitives and sketches cannot express.
- `kernelcad-from-reference` — load when building from a reference photo or visual brief; extends the verification gate set.
- `kernelcad-mcp` — load instead of this skill when you need to introspect a running model dynamically via MCP tools.