Skip to content

Getting started ​

Install ​

sh
bun add @adecore/drawing

The package has no peer dependencies. It brings Zod, Rough.js and perfect-freehand. A store that only validates files imports @adecore/drawing/protocol, which loads Zod alone:

Entry pointHoldsLoads
@adecore/drawing/protocolZod schemas, types, migrateDrawingZod
@adecore/drawingEdits, geometry, paths, SVG, text and reading orderRough.js and perfect-freehand

Create a drawing ​

Start from a copy of EMPTY_DRAWING and add elements with an edit. applyDrawingEdit fills in the id and seed of each new element through a mint the app provides:

ts
import { applyDrawingEdit, type DrawingEditMint } from '@adecore/drawing';
import { EMPTY_DRAWING, type DrawingDocument } from '@adecore/drawing/protocol';

const mint: DrawingEditMint = {
    id: () => crypto.randomUUID(),
    seed: () => Math.floor(Math.random() * 2 ** 31)
};

let drawing: DrawingDocument = { ...EMPTY_DRAWING, elements: [] };

const added = applyDrawingEdit(
    drawing.elements,
    {
        add: [
            { kind: 'note', x: 0, y: 0, w: 160, h: 100, text: 'Idea', fillColor: 'yellow' },
            { kind: 'note', x: 280, y: 0, w: 160, h: 100, text: 'Shipped', fillColor: 'green' },
            { kind: 'line', x: 160, y: 50, w: 120, h: 0, points: [[0, 0], [120, 0]], arrowEnd: true }
        ]
    },
    mint
);

if (!added.ok) {
    throw new Error(added.problems.map((problem) => problem.message).join('\n'));
}

drawing = { ...drawing, rev: drawing.rev + 1, elements: added.elements };
const [idea, shipped, arrow] = added.added;

added.added holds the new ids in the order of add. A new element without a stroke is ink, 2 wide, and a note without a size is 20.

Change it ​

An update names an element by id and lists only the fields that change:

ts
const renamed = applyDrawingEdit(drawing.elements, { update: [{ id: shipped, text: 'Review' }] }, mint);

if (renamed.ok) {
    drawing = { ...drawing, rev: drawing.rev + 1, elements: renamed.elements };
}

The edit is all or nothing. A field the element's kind does not have, such as text on the arrow, returns { ok: false, problems } with the path ['update', 0, 'text'] and leaves the drawing as it was. Edits and rebasing has the rules.

Export it ​

ts
import { DEFAULT_PALETTE, readingOrder, toSvg } from '@adecore/drawing';

const svg = toSvg(drawing.elements, { palette: DEFAULT_PALETTE, background: '#ffffff' });
const lines = readingOrder(drawing.elements);
// ['Idea', 'Review', 'Idea -> Review']

toSvg returns one SVG string sized to the elements plus a margin. readingOrder reads the two notes and the arrow between them. Paths and SVG covers the options, and how to paint on a canvas of your own instead.

Read it back ​

Store the document as JSON. When you load it, parse it and look for duplicate ids, which the schema cannot see:

ts
import { duplicateElementIdIn, migrateDrawing } from '@adecore/drawing/protocol';

const loaded = migrateDrawing(JSON.parse(file));

if (loaded === null || duplicateElementIdIn(loaded.elements) !== null) {
    throw new Error('Not a drawing');
}

Checklist ​

  • The app makes the ids and seeds of new elements, and an id is never one the drawing holds.
  • Every saved change raises rev by one, and the app compares rev with the stored one before it writes.
  • Every loaded file goes through migrateDrawing and duplicateElementIdIn.
  • An export outside the page gets plain colors in its palette, not CSS variables.

Licenses and third-party notices are listed in each package.