An architect drafting a technical plan with a pencil and ruler on white paper, seen from above
Open-source

linework — drop a 3D model, get a rotatable technical drawing in SVG

7 min read Isaac Rowntree

Zack Design has published linework — a tiny true-3D renderer for annotated technical drawings, output as plain SVG strings. Hand it 3D points, or drop in a glTF, OBJ or STL model and it draws itself: it rotates the geometry, projects it with perspective, sorts it back-to-front, and hands you SVG. The whole core is about 180 lines with zero runtime dependencies, and it exists because I went looking for exactly this and the shelf was bare.

Live demo (drop a 3D model, watch it become a drawing) → isaacrowntree.com/linework · Source → github.com/isaacrowntree/linework (MIT)

Why this exists

I’ve been building a “will this part actually fit your bike?” planner — the sort of tool that tells you whether a given rear shock, dropper, or ebike motor bolts onto your frame before you spend the money. It grew out of my earlier bike-shock-planner engine, and the thing that makes it click is the drawing: an exploded, service-manual-style diagram of the bike with every part ballooned to a parts list, that you can grab and rotate to see how the pieces sit in space.

So I needed to render exploded technical diagrams. Rotatable ones. With dimension lines, callout balloons, and a title block — the whole drafting vocabulary. And I went looking for a library, fully expecting to find three of them.

There aren’t any.

The empty category

The 3D tooling world splits cleanly in two, and neither half wants this job.

On one side is three.js and the WebGL universe. It’s superb — at shaded surfaces, lights, materials, cameras. But everything that makes a technical drawing read well is a fight there: crisp 1px strokes (WebGL lines are a known pain), dashed hidden-lines, line-weight hierarchy, and text callouts that have to live in a separate DOM overlay layer bolted on top of the canvas. I’d be spending all my effort making a rendering engine stop doing the thing it’s good at. It’s a cannon for a job that needs a technical pen.

On the other side are the friendly pseudo-3D toys — and there’s really only one that fits the aesthetic, Zdog. Lovely flat-shaded look, tiny, designer-friendly. But its last release was in 2019, it’s self-described as a beta, and text is a third-party plugin. Dimension lines, balloons, and leader text aren’t a nice-to-have for an exploded diagram — they’re half the drawing. A renderer that can’t do text can’t do this.

Nothing in between ships parametric technical illustration: exploded views, callouts, paper-space annotations, styling you control. So I wrote it.

What it actually is

The pipeline is the textbook one, implemented honestly rather than faked:

3D points → yaw/pitch rotation → perspective projection → painter’s-algorithm depth sort → SVG strings.

The important word is sort. Paint order is recomputed every frame from the actual depth of every shape, so when you rotate the model the near stay passes in front of the far one correctly — it’s real occlusion, not hand-authored layering that falls apart at a new angle. Circles in a plane project to correct ellipses (wheels, bearing races), boxes cull their back faces, and cylinders keep their end-caps readable rather than thinning to invisible slivers edge-on. That last one is a deliberate lie in service of legibility, which is exactly the kind of call a drawing library gets to make and a physics-accurate one doesn’t.

Here’s a bearing assembly, exploded — and this exact SVG was rendered by the library, in Node, with zero client-side JavaScript:

An exploded pillow-block bearing assembly: base plate, bearing housing, shaft and two bolts pulled apart along their assembly axes, with numbered callout balloons, a dimension line and a title block

Point it at a model you already have

That pillow block is hand-authored — but the bigger unlock is that linework will import. Hand it a glTF, OBJ or STL mesh (or a three.js BufferGeometry) and it recovers the drawing for you. A shaded 3D model carries no lines; its form lives in where the surface bends. So linework/import keeps only the feature edges — the outline and the hard creases, nothing from the smooth interior of a face — which is precisely the set of lines a draftsperson would ink. One call does it:

import { parseGLB, meshToShapes } from "linework/import";
const meshes = parseGLB(await file.arrayBuffer());   // parseOBJ · parseSTL · fromBufferGeometry
const shapes = meshToShapes(meshes, { angle: 25, fit: { cx: 400, cy: 300, size: 440 } });

Even STEP and IGES work — not by embedding a CAD kernel (tessellating trimmed-NURBS B-reps is a job for OpenCASCADE, not 180 lines), but because linework consumes meshes: run the file through occt-import-js and a ~10-line fromOcct() adapter maps the result straight in. The heavyweight kernel stays an optional peer; the core stays tiny.

Here’s a CC0 street lantern — 5,394 triangles of shaded mesh — reduced to 2,479 feature edges and rendered as a rotatable line drawing, again entirely in Node:

A street lantern imported from a glTF mesh and rendered by linework as a blueprint-style feature-edge line drawing

That reframe is what made this worth releasing. “Hand-code a 3D scene” is a small audience; “bring any model you already have and get a technical drawing” is a large one — and every 3D tool on earth exports glTF. Drop your own .glb on the live demo and watch it happen; it runs in your browser, nothing uploaded.

Why SVG strings, specifically

This is the whole bet, and it’s what neither neighbour can offer. Because the output is just markup:

  • You style it. The library emits class names you define — it never bakes in a colour, a stroke width, or a font. Theme it with CSS variables, restyle it for dark mode, print it.
  • It’s crawlable and accessible. Text callouts are real text. A screen reader and Google both see them.
  • It renders on a server. render() is a pure function from shapes to a string — no canvas, no DOM, no browser. A static-site generator can emit finished 3D-looking diagrams at build time with nothing shipped to the client. The hero image in the repo is generated this way; the live demo uses the same code to orbit under your pointer.

Making it nice to author

A renderer you dread writing scenes for is a renderer you don’t use, so linework ships a small authoring layer on top of the raw primitives. It reads like drafting instead of like assembling tuples — context blocks scope which animated part, which CSS tag, and which layering everything inside them belongs to:

import { sketch } from "linework/sketch";

const s = sketch({ yaw: 0.5, pitch: 0.16, f: 1500, cx: 460, cy: 320 });

s.box(300, 380, 320, 42, 40, 80);                 // base plate

s.part("housing", 'class="prt"', () => {          // one <g>, one sort unit
  s.cyl([460, 218], 70, 34, -34, "ink");          // bearing body
  s.bias(0.6, () => s.cap([460, 218], 34, 32, "ink"));
});

s.note("320 mm", [460, 452]);                      // paper-space annotation
el.innerHTML = s.render();                         // depth-sorted SVG

And because grabbing a drawing to spin it is the same twenty lines of pointer-capture boilerplate every single time, that’s a one-liner too — linework/orbit handles the drag, the clamps, the reset, and a gentle idle sway so an embed is alive before anyone touches it. Install and kick the tyres:

npm i linework          # on npm — ESM, types included, zero runtime deps
npm test                # projection, parallax, sorting, culling, edge extraction

The lesson

There’s a reflex, when a problem looks 3D, to reach for the big 3D engine — and then spend a week teaching a flight simulator to draw a neat labelled diagram. The actual need was smaller and more specific than the tool everyone reaches for, and nobody had written the small specific thing because it sits in the crack between “serious 3D” and “cute toy.”

That crack is where a lot of good little libraries live. This one is ~180 lines, has no dependencies to rot, comes with tests so a wrong drawing fails loudly instead of just looking plausible, and it’s MIT on GitHub — take it, theme it, drop in your own model and draw with it. The bike planner it was born for is coming next.