Root AGENTS.md becomes a map plus cross-cutting rules; folder rules live in one AGENTS.md per package and top-level folder, checked by check:docs. CONTRIBUTING.md owns process; README and the docs page link to it. Release preparation copies the root LICENSE into every published package, Core, CLI, and MCP gain READMEs, and the release workflow test packs its fixture in-process instead of through npm.
11 KiB
Core
Renderer, layout, editor, Figma API, tools, clipboard, vector conversion, and document I/O. Depends on scene-graph, pen, kiwi, and fig. Framework-neutral: no Vue, no app imports, no browser DOM; guard browser globals explicitly.
- Public surface is the compatibility barrel plus the subpaths listed in
packages/core/package.jsonexports; add a subpath there rather than deep-importing. - CanvasKit runtime loading is centralized in
@open-pencil/core/canvaskit. Headless raster export may dynamically loadcanvaskit-wasm/full; elsewhereimport typeand pass CanvasKit in. - Drawing and input share preview-aware geometry through
@open-pencil/core/geometry, built on Scene Graph matrices. Use it for world/screen transforms, inverses, bounds, and handle placement.
Layout
@open-pencil/yoga-layoutsupplies both flexbox and CSS Grid.- Recompute layout after demo creation and for each materialized or imported page; scope computation to the affected page or subtree where possible.
- The first Hug/Fill dimension mutation switches only that axis to Fixed; focus is non-destructive, and mode/value changes share one undo transaction.
Components and instances
- Component types use
#9747ff. - Component edits propagate through editor component sync in
packages/core/src/editor/components/; never hand-copy properties in app UI. Use Scene Graph copy helpers for nested values.
Vector conversion
- Bitmap-to-vector conversion lives in
packages/core/src/vector/vectorize/; app provider clients, preferences, and credential resolution live undersrc/app/editor/vectorize/in the app. Bound request and response sizes and validate provider-owned download URLs before importing returned SVG.
Tools (AI, MCP, CLI, WebMCP)
- Operations are
ToolDefs underpackages/core/src/tools/**;schema.tsdefines the contract and registries expose them. Each definition owns its native Valibotinput, execution/mutation metadata, and optional per-interface exposure exclusions (mcp,ai,webmcp). Exposure defaults to inclusion; adapters useisToolExposed(), then apply execution support and user permissions independently. - Infer arguments from the schema; derive effects and default capabilities from execution metadata. Do not maintain parameter DSLs or tool-name lists. Add work to the nearest existing domain and the appropriate registry.
packages/core/src/tools/ai-adapter.tsconverts ToolDefs for Vercel AI; the app binds them to the active editor'sFigmaAPIinsrc/app/ai/tools/index.ts. MCP v2 registration uses Standard Schema with Valibot JSON Schema conversion; AI and WebMCP adapters share the same input contract.packages/core/src/editor/history/atomic-tool.tsowns synchronous property/variable transactions; Scene Graph owns checkpoint recovery. AI, MCP, and WebMCP share this execution path. Async and structural tools cannot declare atomic property execution.- Shared scene-authoring guidance and tested examples live under
packages/core/src/design-jsx/reference/;reference.tscombines them with renderer metadata. Prompts underpackages/core/src/tools/prompts/and the app chat/ACP prompt compose that reference rather than copying it. Runbun run generate:authoring-referenceafter changes;check:authoring-reference(part ofcheck:docs) verifies the committed skill/docs copies. Do not edit generated reference files. - The installable agent skill is maintained in
skills/open-pencil/. Changes to agent-facing APIs, CLI/MCP behavior, or design authoring must update affected skill examples, prompts, and public documentation in the same change. Keep examples valid in their actual execution environment; do not advertise library exports as scripting globals unless exposed there. Prefer runtime discovery and canonical references over duplicated API/tool inventories. - MCP-only tools and transports:
packages/mcp/AGENTS.md. WebMCP registration and app completion:src/AGENTS.md.
Editor
createEditor() in packages/core/src/editor/create.ts assembles an EditorContext plus domain action modules for viewport, selection, pages, shapes, structure, components, clipboard, undo/history, text, variables, layout, color space, graph reads, and the tool registry. Editor is ReturnType<typeof createEditor>. Check the folder before adding behavior; keep new actions in the nearest domain module or folder instead of growing unrelated files. Modules share state through EditorContext, never through app code or Vue.
- All selection mutations go through
ctx.setSelectedIds()and all tool changes throughctx.setActiveTool()so events fire consistently. App code uses editor actions such asclearSelection(),select(), orsetTool(), never directstate.selectedIds =orstate.activeTool =assignments. - The editor exposes a typed nanoevents emitter. Event names and payloads live in
EditorEventsinpackages/core/src/editor/types.ts; graph events are bridged from SceneGraph bypackages/core/src/editor/graph-events.ts. Subscribe witheditor.onEditorEvent(event, handler); in Vue useuseEditorEvent()frompackages/vue/src/editor/events/use.ts. UI that only cares about graph data should use editor events for incremental surfaces such as the layer tree instead of watching repaint-only state. - Commands under
packages/core/src/editor/structure/(group, boolean, container wrap, flatten) andpackages/core/src/editor/components/are the canonical implementations of user actions. The Figma API, tools, and app call them or share their helpers; do not reimplement sizing, placement, or propagation elsewhere. - Live property controls use selected-node projections from
packages/core/src/editor/selection-state/nodes.ts: shallow reactive copies that receivenode:previewUpdatedpatches at property granularity. Never add preview invalidation to alluseSceneComputedconsumers; catalogs and unrelated controls must not refresh for geometry previews. Projection subscriptions belong to the consuming scope or session and must be disposed. - Numeric geometry edits own a
beginNodePreview()handle withupdate,commit, andcancel. It captures all affected fields and layout children, publishes the complete delta once, and restores exact originals on cancellation. Selection, page, and graph changes and disposal cancel the old edit; trailing input must not target the new selection. Controls must close previews even when a gesture returns to its starting value. - Rotation previews change through
setRotationPreview()androtation:preview-changed; cancellation must close the owning gesture without deselecting or committing it. - Renderer interaction policy uses explicit
beginInteractiveEdit()leases andisInteractiveEditing(), not undo batching. Release leases on every terminal path. Keep live queries callable across app facades that spread editor actions. packages/core/src/editor/history/atomic-tool.tsowns synchronous property/variable transactions for AI, MCP, and WebMCP tools; see Tools above.
Renderer
Canvas is CanvasKit (Skia WASM) on a WebGL surface, not DOM.
Invalidation
renderVersionis a canvas repaint (pan, zoom, hover);sceneVersionis a scene-graph mutation.requestRender()bumps both;requestRepaint()bumps onlyrenderVersion. UI that only cares about graph data must not watch repaint-only state.renderNow()is only for surface recreation and font loading, where an immediate draw is required.- The resize observer uses a rAF throttle, not a debounce; debounce causes canvas skew.
- Viewport culling skips off-screen nodes; unclipped parents are not culled because children may extend beyond bounds.
- Overscan images accelerate navigation; settled scenes rasterize existing retained pictures at the live viewport size and origin. Pixel-grid alignment alone does not guarantee Skia anti-aliasing parity. Keep settlement pending until the viewport pass completes; do not add a second viewport image cache.
Caches
- Bounded rendering caches share
packages/core/src/cache/resource.tsfor recency, count/weight accounting, and removal disposal. Domain adapters own keys, font/page/dependency invalidation, and sizing units; use non-touchingpeek()for FIFO or planning reads. Rejected insertions leave ownership with the caller. - Keep weak memos, async request registries, pools, and dependency-owned picture/path maps on their distinct lifetime policies.
- Paragraph construction is typed against
packages/core/src/canvas/text/paragraph-inputs.ts; the same inputs drive preparation-cache invalidation. Add a mutation case when extending that contract. Drawing borrows native paragraphs; the renderer owns their bounded cache and destruction.
Geometry and overlays
- Use
@open-pencil/core/geometryfor world/screen transforms, inverses, bounds, and handle placement instead of interpreting ancestor rotations or reflections independently. - Label drawing and hit testing share
packages/core/src/canvas/labels/{layout,transform,style}.ts, including paragraph measurements and unreflected label axes. - Selection border width is constant regardless of zoom: divide by scale. Section and frame title text never scales: render at a fixed font size and ellipsize to fit.
- Rulers are rendered on the canvas with selection range badges that do not overlap tick numbers. Remote cursors are Figma-style colored arrows with a white border and name pill, rendered in screen space.
Visual coverage
Pixel-affecting features need committed visual coverage, not only mock or geometry assertions. Add or update a Playwright canvas snapshot for changes to fills, gradients, images, blend modes, masks, boolean geometry, corners, strokes, shadows, blur, text rendering, or demo showcase scenes. Use targeted updates such as bun run test tests/e2e/canvas/fill-modes-visual.spec.ts --update-snapshots, then rerun the same test without --update-snapshots.
Figma API
FigmaAPI mimics Figma's Plugin API over the SceneGraph. openpencil eval, the AI and MCP tools, and the app bind to it. Tests live under tests/engine/figma/api/ and mirror the API surface.
- Shape follows Figma. Match
@figma/plugin-typingsat the version pinned in the rootpackage.json.packages/core/src/figma-api/compatibility.tstype-checks the supported surface againstPluginAPI; add every new member toSupportedPluginAPIthere. Model unsupported members explicitly rather than approximating them. - Behavior follows Figma. Read the Plugin API documentation for the member, and for anything observable (geometry, ordering, defaults, errors) run the same script in live Figma and record the observed result in the test.
figma-useandtests/figma/are the live oracle;tests/fixtures/figma-oracles/holds recorded values. - User actions are shared code. When an editor command does the same thing (group, ungroup, boolean, flatten, instance creation, component sync), call the implementation under
packages/core/src/editor/or its shared helper. Do not reimplement geometry, placement, or propagation here; new geometry helpers belong in Scene Graph. - Node proxies live in
packages/core/src/figma-api/proxy.tsandpackages/core/src/figma-api/accessors/;packages/core/src/figma-api/render-bounds.tsimplements Figma'sabsoluteRenderBoundssemantics and is the only geometry that is Figma-specific. packages/core/src/figma-api/index.tsstays under the 600-line limit by moving whole domains into sibling modules such ascomponents.tsandtext.ts, not by extracting generic helpers into this folder.