Zero-dependency ยท SVG out

Line/work

A tiny true-3D renderer for annotated technical drawings. 3D points in โ€” depth-sorted SVG strings out.

~180-line core 0 deps tested MIT
$ npm i linework

Every drawing on this page โ€” including the one to the right โ€” is rendered live by the library when the page loads.

โ€”

Drag to orbit ยท double-click resets ยท the slider raises the crease threshold. The amber readout is real: feature edges extracted and depth-sorted this frame.

Why this exists

The empty category

WebGL fights linework

three.js makes shaded surfaces easy and technical drawing painful: 1px line limits, dash patterns via shaders, text callouts as a separate DOM overlay system. Every signature element of a service manual is an uphill fight.

Pseudo-3D toys can't annotate

Zdog nailed the flat-shaded look โ€” and shipped its last release in early 2022, with text as a third-party plugin. Dimension lines, balloons and leader text are the load-bearing half of a technical drawing.

SVG strings are the point

Output is plain markup: themeable with CSS variables, crawlable, printable, screen-reader friendly โ€” and renderable server-side at build time with zero client JavaScript.

Objectivity

The code behind the hero

// the lantern above โ€” a whole glTF mesh becomes linework in three calls
import { parseGLB, meshToShapes } from "linework/import";
import { render } from "linework";

const meshes = parseGLB(await (await fetch("lantern.glb")).arrayBuffer());
const shapes = meshToShapes(meshes, { angle: 25, fit: { cx: 400, cy: 300, size: 440 } });
el.innerHTML = render(shapes, { yaw: .6, pitch: .35, f: 1400, cx: 400, cy: 300 });

Feature-edge extraction keeps the outline and hard creases, nothing from the smooth interior of a face โ€” the lines a draftsperson would draw. Prefer to hand-author instead? The sketch layer builds annotated scenes โ€” boxes, cylinders, dimension notes, exploded parts โ€” with scoped context blocks; see docs/scene.js and the sketch docs.

Architecture

The classic pipeline, honestly implemented

3D modelโ†’ rotate ยท yaw/pitchโ†’ perspective projectโ†’ painter's sortโ†’ SVG strings

Paint order is computed every frame, never authored โ€” members interleave correctly at any angle. Shapes sharing a part key sort as one object and emit inside one <g>, so CSS transforms on animated parts keep working. Known painter's-algorithm caveat: cyclic overlaps can't sort โ€” split long members into segments if you construct one.

Conventions

Coordinates

FieldMeaning
xright, in your SVG's user units
ydown โ€” screen convention, not math convention
ztoward the viewer; negative z recedes and depth-dims
view.yawrotation about the vertical axis through cx (radians)
view.pitchrotation about the horizontal axis through cy; positive looks down
view.fperspective focal distance โ€” k = f / (f โˆ’ z); larger = flatter
biasper-shape depth nudge for deliberate layering of coplanar shapes
No client JS required

Renders server-side

// render() is a pure string function โ€” run it at build time
import { render } from "linework";
import { writeFileSync } from "node:fs";

writeFileSync("diagram.svg", wrap(render(shapes, view)));

Static site generators can ship finished 3D-looking exploded diagrams with zero client JavaScript โ€” the README hero image in this repo is generated exactly this way (npm run images).