Added from ~/Development sessions: scene graph behaviors, auto-layout gotchas, section/frame rendering rules, .fig round-trip testing, Tauri permissions, icon conventions, ScrubInput cursor handling, culori usage, vueuse hooks.
4.6 KiB
4.6 KiB
OpenPencil
Vue 3 + CanvasKit (Skia WASM) + Yoga WASM design editor. Tauri v2 desktop, also runs in browser.
Commands
bun run check— lint + typecheck (run before committing)bun run format— oxfmt with import sortingbun test ./tests/engine— unit testsbun run test— Playwright visual regressionbun run tauri dev— desktop app with hot reload
Code conventions
@/import alias for cross-directory imports,./for same directory- No
any— use proper types, generics, declaration merging - No
!non-null assertions — use guards,?.,?? - Shared types (GUID, Color, Vector, Matrix, Rect) live in
src/types.ts - Window API extensions (showOpenFilePicker, queryLocalFonts) live in
src/global.d.ts - Use
culorifor color conversions — don't reimplement parseColor/colorToRgba - Use
@vueuse/corehooks (useEventListener, etc.) — don't do manual addEventListener/removeEventListener src/kiwi/kiwi-schema/is vendored — don't modify
Rendering
- Canvas is CanvasKit (Skia WASM) on a WebGL surface, not DOM
- Rendering is coalesced via rAF — call
scheduleRender(), notrenderNow() renderNow()is only for surface recreation and font loading (need immediate draw)- Resize observer uses rAF throttle, not debounce — debounce causes canvas skew (old WebGL surface stretched into resized element)
- Viewport culling skips off-screen nodes; unclipped parents are NOT culled (children may extend beyond bounds)
- Selection border width must be constant regardless of zoom — divide by scale
- Section/frame title text never scales — render at fixed font size, ellipsize to fit
- Rulers are rendered on the canvas (not DOM), with selection range badges that don't overlap tick numbers
Scene graph
- Nodes live in flat
Map<string, SceneNode>, tree viaparentIndexreferences - Frames clip content by default is OFF (unlike what you'd assume)
- When creating auto-layout, sort children by geometric position first
- Dragging a child outside a frame should reparent it, not clip it
- Layer panel tree must react to reparenting — watch for stale children refs
- Groups: creating a group must preserve children's visual positions
Components & instances
- Purple (#9747ff) for COMPONENT, COMPONENT_SET, INSTANCE — matches Figma
- Instance children map to component children via
componentIdfor 1:1 sync - Override key format:
"childId:propName"in instance'soverridesrecord - Editing a component must call
syncIfInsideComponent()to propagate to instances SceneGraph.copyProp<K>()typed helper — usesstructuredClonefor arrays
Layout
computeAllLayouts()must be called after demo creation and after opening .fig files- Yoga WASM handles flexbox; CSS Grid blocked on upstream (facebook/yoga#1893)
- Auto-layout creation (Shift+A) must recompute layout immediately to update selection bounds
UI
- Use reka-ui for UI components (Splitter, ContextMenu, DropdownMenu, etc.)
- Tailwind 4 for styling — no inline CSS, no component-level
<style>blocks - Mac keyboards: use
e.codenote.keyfor shortcuts with modifiers (Option transforms characters) - Splitter resize handles need inner div with
pointer-events-nonefor sizing (zero-width handle collapses without it) - Number input spinner hiding is global CSS in
app.css, not per-component - ScrubInput (drag-to-change number) — cursor and pointerdown on outer container, not inner spans
- Icons: use unplugin-icons with Iconify/Lucide (
<icon-lucide-*>) — don't use raw SVG or Unicode symbols - Sections are draggable by title pill, not by the area to the right of the title
File format
- .fig files use Kiwi binary codec — schema in
src/kiwi/codec.ts NodeChangeis the central type for Kiwi encode/decode- Vector data uses reverse-engineered
vectorNetworkBlobbinary format — encoder/decoder insrc/engine/vector.ts - showOpenFilePicker/showSaveFilePicker are File System Access API (Chrome/Edge), not Tauri-only — code has fallbacks
- Tauri detection:
IS_TAURIconstant fromsrc/constants.ts— don't use'__TAURI_INTERNALS__' in windowinline - .fig export: compression with fflate (browser) or Tauri Rust commands
- Test .fig round-trip by exporting and reimporting in Figma
Tauri
- Tauri v2 with plugin-dialog, plugin-fs, plugin-opener
- File system permissions must be configured in
desktop/tauri.conf.json— "Internal error" on save means missing permissions - Dev tools: add a menu item to toggle, don't rely on keyboard shortcut
Known issues
- Safari ew-resize/col-resize/ns-resize cursor bug (WebKit #303845) — fixed in Safari 26.3 Beta