30 KiB
Progress
Last updated: 2026-10-24.
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): 53 tools at the time (61 now), using 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).
2026-10-25: 0.7.3 (QA sub-agent, helper agents, auto mode, mid-turn messages, isolated dev settings)
- QA sub-agent: Claude Code chats always get a Sonnet
qaagent that lints and screenshots built frames and returns only findings, so screenshots stay out of the main context. - Helper agents (
src/mcp/delegate.rs, Settings, Chat, Helper):delegatehandsqa,readorbuildtasks to a cheaper agent CLI (pi with MiniMax M3.1 Flash, opencode, an Ollama-backed agent, any command taking{prompt}). It drives PaperCraft throughpapercraft-cli, runs off the UI thread, and only its last 6,000 characters come back. With a helper set, the chat agent orchestrates: keeps the brief and taste, delegates review, bulk reading and fully specified edits. - Dev config isolation:
PAPERCRAFT_HOMEalso isolates the chat settings ($PAPERCRAFT_HOME/.config), so a second copy for testing never touches the installed app's settings. - Live check in an isolated copy:
delegateran the pi helper (MiniMax M3.1 Flash) as QA, a message added mid-turn went Waiting then Seen, and the agent drafted #13. - Fixes #10, #11, #12, #13:
- #10:
move_nodesto another page keeps mains' component keys and relinks every instance to moved nodes; newrename_filerenames a project. - #11: swapped nested instances persist as swap overrides (
InstanceLink.swap), preserving component identity across outer component updates and copies. - #12:
move_nodesto another page takes the moved nodes' annotations along; detached notes resolve asDetached #n. - #13:
get_computed_stylesreports a text colour bound to a variable asvar(--name).
- #10:
- Auto mode (the composer's Auto chip): when a turn ends on
ask_userquestions, the first (recommended) option of each is sent and the chat goes on, until Stop or a turn hits its budget. The agent is told to put its recommendation first, default what only the user can know, and end each turn with next steps. - The Present button uses a play icon drawn like the icons beside it. Composer chips wrap instead of running under Stop and Add. Top bar: the agent pill drops its text when it would run under the buttons on the right.
- While a chat turn works, the composer's Add (or Enter) queues a message (
src/mcp/steer.rs). It rides on the agent's next PaperCraft tool result for that chat, so every backend reads it mid-turn; the composer shows each as Waiting or Seen. What no tool call picked up is sent as the next message when the turn ends, or goes back into the box after Stop.
2026-10-24: 0.7.2 (UI kits, code links, issues from the app, design playbook)
- Code links (Figma Code Connect style): a main component carries its library import and a React and an egui template (
set_code_link). The React, Tailwind, inline JSX and egui exports write linked instances as that code, with overrides filled in and imports gathered once; the AI spec lists them as Library Components with a "use it, do not rebuild it" rule and stops describing their inner layers. - UI kits (
src/ui_kits.rs,apply_ui_kit): shadcn/ui, Ant Design, React Bits and egui, each with Light and Dark variables, tokens and components in its look and density, all linked. The chat brief asks for the code library and starts from its kit. - Issues from the app (
src/issues.rs): the agent'sreport_issuedraft card (Send or Discard) and a Report a PaperCraft issue form, filed on the repository in Settings (token in the keychain, else thetealogin), de-duplicated by title against open and closed issues. - design-playbook in the chat: with the
playbookCLI installed, the brief asks whether to follow the playbook and the agent looks rules up withdesign_rules, citing ids in its report. - Top bar: project name and page picker on one centred row; the two-row title had outgrown the bar and covered the canvas.
- Evidence:
src/ui_kits.rs,src/issues.rs,src/mcp/codegen.rs(linked_components_export_as_library_code),specs/ui-kits/spec.md.
2026-10-24: 0.7.1 (rename fix)
- Enter, Escape and clicking away end a rename again (projects on the home screen, library assets, versions). The fields took focus back before checking whether they had lost it.
2026-10-24: 0.7.0 (images, research, plugins, fewer tokens)
- Images in chat: paste from the clipboard (Cmd/Ctrl+V with an image-only clipboard now reaches the composer), thumbnails in the composer and in sent messages, a lightbox, image-only messages. Attachments are sent at most 1568 px (originals kept);
zoom_imagereads a region of the original. - Brief and finish:
ask_userdraws questions as a card of clickable answers (palette swatches, reference image tiles, the recommended answer marked). The chat rules ask for a brief before the first design, variables and components first, sizes from the agreed density, a short report and next steps as a card. - Web research (the composer's Web chip):
fetch_reference_images(public addresses only, icons, logos and backdrops dropped, social cards last), a References gallery that dims what the agent did not keep; on Claude Code a Haikuresearchersub-agent does the searching. - Image generation, opt-in (Settings, Chat, Images): Codex (offered once when signed in with a ChatGPT plan), Google Flow through
gf, or any command ({prompt},{aspect},{ratio},{out}, passed as data). - Plugins (Settings, Plugins): Avataaars, DiceBear, Iconify, Lorem Picsum, Flags, QR codes, placed with
<x-plugin use="…">, cached on disk. SVG import draws masked groups as images (Avataaars). - Tokens: lean Claude Code sessions (42.7k to 20.6k prompt tokens), deferred tool schemas and context editing on the API, a fresh session with a recap past 120k (or on demand from the meter), per-turn cost for resumed Claude Code sessions, a 1M context meter, a turn budget. Model and effort pickers for Claude Code.
- Also: page switcher in the Chat top bar, steps stay folded while a turn runs (no flicker),
lint_designasks 24 px targets on desktop screens, the installer keepspapercraft-mcpcurrent. - Evidence:
src/mcp/{refs,imagegen}.rs,src/plugins.rs,src/io/svg_import.rs,src/ui/{chat,chat_msgs,chat_agents,settings}.rs,src/chat/{anthropic,claude_code}.rs,tests/chat_tools.rs,specs/chat-tokens/spec.md.
2026-10-22: 0.6.1 (agents via ACP, annotations)
- Agents are found through the login shell's PATH (read once at startup, 3 s limit, merged with the app PATH and the usual folders), so an app opened from the Dock finds Claude Code, Codex and the rest. No install instructions anywhere; a missing agent shows Not found with Set path… and Docs.
- ACP backend (
src/chat/acp.rs): any agent that speaks the Agent Client Protocol over stdio, with PaperCraft's MCP server, streamed updates, permission flow, cancel and load. Registry as data insrc/chat/agents.rs: native agents found on PATH, npx agents off until enabled, custom agents by name, command and arguments. Verified here: OpenCode works; Gemini CLI's server currently refuses ACP clients; Cursor needscursor-agent login; Pi works throughpi-acpafter enabling. - Agent panel over the chat column (Detected, Enable via npx, Not found, API endpoints, Custom, search), a Connect an agent setup card, a sign-in card, and multiple
@ Selectionchips. - Settings → Chat redesigned: one agents list with details, endpoints, defaults, scrolling page on one label grid.
- Send bug: a turn that cannot start keeps the typed text and shows an error row with Retry, never a silent no-op (
a_failed_start_keeps_the_text_and_shows_an_error). - Annotations: notes with leader lines on a page, six categories with keys (style comes only from the category), project and per-page templates with built-ins and a Customize modal, view modes (⌘⇧A), Annotate tool (N), side view, inspector section, Annotate with AI, six MCP tools (
annotatebatches), and comments inget_design_specand the code exports. Exports and Present leave annotations out. - Evidence:
src/chat/{path,agents,acp}.rs,src/ui/{chat_agents,annotations,annotations_canvas}.rs,src/model/annotation.rs,src/editor/annotations.rs,src/mcp/tools.rs,tests/annotations*.rs,specs/chat-agents/,specs/annotations/,docs/chat.md,docs/annotations.md.
2026-10-21: Chat mode
- Chat layout next to the canvas (⌘J,
Design | Chat), with three backends: Claude Code and Codex through their CLIs (claude -pstream-json,codex exec --json) against the app's MCP server, and the Anthropic API with a key in the OS credential store. One event stream for all three; tool calls run through the same MCP dispatch, so runs, review and revert behave as before. - Thread: markdown replies, thinking row, tool cards, step groups, plan from
TodoWrite, approval for destructive tools on the API backend, run checkpoints with Restore to here, usage line and context meter,@ Selectionand image attachments, Stop. - Chats live in the project's
chats/folder. Settings → Chat holds CLI paths, the default backend and the API key. - Offline CLI fix: an offline save writes no camera, and a top-level shape created without a position goes to the next free spot instead of the origin.
- Evidence:
src/chat/,src/ui/chat.rs,src/ui/chat_msgs.rs,src/ui/chat_md.rs,src/ui/settings.rs,src/mcp/exec.rs,tests/fixtures/chat/,specs/chat/,docs/chat.md.
2026-10-21: Agent runs and change review
- Agent editing calls group into runs, snapshot each edited page as
Before agent run N, and end on 30 seconds idle,finish_working_on_nodes, or project switch. Runs persist underruns/and keep the newest 50. - Agent view lists runs and supports review, whole-run revert, per-change undo and accept all. Review adds canvas outlines and a finish toast; palette commands review or revert the last run.
- Id-paired page diffs power review; History version compare UI remains todo. Completed with full restore (including variables, modes and tokens), tool edit classification, and wasm support.
2026-10-21: Design libraries and offline CLI
- Variables are now collections with modes, slash groups, four types and aliases, stored in
design.json(project version 2; 0.4.0 refuses migrated projects). Each page keeps a derived cache; every edit is one undo step per page, and the page-only removals are gone. Per-page active mode per collection. - Fields bind to variables (stroke, text color, size, gap, padding, radius, font size, line height, opacity, visible) in the inspector and with
bind_variable; gradients do not bind yet. - Instances work across pages (
component_key, source page or library); deleting a main flags its instances. - Libraries: publish a snapshot (
library.json), enable, update with review (overrides kept), disable, and keep working when the source is missing. Variables modal, Libraries modal and Assets view in the UI;create_collection,delete_variables,bind_variableand five*_librarytools in MCP (61 tools). papercraft-cli call --offlineandpapercraft-cli shellrun the tools in-process on the project store, refuse a project the app has open, and fall back to the software adapter for screenshots. Seedocs/agents.md.- Evidence:
src/variables.rs,src/editor/design.rs,src/libraries.rs,src/cli.rs,tests/{libraries,cross_page_components,variables_ui,assets_panel,cli_offline}.rs, specs inspecs/design-libraries/andspecs/cli-offline/.
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.