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 13/13 tests 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 (yaw + pitch) ยท double-click resets ยท slider explodes the assembly. The amber readout is real: shapes sorted and emitted 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 2019, 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 sketch layer: context blocks scope parts, tags and layering โ€”
// authoring should read like drafting, not like assembling tuples
import { sketch } from "linework/sketch";

const s = sketch({ yaw: .5, pitch: .16, f: 1500, cx: 460, cy: 320 });

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

s.part("housing", 'class="prt"', () => {
  s.cyl([460, 218], 70, 34, -34, "ink");       // bearing body
  s.bias(.6, () => s.cap([460, 218], 34, 32, "ink"));  // bore
});

s.part("shaft", 'class="prt"', () =>
  s.cyl([460, 218], 19, 168, 24, "ink"));      // pulled out

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

Lightly abridged (bolts, balloons and the title block omitted for width) โ€” the full scene is docs/scene.js, the exact module this page executes, written on the same sketch API.

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 hero).