On copy: serialize each text node's paragraph into an SkPicture and
embed the bytes (base64) in the OpenPencil clipboard payload.
On paste: if the font isn't available, render from the cached SkPicture
instead of the broken fallback. Text looks pixel-perfect without the
font installed — same behavior as Figma.
Missing font indicator:
- Orange ⚠ badge on canvas over text nodes with unavailable fonts
- Amber warning banner in the Typography panel listing missing families
- isFontLoaded() check in @open-pencil/core for querying font status
- Use Int32Array instead of Uint32Array for Kiwi pasteID (signed int field)
- Error toasts: don't auto-dismiss, show copy button, text is selectable
- Add clipboard roundtrip tests (encode → decode → verify)
- letterSpacing is an object {value, units} not a number — was NaN,
which silently breaks CanvasKit paragraph layout
- Add mapLetterSpacing() to handle PIXELS and PERCENT units
- Force textAutoResize to NONE (Yoga has no text measurement callback)
- Record undo entry for Figma paste with full subtree snapshots
- Add stackChildAlignSelf, fontWeight to NodeChange interface
Figma clipboard data includes VARIABLE_SET, VARIABLE, and other non-visual
types that were falling through to the default RECTANGLE case in mapNodeType.
These now join DOCUMENT and CANVAS in the skipTypes set.
The first render records an SkPicture before fonts are loaded (fallback
drawText path). When fonts finish loading asynchronously, the cached
picture was never invalidated — subsequent renders replayed stale picture
without paragraph text. Hovering a node forced a full re-render (volatile
overlay path), making text reappear. Un-hovering replayed the stale cache.
Fix: call invalidateScenePicture() at the end of loadFonts() so the next
render records a fresh picture with proper paragraph text.
29 tools: open_file, save_file, new_document + all 26 from @open-pencil/core.
Stdio for MCP clients (Claude Code, Cursor), HTTP (Hono + Streamable HTTP
with sessions) for scripts, browser extensions, CI.
Runs on both Bun and Node.js (via tsx).
Penpot 2.x ships with Rust/Skia WASM rendering enabled by default.
Update architecture diagram, rendering pipeline, and file format sections
to reflect the current state instead of the old SVG-based renderer.
- Cloudflare Worker + Durable Object server (packages/collab/)
- One DO per room with WebSocket Hibernation for near-zero idle cost
- Yjs sync protocol for document state (CRDT merge)
- Yjs awareness protocol for cursors, selection, presence
- Persists doc state to DO storage
- CORS headers for cross-origin WebSocket upgrade
- Client composable (src/composables/use-collab.ts)
- Y.Doc ↔ SceneGraph bidirectional binding
- Awareness: broadcasts cursor position, selection, name, color
- Auto-join from /share/:roomId URL
- Suppresses echo loops between Yjs and graph updates
- CollabPanel component (src/components/CollabPanel.vue)
- Share button generates room link, copies to clipboard
- Join flow: paste link or room ID
- Avatar stack with colored initials and tooltips
- Name input persisted to localStorage
- Remote cursor rendering in CanvasKit
- Colored arrow cursor + name label pill per remote peer
- Remote selection outlines in peer's color
- Only shows cursors on the same page
- SPA redirect for /share/* routes (Cloudflare Pages _redirects)