# Krbn API — building scenes & animations A short guide to the public API. Krbn turns **3-D geometry into pencil-style SVG strokes**: you describe a scene (primitives + a camera + a little semantics), and the engine derives, hides, styles, and emits the strokes. ## The deliverable model A scene lives in a `*.krbn.ts` file that **default-exports a deliverable**: - a **`Drawing`** — one still frame → one `.svg`, or - a **`Film`** — a sequence of frames → a folder of `frame-###.svg` + a `flipbook.html`. Render it with the CLI (writes the output beside the source): ```bash bun run render path/to/scene.krbn.ts # → scene.svg, or scene/ for a film bun run render:gallery # every examples/gallery/*.krbn.ts bun run render:animation # examples/animation.krbn.ts ``` A `Drawing` is just `{ toSvg(): string }`, so you can also call it directly (`scene.toSVG(cam)`), embed the string, or ship it however you like. ## A still, end to end ```ts // hello.krbn.ts → bun run render hello.krbn.ts → hello.svg import { Scene, sphere, Cylinder, view, type Camera } from "krbn"; const cam: Camera = { eye: [4, 3, 2], target: [0, 0, 0], up: [0, 0, 1], projection: "perspective", scale: Math.PI / 4, viewport: { width: 640, height: 480 }, }; const scene = new Scene({ svg: { background: "#faf9f5" }, light: { direction: [-0.4, -0.5, -0.7] }, }); scene.add(sphere([0, 0, 0], 1)) .style({ wobble: 0.3, hatch: { mode: "cross", angle: 20 } }); scene.add(new Cylinder([1.6, 0, -1], [0, 0, 2], 0.5)) .setImportance(0.3, { role: "context" }); // quieter, ghosted export default view(scene, cam); // a Drawing ``` ## Building a scene **Camera** — `{ eye, target, up, projection: "perspective" | "orthographic", scale, viewport: { width, height } }`. `scale` is the half-FOV (radians) for perspective, world-units-per-pixel for orthographic. Points are `[x, y, z]`. **`new Scene(opts?)`** options: `light: { direction }`, `svg: { background, centerline }` (see [Pen plotters](#pen-plotters)), `style` (scene-wide default style), `abstraction: { toneLevels?, minFeaturePx?, consolidate? }`. **Primitives** (each returns a source you pass to `scene.add`): | Primitive | Constructor | |---|---| | Sphere | `sphere(center, radius)` | | Ellipsoid | `ellipsoid(center, [rx, ry, rz])` | | Cylinder | `new Cylinder(base, axis, radius)` | | Cone | `new Cone(apex, axis, radius)` | | Torus | `new Torus(center, axis, majorR, minorR)` | | Line | `new Line(a, b)` | | Polygon | `new Polygon([v0, v1, …])` | | Point | `new Point(pos, { mark: "cross" \| "dot", sizePx })` | | Bézier | `new BezierCurve([p0, p1, p2, p3])` | | Helix | `helix(center, radius, pitch, turns)` | | Function plot | `functionPlot(x => y, x0, x1)` | | Mesh | `new Mesh(input, opts?)` — see [Meshes](#meshes) | **Add & configure.** `scene.add(source, opts?)` returns an `Element` you can chain: ```ts scene.add(sphere([0, 0, 0], 1)) .setImportance(1, { role: "subject" }) // subject | context | default .style({ wobble: 0.4, // 0 = ruler, ~1 = hand-drawn weight: 1.6, // stroke width hidden: "ghost", // "ghost" (x-ray) | "drop" (opaque) hatch: { mode: "cross", angle: 20, spacingPx: 8, field: true }, }); ``` Hatch `mode` is `"single" | "cross" | "triple"` (1/2/3 tonal layers); `field: true` (default) uses the surface's exact curved iso-parameter field, `false` forces straight parallels. `importance`/`role` drive the abstraction stage (how much detail to keep) and supply styling defaults. **Output masking — splitting a scene into layers.** `output: false` mutes an element's *own* strokes while keeping it fully in the pipeline: it still occludes other elements and is still occluded by them (hidden-line, hatch clipping). Render the scene once per subset — muting the rest — and every layer keeps correct visibility against the *whole* scene. This is the split for **multi-pen / multi- colour plotting**: one SVG per pen, and no layer draws what another is in front of. ```ts const well = scene.add(new Mesh(gravitySheet(3, 72, 1.7, 0.95))).style({ /* … */ }); const ball = scene.add(sphere([0, 0, -0.66], 0.8)).style({ /* … */ }); ball.setOutput(false); // or: scene.add(src, { output: false }) const wellLayer = scene.render(cam).svg; // only the well — but the ball still // carves its silhouette out of the hatch ball.setOutput(true); well.setOutput(false); const ballLayer = scene.render(cam).svg; // only the ball — its base still cut by well.setOutput(true); // the (invisible) sheet's near lip ``` `output` defaults to `true`; set it in `scene.add(src, { output })` or with the chainable `el.setOutput(bool)`. It gates only the final emit — the visibility classification upstream always runs over every element. See [`examples/gallery/22-output-split.krbn.ts`](examples/gallery/22-output-split.krbn.ts). **Relations.** ```ts const a = scene.add(sphere([0, 0, 0.5], 1)); const b = scene.add(new Polygon([[-2,-2,0],[2,-2,0],[2,2,0],[-2,2,0]])); scene.intersect(a, b, { emphasis: "bold" }); // exact waterline curve scene.highlight(a, { weight: 1.8, dashWhenHidden: true, halo: { weight: 12, opacity: 0.28 } }); ``` ## Composing figures Panels are just SVG strings (`scene.toSVG(cam)`); the layout helpers stitch them into one labelled `Drawing`: ```ts import { grid, stack, raw, textAt, withLabels } from "krbn"; grid(cam.viewport, rows /* string[][] */, { rowLabels, colLabels }); stack(topSvg, bottomSvg, cam.viewport, { top: "before", bottom: "after" }); raw(withLabels(scene.toSVG(cam), [textAt(cam, [0,-1.2,0], "label")])); ``` ## Meshes A triangle mesh is just another source, rendered through the same pipeline. ```ts import { Mesh } from "krbn"; import { torusMesh, gravitySheet } from "krbn/shapes"; scene.add(new Mesh(torusMesh(1.3, 0.5), { suggestive: { threshold: 0.02 } })); scene.add(new Mesh(gravitySheet(3, 72, 1.7, 0.95))); ``` Raycasts (which is how hidden-line visibility and tonal hatching are resolved) are accelerated by a per-mesh BVH, built lazily on the first ray and reused for the mesh's lifetime — worth roughly 20× on a model of a few thousand triangles. It is pure culling in front of the same exact intersector, so results are identical either way; `new Mesh(input, { bvh: false })` opts out and linear-scans every triangle, which is only useful for debugging. `MeshInput` is `{ positions: Vec3[], triangles: [number, number, number][] }` — bring your own geometry, or use the starter generators in **`krbn/shapes`** (`cube`, `tetrahedron`, `uvSphere`, `tube`, `torusMesh`, `gravitySheet`, `bumpyBlob`, `knotTube`, `extrude`, plus `translate` / `rotate`). These are convenience content, kept off the core API. `extrude(profile, height)` pushes a 2-D polygon (`Vec2[]` in the z=0 plane, any winding) up to `height`: a flat lid, a flat floor, and one wall per edge, with the caps ear-clipped so **non-convex** outlines (an L, a star, a gear) work. A rounded profile reads as smooth walls under a flat lid; a sharp one reads faceted — see [`gallery/20`](examples/gallery/20-extrusion.svg). ### Importing meshes (STL / OBJ) `parseSTL(bytes | string)` and `parseOBJ(bytes | string)` turn a mesh file into a `MeshInput`. Both are pure decoders — they read no files, so you bring the bytes — and drop degenerate triangles. ```ts import { readFileSync } from "node:fs"; import { Mesh, parseSTL, parseOBJ } from "krbn"; scene.add(new Mesh(parseSTL(readFileSync("car.stl")), { weldEps: 0.05 })); scene.add(new Mesh(parseOBJ(readFileSync("hand.obj")))); // OBJ: no weld needed for topology ``` - **`parseSTL`** auto-detects **binary vs ASCII** (by the exact size formula, not the header text — binary files often start with `solid` too) and repairs each triangle's winding against its stored facet normal. STL is unwelded triangle soup (no shared vertices), so pass **`weldEps`** to fuse coincident corners back into the shared topology that creases and silhouette chaining need. - **`parseOBJ`** reads the geometry subset (`v`/`f`), skips the rest (`vt`/`vn`/`g`/`usemtl`/…), handles all face-index forms (`v`, `v/vt`, `v/vt/vn`, `v//vn`), 1-based and negative indices, and fan-triangulates quads / n-gons. OBJ already ships a shared vertex table, so topology is there for free — `weldEps` is optional (use it only to decimate). `weldEps` is the **decimation knob** — a distance in model units; turning it up merges more vertices, so the mesh gets lighter. Tiny keeps full detail; larger trades detail for a faster hidden-line pass and a cleaner sketch. That level is the caller's choice, never baked into the loader. Real CAD/scan meshes are often non-manifold; welding fixes most of it, and any faces that collapse under the weld are dropped. > **Importing meshes is early.** The analytic primitives are exact, but arbitrary > imported meshes are the young part of the engine — a complex model won't render > beautifully out of the box, and there's a lot of refinement still ahead. Expect > to tinker per model: dial **`weldEps`** (decimation) and **`creaseAngle`** (raise > it to suppress the spurious creases a decimated/noisy tessellation invents; past > π turns creases off for smooth organic scans) to find the sweet spot. See [`examples/importers/`](examples/importers) (`bun run render:importers`) — STL (`stl.krbn.ts`: cube + heart) and OBJ (`obj.krbn.ts`: mushroom + Tower of Hanoi + fist), several showing flat vs curvature hatch side by side. ## Animations An animation is a `Film`: a driven sequence of frames, **each an ordinary `Drawing`** — so everything above composes a frame exactly as it does a still. A `FrameSession` carries stroke identity across frames (temporal coherence), and `film(...)` steps the sequence. ```ts // orbit.krbn.ts → bun run render orbit.krbn.ts → orbit/frame-###.svg + flipbook.html import { Scene, sphere, FrameSession, film, raw, type Camera } from "krbn"; const W = 640, H = 480, FRAMES = 60; const cam = (a: number): Camera => ({ eye: [9 * Math.sin(a), -9 * Math.cos(a), 3.5], target: [0, 0, 0], up: [0, 0, 1], projection: "orthographic", scale: 0.02, viewport: { width: W, height: H }, }); const scene = new Scene({ style: { wobble: 0.6 } }); scene.add(sphere([0, 0, 0], 1.2), { style: { hatch: { mode: "cross", angle: 20 } } }); const session = new FrameSession(scene); export default film( FRAMES, (k) => raw(session.render(cam((Math.PI / 1.5) * k / (FRAMES - 1))).svg), { viewport: { width: W, height: H }, fps: 12 }, ); ``` `film(count, k => Drawing, { viewport?, fps?, onFrame? })` — the optional `onFrame(k)` hook runs after each frame (progress, coherence reports, …). The CLI writes the frames + a `flipbook.html` that references them (scrub/play in a browser). `session.render(cam)` returns `{ svg, strokes, renderStrokes, coherence }`. ## Pen plotters By default the SVG backend draws pencil-like lines: constant-width strokes as `` and the calligraphic variable-width strokes as **filled `` ribbons**. A pen plotter wants neither — a filled ribbon gets traced as a double outline, and many SVG→G-code converters choke on ``. Set the backend flag: ```ts const scene = new Scene({ svg: { centerline: true, background: null }, // plotter-ready output light: { direction: [-0.4, -0.5, -0.7] }, }); ``` With `centerline`, **every stroke is emitted as a single open `` centreline** (`M … L …`) at a constant `stroke-width` — no ``, no filled ribbons. One path per stroke, so the pen draws each line exactly once; dashes (`hidden: "ghost"`) and colours are preserved, and only the calligraphic taper is dropped (it has no meaning for a fixed pen). `background: null` omits the background rectangle so the file is nothing but stroke paths. It's a backend option carried on each `Scene`, so it composes with `grid` / `stack` / `film` — set it on every `Scene` whose panels you want in centreline form. The whole pipeline stays headless: `bun run render scene.krbn.ts` emits a file you can hand straight to a G-code converter, no Inkscape round-trip. ## Exports at a glance **`krbn`** — `Scene`, `FrameSession`; primitives (`sphere`, `ellipsoid`, `Cylinder`, `Cone`, `Torus`, `Line`, `Polygon`, `Point`, `BezierCurve`, `helix`, `functionPlot`); `Mesh` + the `MeshInput`/`Tri` types + the `parseSTL` / `parseOBJ` loaders; deliverables & layout (`view`, `raw`, `grid`, `stack`, `figures`, `film`, `flipbook`, `textAt`, `withLabels`, and the `Drawing`/`Figures`/`Film` types); core math types (`Camera`, `Vec3`, …). **`krbn/shapes`** — mesh generators (`cube`, `uvSphere`, `torusMesh`, `gravitySheet`, `knotTube`, `extrude`, …) and `translate` / `rotate`. See [`docs/DESIGN.md`](docs/DESIGN.md) for how the engine works and [`examples/`](examples) for the full gallery and animation.