Draw on PDF pages

Stage four. Strokes are kept per page in that page's own coordinate
space — PDF points at scale 1 — not in the fixed 1200x1600 space a note's
drawing uses. A PDF cannot use one space: every page has its own size,
and a stroke belongs to the page it was drawn on.

That is what keeps a mark on the word it was drawn over at any zoom and
on any screen, and it is the only form that can be stamped back into the
file in stage seven, where page points are the only coordinates that mean
anything. Measured rather than assumed: zooming moves a stroke by 0.001
of the page.

Pen sizes are scaled by the page width. The sizes are calibrated for the
1200-unit note page, so on a 595-point A4 page the same number would have
drawn twice as thick.

Reading and drawing are separate modes. While reading the ink layer takes
no pointer events, so scrolling and selecting text reach the page beneath
it; while drawing, a finger still scrolls and only a pen or mouse marks,
because a touch-drag on a phone means scroll far more often than it means
draw. The drawing can be hidden without being erased, for reading the
original clean.

The server accepted only the single-page shape; it now takes either and
counts strokes across pages for its limit.

Carried over from notes: the save token guard, so the six-second poller
adopting a response issued before the local save cannot undo what was
just drawn. The suite waits nine seconds and checks the ink is still
there — that bug has cost strokes twice before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-08 20:54:10 +02:00
1 parent 2986bcb620
commit 1be3a9b9ba
7 files changed
+589 -14

No files matched your search

+29 -5
View File
@@ -1035,6 +1035,30 @@ app.delete("/api/notes/:id/permanent", authMiddleware, (req, res) => {
/* ─── Drawing data route ─── */
const MAX_STROKES = 20000;
/**
* Counts the strokes in either shape of drawing document.
*
* A note's drawing is one page: `{ w, h, strokes }`. A PDF's is one entry per
* page it has been drawn on: `{ pages: { "1": { w, h, strokes }, … } }`, because
* every PDF page has its own size and a stroke belongs to the page it was drawn
* on, not to a canvas shared by all of them.
*
* Returns null for anything that is neither, so the caller can refuse it.
*/
function countStrokes(doc) {
if (!doc || typeof doc !== "object") return null;
if (Array.isArray(doc.strokes)) return doc.strokes.length;
if (doc.pages && typeof doc.pages === "object") {
let n = 0;
for (const page of Object.values(doc.pages)) {
if (!page || !Array.isArray(page.strokes)) return null;
n += page.strokes.length;
}
return n;
}
return null;
}
app.put("/api/notes/:id/drawing", authMiddleware, (req, res) => {
const { drawingStrokes, drawingData } = req.body;
const note = db.prepare("SELECT id FROM notes WHERE id = ? AND user_id = ?").get(req.params.id, req.userId);
@@ -1042,13 +1066,13 @@ app.put("/api/notes/:id/drawing", authMiddleware, (req, res) => {
if (drawingStrokes !== undefined) {
// Vector strokes in the note's canonical coordinate space — the current format.
let count = 0;
if (drawingStrokes !== null) {
if (typeof drawingStrokes !== "object" || !Array.isArray(drawingStrokes.strokes))
return res.status(400).json({ error: "Invalid strokes format" });
if (drawingStrokes.strokes.length > MAX_STROKES)
return res.status(413).json({ error: "Too many strokes" });
count = countStrokes(drawingStrokes);
if (count === null) return res.status(400).json({ error: "Invalid strokes format" });
if (count > MAX_STROKES) return res.status(413).json({ error: "Too many strokes" });
}
const empty = !drawingStrokes || drawingStrokes.strokes.length === 0;
const empty = !drawingStrokes || count === 0;
const previous = db.prepare("SELECT drawing_strokes FROM notes WHERE id = ?").get(req.params.id);
// Clearing a drawing is always worth being able to undo.
snapshotNote(req.userId, req.params.id, req.deviceId, {