Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
Progress
Last updated: 2026-10-11.
Phase 3: local-first design and whiteboard tool
Goal: grow paper-rs into a Paper / Figma / Excalidraw-class tool. Figma and Paper are studied
clean-room (behavior only, analysis/FIGMA_NOTES.md, analysis/PAPER_ARCHITECTURE.md).
Excalidraw is MIT and studied from source (analysis/EXCALIDRAW_NOTES.md); ported algorithms
are credited in THIRD_PARTY.md.
| Milestone | Status | Evidence |
|---|---|---|
| Analysis notes | ✅ done | analysis/FIGMA_NOTES.md, analysis/EXCALIDRAW_NOTES.md, fixtures in tests/fixtures/excalidraw/ |
| P1: local projects, home screen, autosave, atomic writes, versioned format | ✅ done | src/project/, src/session.rs, src/io/atomic.rs, tests/projects.rs::m1_*, milestone1_home.png, milestone1_trash.png |
| P2: multiple pages, infinite canvas per page | ✅ done | src/editor/pages.rs, src/ui/pages.rs, tests/projects.rs::m2_*, milestone2_pages.png |
| P3: Figma core | ✅ done | src/layout.rs, src/editor/{structure,vector_ops,text_edit}.rs, src/render/{draw,tess}.rs, src/ui/inspector.rs, tests/figma.rs, tests/perf.rs, milestone3_figma_core.png, milestone3_canvas.png |
| P4: Excalidraw core | ✅ done | src/render/rough.rs, src/connector.rs, src/editor/whiteboard.rs, src/library.rs, src/ui/library.rs, tests/excalidraw.rs, milestone4_whiteboard.png, THIRD_PARTY.md |
| P5: inspector, layers drag-reorder, command palette, shortcuts | ✅ done | src/ui/{inspector,palette}.rs, src/ui/mod.rs (layers drag, help), src/editor/{commands,shortcuts}.rs, src/macos.rs, tests/workflow.rs, milestone5_palette.png, milestone5_shortcuts.png, milestone5_inspector.png |
| P6: import/export (PNG/SVG/PDF, .excalidraw, SVG), cross-project paste | ✅ done | src/io/{pdf,export,excalidraw,svg_import,clipboard}.rs, inspector Export section, CLI --export*/--import, tests/io.rs, milestone6_*.png |
| P7: MCP server for AI agents (Paper-compatible tools, HTTP + stdio bridge) | ✅ done on Linux; ⏳ Mac run pending (steps below) | analysis/paper-mcp.md, src/mcp/, src/bin/paper-rs-mcp.rs, tests/mcp.rs, milestone7_mcp.png, milestone7_mcp_live_xvfb.png |
P7 notes: MCP server
-
Study (
analysis/paper-mcp.md): Paper's desktop app serves MCP over streamable HTTP on127.0.0.1:29979/mcp(hono + MCP SDK), forwards calls into the renderer's editor, guards against browsers and DNS rebinding, and ships a Gopaper mcpstdio relay. 29 tools; the write path iswrite_html(HTML + inline CSS → layers) andupdate_styles(CSS maps). Studied with REA (behaviour only; raw dumps in gitignoredanalysis/raw/). -
Server (
src/mcp/server.rs, rmcp 3.5): streamable HTTP on127.0.0.1:29980/mcp(PAPER_RS_MCP_PORT,PAPER_RS_MCP=0to disable), on its own Tokio thread. Calls are sent to the winit loop (UserEvent::Mcp) and run on the UI thread viamcp::exec, each as oneEditor::edittransaction (one undo step, live repaint, autosave). Edits to other pages go to that page's own document and history (Editor::with_page) without switching the view. -
Bridge (
paper-rs-mcp): rmcp stdio server that lists tools offline and relays calls to the app over HTTP, reconnecting after app restarts. -
Tools (
src/mcp/tools.rs): Paper's names and argument shapes where there is an equivalent (see README), pluscreate_shape,select_nodes,undo,redo.write_html(src/mcp/{html,css}.rs,exec.rsBuilder) maps flex/block layout onto auto layout with CSS stretch semantics, text styles and rich-text runs, backgrounds, gradients, borders, images, inline SVG and clones. -
Verified:
tests/mcp.rs(12 tests: every tool on a headless session, rmcp client over HTTP and through the bridge, browser/Origin rejection, a raw 2026-07-28 request through the bridge, screenshots). End to end on Linux: the real app under Xvfb, driven by Claude Code (claude -pwith the server added over HTTP, then over the stdio bridge) drew a frame with a heading, paragraph, button and sticky note that appeared live in the window (docs/screenshots/milestone7_mcp_live_xvfb.png). That run found a bug the rmcp-only tests missed (bridge results lackedresultTypefor 2026-07-28 clients), fixed in01e0836. The macOS target type-checks (cargo check --target aarch64-apple-darwin --all-targets). -
Mac verification steps (not run here: no access to the Mac from this machine). In
/Users/Shared/projects/paper-rson the Mac:export CARGO_TARGET_DIR=~/.cache/paper-rs-target # keep build output out of the synced tree cargo build --release $CARGO_TARGET_DIR/release/paper-rs & # app; log says "MCP server listening on http://127.0.0.1:29980/mcp" curl -s http://127.0.0.1:29980/x # → {"status":"not_found",...The MCP endpoint is /mcp."} # HTTP client claude mcp add --transport http --scope user paper-rs http://127.0.0.1:29980/mcp claude -p "Use the paper-rs MCP tools: create_file 'Mac check', open_file it, create a 480x320 \ artboard 'Hello', and write_html a heading 'Hello from the Mac' into it." \ --allowedTools "mcp__paper-rs__*" # stdio client (through the bridge) claude mcp remove paper-rs --scope user claude mcp add --scope user paper-rs -- $CARGO_TARGET_DIR/release/paper-rs-mcp claude -p "In paper-rs, add a yellow sticky note saying 'stdio works' next to the Hello artboard." \ --allowedTools "mcp__paper-rs__*" # Codex works the same way: # codex mcp add paper-rs --url http://127.0.0.1:29980/mcp # codex mcp add paper-rs -- $CARGO_TARGET_DIR/release/paper-rs-mcpDone when the "Mac check" project is open in the window with the "Hello" frame and its heading (and the sticky note) visible, and ⌘Z removes each agent edit in one step.
-
Not carried over: comments, design tokens, AI image generation, video export, teams; moving layers between pages; remote image URLs (see README Known gaps).
P6 notes
- Export (
io/export.rs): scope = selection, each frame, page or all pages; format = PNG (1–4x, transparent or white), SVG or PDF. Several PNG/SVG outputs go into a chosen folder (named after the frames/pages); PDF is always one file with one page per item. Inspector Export section, ⌘⇧E / ⌘⌥⇧E / ⌘⌥P, copy as PNG (⌘⇧C) / SVG, and the CLI. - PDF (
io/pdf.rs): hand-written PDF 1.7 writer over the shared draw ops — paths (quadratics elevated), solid fills, axial/radial shadings (stitched functions for 3+ stops), images as Flate-compressed RGB XObjects with an alpha soft mask, strokes with caps/joins/dashes, ExtGState opacity and all 16 blend modes, frame clipping. Verified by structure tests and by rasterizing with pdftoppm. - Excalidraw import (
io/excalidraw.rs): scenes and clipboard JSON (rectangles, ellipses, diamonds, text, lines, arrows with bindings/elbows/heads, freedraw with pressures, images fromfiles, frames, sticky notes with their text, groups → group layers, rotation baked into paths, hand-drawn style kept with the element's seed and fill style);.excalidrawlibv1/v2 into the library. - SVG import (
io/svg_import.rs, usvg): paths with transforms, fills (solid, linear, radial), strokes, opacity, embedded raster images, nested SVG and text outlined with Inter. - Clipboard (
io/clipboard.rs): copy writes apaper-rs/clipboardJSON payload with image bytes to the system clipboard (arboard), so layers paste between pages, projects, windows and restarts; paste also takes Excalidraw JSON, SVG markup, images and plain text. Our own last copy pastes from the internal clipboard (offset on the same page). - Drag-and-drop of
.svg,.excalidraw,.excalidrawliband images onto the canvas, and ⌘O imports into the current page.
P5 notes
- Inspector: grouped macOS-style sections (header actions, alignment icons, position & size with per-corner radii, constraints / sizing, auto layout, text, fills with gradients and images, stroke, layer blend/opacity, hand-drawn, arrow, sticky). Number fields scrub and coalesce into one undo step; edits inside instances become overrides.
- Layers tree: drag rows to reorder — above / below a row, or into a frame or group (the middle of the row); the canvas position is kept and the move is one undo step.
- Command palette (⌘K, also ⌘⇧/): fuzzy search over every bound action, all tools, pages, library items and layers (select + zoom). Arrow keys, Enter, Esc.
- Shortcuts: one binding table drives dispatch, the grouped help panel (⌘/ or ?), the egui menu bar and the native macOS menu (⌘ chords become key equivalents; single keys are shown in titles). Added Tab/⇧Tab sibling selection, ⇧Enter parent, ⇧H/⇧V flip, ⌘⌥C/⌘⌥V copy/paste properties, ⌥-drag duplicate (one undo step), PageUp/PageDown pages, ⌘R rename, ⌘\ hide UI, ⇧0 zoom 100%, Excalidraw number keys. 100+ bindings, checked for conflicts by a test.
- Deferred: Figma's number-key opacity (the digits pick tools, as in Excalidraw), drag of multiple layers at once in the tree, customizable shortcuts.
P4 notes
- rough.js 4.6.4 port (
render/rough.rs): RNG, lines, curves, ellipses, SVG paths, solid, hachure, cross-hatch, zigzag and dots fills, plus Excalidraw's element → options mapping (adjustRoughness, dash patterns, fill weight, hachure gap). The test compares ten generator calls against real rough.js output (tests/fixtures/rough/, produced by running the MIT library under Node) to 1e-6, so the same seed draws the same wobble. Per-node toggle (⌘⌥H or the inspector), sloppiness 0/1/2, fill style, re-roll seed; with nothing selected the toggle sets the default for new shapes. - Arrows (
connector.rs): aConnectornode with start/end bindings (node, normalized fixed point, inside/orbit). Orbit ends stop at the target outline +5 + stroke/2, aimed at the other end's focus, and re-route in the fixup pass whenever a bound layer (or an ancestor) moves, resizes or is deleted. Straight, curved (Catmull-Rom) and elbow routes; elbow uses a grid + A* with Excalidraw's bend penalty and segment-count heuristic. Heads: arrow, triangle, dot, bar, diamond. Endpoint handles rebind; the midpoint handle bends. Labels on shapes defer to the shape when binding. - Quick diagram shapes: diamond tool (D), polygon/star, and quick-connect "+" knobs on a selected shape that duplicate it 80 px away and connect the two with a bound arrow.
- Sticky notes (S): colored note with padding 16, text wraps and the font shrinks (to 12) to fit, six colors.
- Laser pointer (K): a trail that fades over 1 s and shows the last 50 points, eased like Excalidraw; the event loop animates only while points are visible, then idles.
- Library: a shared
library.jsonin the projects folder (starter flowchart items on first use), a Library tab with tessellated previews, add-selection, rename, remove, insert (fresh ids, arrow bindings and instance links remapped). - Number keys 1–0 select tools like Excalidraw (1 select … 0 eraser).
- Deferred depth: Excalidraw's dynamic-AABB "side hack" in elbow routing, arrow labels and text bound inside shapes, multi-point arrows by clicking, freedraw with perfect-freehand (paper-rs keeps its own ink), rough rendering of text/ink, collaboration.
P3 notes
- Renderer rework. Nodes become backend-neutral draw ops (fills with solid / linear /
radial / image paints, strokes, glyph runs) in
render/draw.rs. The GPU path tessellates them in world space and caches per node (keyed by content, world rect, inherited opacity and zoom octave). Unchanged frames reuse the item list; the world vertex buffer is only re-uploaded when the item list changes, so panning costs one uniform write. Gradients use a ramp atlas; images are mipmapped textures; multiply/screen/darken/lighten have their own blend pipelines (premultiplied output). The SVG exporter consumes the same ops. - Model v3: fill layers, blend, stroke align/dash/caps/joins, per-corner radii, rich text
spans, auto layout, layout-child sizing and constraints, component flag and instance links,
groups, vector paths, polygons, stars. Page format v3 with a v2→v3 migration (
fill→fills). - Fixup pass (
Editor::fixup) runs inside every transaction: normalizes group bounds, syncs instances of edited components (respecting per-property overrides recorded by the inspector, text edits and drags), and re-runs auto layout from the outermost auto-layout ancestor. It undoes together with the edit. - Resize restores the drag-start snapshot each move and re-applies constraints/group scaling from it, so repeated resizes never drift.
- 10k elements:
tests/perf.rsbuilds 10 000 nodes (rects, ellipses, stars, text), all in view. Release: first frame ~140 ms (CPU tessellation of everything), cached frame ~3 ms, editing one node re-tessellates one node, panning re-uploads nothing. - Deferred depth: rounded-corner frame clipping, live (non-destructive) booleans, on-canvas gradient handles, rotation, variants, layout grids, min/max sizes, baseline alignment, and exact GPU compositing for the remaining 11 blend modes and group opacity.
P2 notes
- Each page is its own infinite canvas with its own undo history, selection and camera. The
active page lives in the editor; the others are parked in their slot (
PageStash). - Pages panel at the top of the left sidebar: click to switch, + to add, double-click to rename, right-click for Rename / Duplicate / Delete, drag to reorder. The last page can't be deleted.
- Paste on another page (or in another project) keeps the original position; paste on the same page offsets by 16 px.
- Deleting a page isn't undoable (the page file is removed at the next save).
P1 notes
- Projects live in
~/Documents/paper-rs/<name>/(PAPER_RS_HOMEoverrides):project.json(manifest: id, name, created/modified, ordered pages, active page),pages/<id>.json, andthumbnail.png. Deleting moves the folder to.trash/with atrashinfo.json, restorable from the Trash view. - Every write is temp file + fsync + rename + directory fsync (
io::atomic). Stale temp files from a crash are removed when the project opens. - Formats carry
format+version; loading runs a chain ofValue -> Valuemigrations. paper-rs 0.1.paper-rsfiles are page format v1 and migrate to v2 (tested). - Autosave: changes are written 1 s after the first unsaved edit (bounded latency even while editing, never during a drag), on page switch, when leaving the project and on quit. The event loop sleeps until the autosave deadline, so idle CPU stays at zero. Thumbnails refresh at most every 20 s and when leaving the project.
- Known gap: typing inside an open text edit is saved when the edit ends (it is one undo step).
Phase 3 verification log (2026-10-11)
cargo build --release: no warnings.cargo clippy --all-targets: clean.cargo fmt.cargo test: 99 unit tests + 42 integration tests (projects, figma, excalidraw, workflow, io, perf, headless), all passing; GPU tests on Mesa lavapipe.cargo check --target aarch64-apple-darwinand--target x86_64-pc-windows-gnupass (the macOS menu is generated from the shared menu table and type-checks).- Real window under Xvfb: idle CPU measured from
/proc/<pid>/statover 5 s after startup — 0 ticks on the home screen and 0 ticks in the editor. Autosave, tooltips and the laser trail wake the loop only while they need to. - 10k elements (
tests/perf.rs, release): cached frame ~3 ms, first frame at a new zoom octave ~140 ms, one edited node re-tessellates one node, panning uploads nothing. - Not yet run on Mac hardware (no Mac available to this session); see README Known gaps.
Phase 2 (paper-rs 0.1) record
| Phase / milestone | Status | Evidence |
|---|---|---|
| Phase 1: reverse engineering | ✅ done | analysis/PAPER_ARCHITECTURE.md. REA scoped run is summarized in the appendix; raw output in analysis/raw/ (gitignored). |
| Phase 2: design | ✅ done | analysis/DESIGN.md |
| M1: window + infinite canvas, smooth pan/zoom | ✅ done | tests/headless.rs::m1_*, editor scenario tests, m1_*.png, x11_window_xvfb.png |
| M2: freehand sketch with smoothing | ✅ done | ink.rs tests, m2_freehand_sketch_with_smoothing, m2_sketch.png |
| M3: shapes, frames, text | ✅ done | scenario tests, m3_shapes_frames_and_text, m3_*.png |
| M4: select/move/resize, multi-select, snapping | ✅ done | scenario tests, m4_select_move_resize_snap, m4_*.png |
| M5: layers panel, undo/redo | ✅ done | history.rs tests, undo_redo_round_trips_a_whole_session, m5_layers_and_undo_redo, m5_*.png |
M6: save/load .paper-rs, PNG/SVG export |
✅ done | io/* tests, m6_save_load_and_export, m6_svg_export_matches_gpu_render, m6_* |
| M7: keyboard shortcuts | ✅ done | editor/shortcuts.rs (conflict test), README table, in-app ? help |
| README (build/run, parity table, gaps) | ✅ done | README.md |
Verification log
cargo build --releasepasses with no warnings.cargo clippy --all-targetsis clean.cargo testruns 51 unit/scenario tests and 8 headless GPU tests, all passing. The GPU tests ran on Mesa lavapipe (llvmpipe, Vulkan) on the headless VPS.- The real windowed app was launched under Xvfb (X11) with the release binary and screenshotted.
It needed
libxkbcommon-x11andlibxcb-xkbplaced in~/.local/lib, since the VPS has no root access. cargo check --release --target aarch64-apple-darwinand--target x86_64-pc-windows-gnuboth pass.- The SVG export was rasterized with resvg and compared with the GPU PNG export: 0.004% of pixels differ.
Notes
- Commit history: the core model, the editor and the app/renderer/I/O layer landed as three feature commits. Each one builds in release mode and has its own tests. Milestones 1–7 are spread across them because the layers depend on each other: the app needs I/O, and the overlay needs the editor.
- REA's full-asar analysis stalled on
node_modules/typescript/lib/_tsc.jsafter 20+ minutes at 2.6 GB RSS. A scoped run on Paper's own sources finished in 65 s.
Next steps (not started)
- Image nodes (textured quads) and drag-and-drop of image files, to match
paper-asset:. - Per-node mesh cache keyed by revision and zoom bucket.
- Rotation, plus a bezier pen tool.
- Multiple pages, and system clipboard interop (SVG/PNG/text).
- Run and tune on macOS hardware, especially trackpad scroll feel and tablet pressure.