---
url: 'https://openpencil.dev/user-guide/canvas-navigation.md'
description: 'Panning, zooming, and the hand tool in OpenPencil.'
---
# Canvas Navigation
The canvas is your infinite workspace. You can pan and zoom freely to navigate your design.
## Panning
Move the visible area of the canvas without affecting any objects.
* Space + drag — hold Space and drag anywhere on the canvas
* **Middle mouse drag** — press and drag the middle mouse button
* **Two-finger trackpad** — swipe with two fingers on a trackpad
## Hand Tool
Press H to activate the hand tool for continuous panning. Any drag on the canvas pans the viewport without needing to hold Space. Switch to another tool (e.g., **V** for Select) to deactivate.
## Zooming
Zoom in and out centered on your cursor position.
* Ctrl + scroll (or ⌘ + scroll on Mac) — scroll up to zoom in, scroll down to zoom out
* **Pinch gesture** — pinch on a trackpad to zoom in/out
* **Keyboard shortcuts** — see table below
Pinch-to-zoom on UI panels (layers, properties) is prevented so it doesn't accidentally change the browser zoom level.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Pan | Space + drag | Space + drag |
| Hand tool | H | H |
| Zoom in | ⌘+ | Ctrl + + |
| Zoom out | ⌘− | Ctrl + − |
| Zoom to 100% | ⌘0 | Ctrl + 0 |
## Tips
* Zooming always targets the cursor position, so point at what you want to see closer.
* The hand tool is useful when you need to pan frequently — it stays active until you switch tools.
* See [Selection & Manipulation](./selection-and-manipulation) for how to work with objects on the canvas.
---
---
url: 'https://openpencil.dev/user-guide/selection-and-manipulation.md'
description: >-
Selecting, moving, resizing, rotating, duplicating, and organizing nodes in
OpenPencil.
---
# Selection & Manipulation
Select objects to move, resize, rotate, duplicate, and organize them on the canvas.
## Selecting
* **Click** a node to select it (deselects everything else)
* Shift + click to add or remove a node from the current selection
* **Marquee drag** — drag on empty canvas to draw a selection rectangle; all intersecting nodes are selected on release
* ⌘A — select all nodes on the current page
* **Click empty canvas** — deselect all
## Moving
* **Drag** a selected node to move it (all selected nodes move together)
* **Arrow keys** — nudge selected nodes by 1 px
* Shift + arrow keys — nudge by 10 px
## Resizing
Selected nodes show 8 resize handles (4 corners + 4 edge midpoints). Drag any handle to resize.
* Shift + drag a corner handle to constrain proportions
## Rotating
Hover just outside a corner handle to see the rotation cursor. Drag to rotate.
* Shift + drag snaps rotation to 15° increments
## Duplicating
* Alt + drag (⌥ + drag on Mac) — duplicate the selected node and move the copy
* ⌘D — duplicate in place
## Deleting
Press Backspace or Delete to remove all selected nodes.
## Z-Order
Change the stacking order of nodes within their parent:
* **]** — bring to front (top of sibling list)
* **\[** — send to back (bottom of sibling list)
## Visibility & Lock
* ⇧⌘H — toggle visibility. Hidden nodes don't render but stay in the layers panel.
* ⇧⌘L — toggle lock. Locked nodes can't be selected or moved on canvas.
## Move to Page
Move selected nodes to a different page via the [context menu](./context-menu). The nodes are reparented under the target page's canvas.
## Sections
Drawing a section on the canvas automatically adopts overlapping sibling nodes as children of the new section.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Select all | ⌘A | Ctrl + A |
| Duplicate | ⌘D | Ctrl + D |
| Duplicate + move | ⌥ + drag | Alt + drag |
| Delete | ⌫ / Delete | Backspace / Delete |
| Nudge 1 px | Arrow keys | Arrow keys |
| Nudge 10 px | ⇧ + Arrow keys | Shift + Arrow keys |
| Bring to front | ] | ] |
| Send to back | \[ | \[ |
| Toggle visibility | ⇧⌘H | Shift + Ctrl + H |
| Toggle lock | ⇧⌘L | Shift + Ctrl + L |
## Tips
* Use the [Layers & Pages](./layers-and-pages) panel to see and reorder nodes when they overlap.
* See [Context Menu](./context-menu) for additional actions like grouping and component creation.
---
---
url: 'https://openpencil.dev/user-guide/context-menu.md'
description: >-
Right-click context menu actions in OpenPencil — clipboard, z-order, grouping,
components, and more.
---
# Context Menu
Right-click on the canvas to open the context menu. If you right-click on a node, it is selected first. Right-clicking on empty canvas clears the selection.
## Copy/Paste as
The **Copy/Paste as** submenu offers additional clipboard formats for the selected node(s):
| Action | Shortcut (Mac) | Shortcut (Win/Linux) |
|--------|----------------|----------------------|
| Copy as text | — | — |
| Copy as SVG | — | — |
| Copy as PNG | ⇧⌘C | Shift + Ctrl + C |
| Copy as JSX | — | — |
* **Copy as text** — copies visible text content from the selection
* **Copy as SVG** — copies the node tree as SVG markup
* **Copy as PNG** — renders at 2× and places on the system clipboard (paste into Slack, Notion, etc.)
* **Copy as JSX** — copies the OpenPencil JSX representation for use with `renderJsx()`
## Clipboard Actions
| Action | Shortcut (Mac) | Shortcut (Win/Linux) |
|--------|----------------|----------------------|
| Copy | ⌘C | Ctrl + C |
| Cut | ⌘X | Ctrl + X |
| Paste here | ⌘V | Ctrl + V |
| Duplicate | ⌘D | Ctrl + D |
| Delete | ⌫ | Backspace / Delete |
Clipboard actions are disabled when nothing is selected (except Paste, which is available when the clipboard has content).
## Z-Order
| Action | Shortcut |
|--------|----------|
| Bring to front | ] |
| Send to back | \[ |
Moves the selected node to the top or bottom of its parent's child list.
## Grouping
| Action | Shortcut (Mac) | Shortcut (Win/Linux) |
|--------|----------------|----------------------|
| Group | ⌘G | Ctrl + G |
| Ungroup | ⇧⌘G | Shift + Ctrl + G |
| Add auto layout | ⇧A | Shift + A |
* **Group** requires 2 or more selected nodes
* **Ungroup** appears when a group is selected — children are reparented to the group's parent
* **Add auto layout** wraps the selection in a new [auto layout](./auto-layout) frame
## Component Actions
Component actions are displayed in purple to match the component color theme.
| Action | Shortcut (Mac) | Shortcut (Win/Linux) | Available on |
|--------|----------------|----------------------|--------------|
| Create component | ⌥⌘K | Ctrl + Alt + K | Frames, groups, multi-selection |
| Create component set | ⇧⌘K | Shift + Ctrl + K | 2+ selected components |
| Create instance | — | — | Components (no shortcut) |
| Go to main component | — | — | Instances |
| Detach instance | ⌥⌘B | Ctrl + Alt + B | Instances |
See [Components](./components) for details on the component workflow.
## Visibility & Lock
| Action | Shortcut (Mac) | Shortcut (Win/Linux) |
|--------|----------------|----------------------|
| Hide / Show | ⇧⌘H | Shift + Ctrl + H |
| Lock / Unlock | ⇧⌘L | Shift + Ctrl + L |
The label toggles based on the node's current state (e.g., "Hide" for a visible node, "Show" for a hidden one).
## Move to Page
The **Move to page** submenu lists all pages except the current one. Select a page to reparent the selected nodes under that page's canvas.
## Tips
* Right-clicking empty canvas gives you access to Paste — useful for placing content at a specific location.
* Component actions only appear when relevant (e.g., "Create instance" only for component nodes).
* The context menu mirrors the keyboard shortcuts — it's a good way to discover shortcuts you don't know yet.
---
---
url: 'https://openpencil.dev/user-guide/drawing-shapes.md'
description: >-
Creating rectangles, ellipses, lines, frames, sections, polygons, and stars in
OpenPencil.
---
# Drawing Shapes
The bottom toolbar provides tools for creating shapes, frames, and sections. Select a tool, then click and drag on the canvas to draw.
## Toolbar Tools
| Tool | Shortcut | Description |
|------|----------|-------------|
| Rectangle | R | Draws a rectangle |
| Ellipse | O | Draws an ellipse |
| Line | L | Draws a line |
| Frame | F | Draws a frame (container for other nodes) |
| Section | S | Draws a section (auto-adopts overlapping siblings) |
## Shapes Flyout
The shapes flyout (accessible from the toolbar) includes additional shapes:
* **Polygon** — creates a polygon with 3 sides by default (triangle)
* **Star** — creates a 5-pointed star with 0.38 inner radius
Polygon and Star have no keyboard shortcut — access them from the shapes flyout in the toolbar.
## Constrained Drawing
Hold Shift while dragging to constrain the shape:
* Rectangle → square (equal width and height)
* Ellipse → circle
* Line → snaps to 0°/45°/90° angles
## Shape Properties
After drawing a shape, select it to edit its properties in the Design tab of the properties panel.
### Fill
Every shape can have a fill. The fill section supports:
* **Solid color** — pick via the HSV color picker or type a hex value
* **Gradient** — Linear, Radial, Angular, or Diamond with editable gradient stops
* **Image** — select an image file as the fill
### Stroke
Add an outline to any shape. Stroke properties include:
* **Width** — uniform or per-side (Top/Right/Bottom/Left) via the side selector dropdown
* **Color** — solid color with opacity
* **Alignment** — Inside, Center, or Outside the shape boundary (clip-based rendering matches Figma behavior)
* **Cap style** — None, Round, Square, Arrow Lines, Arrow Equilateral (for open paths)
* **Join style** — Miter, Bevel, Round
* **Dash pattern** — dash-on/dash-off
### Corner Radius
Available for rectangles, frames, components, and instances. Click the independent corners toggle to set each corner (top-left, top-right, bottom-left, bottom-right) separately.
### Effects
Add visual effects from the Effects section:
* **Drop Shadow** — offset, blur radius, spread, color
* **Inner Shadow** — same controls, rendered inside the shape
* **Layer Blur** — blurs the entire node
* **Background Blur** — blurs content behind the node
* **Foreground Blur** — blurs content in front
Click **+** to add an effect. Each effect row is collapsible with inline controls. Toggle the eye icon to enable/disable an effect.
## Frames and Sections
**Frames** are containers. Drag shapes into a frame to make them children. Frames can clip their content (off by default) and support [auto layout](./auto-layout).
**Sections** are top-level containers that automatically adopt overlapping sibling nodes when drawn. They're useful for organizing large canvases into logical areas. Sections display a title pill that you can drag.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Rectangle tool | R | R |
| Ellipse tool | O | O |
| Line tool | L | L |
| Frame tool | F | F |
| Section tool | S | S |
| Constrain to square/circle | Shift + drag | Shift + drag |
## Tips
* Sections can only exist at the top level — they can't be nested inside frames.
* Use frames with [auto layout](./auto-layout) to build responsive layouts.
* [Export](./exporting) individual shapes or groups as images via the properties panel or context menu.
---
---
url: 'https://openpencil.dev/user-guide/text-editing.md'
description: >-
Creating and editing text with rich formatting, fonts, and inline editing in
OpenPencil.
---
# Text Editing
Create text nodes and edit them directly on the canvas with full rich text support.
## Creating Text
Press T to activate the text tool, then click on the canvas. An empty text node appears with a blinking cursor — start typing immediately.
## Inline Editing
Double-click any existing text node to enter inline editing mode. A blue outline appears around the text to indicate edit mode. Click outside the text node to commit and exit editing.
Text is rendered directly on the canvas — there's no separate text input overlay.
## Cursor Navigation
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Move left/right | ← / → | ← / → |
| Move up/down | ↑ / ↓ | ↑ / ↓ |
| Move by word | ⌥← / ⌥→ | Ctrl + ← / Ctrl + → |
| Move to line start/end | ⌘← / ⌘→ | Home / End |
Hold Shift with any movement key to extend the selection.
## Text Selection
* **Click** inside a text node to position the cursor
* **Click + drag** to select a range of text
* **Double-click** a word to select it
* **Triple-click** to select all text in the node
## Rich Text Formatting
Apply formatting to selected text, or toggle the style for the entire node when nothing is selected.
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Bold | ⌘B | Ctrl + B |
| Italic | ⌘I | Ctrl + I |
| Underline | ⌘U | Ctrl + U |
Strikethrough is available via the **S** toggle button in the Typography section of the properties panel (no keyboard shortcut — ⌘S is used for Save).
Formatting is applied per character. When you type between a bold and regular segment, the new text inherits the style of the preceding segment.
The **B / I / U / S** toggle buttons in the Typography section of the properties panel also apply formatting.
## Editing Operations
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Delete word before cursor | ⌥⌫ | Ctrl + Backspace |
| Delete to line start | ⌘⌫ | — |
| Cut | ⌘X | Ctrl + X |
| Copy | ⌘C | Ctrl + C |
| Paste | ⌘V | Ctrl + V |
## Font Picker
Open the font picker in the Typography section of the properties panel to change the font family. The picker features:
* **Search filter** — type to narrow the font list
* **Font preview** — each font name is rendered in its own typeface
* **Virtual scroll** — handles large font lists efficiently
* **Scroll-to-current** — the current font is highlighted when the picker opens
## Font Weight
Change the font weight in the Typography section of the properties panel. Available weights depend on the selected font family (e.g., Regular, Medium, Bold, Black).
## Font Sources
* **Default font** — Inter is loaded automatically
* **Desktop app** — all system fonts are available
* **Browser** — system fonts are available in Chrome and Edge
## Tips
* The font list is preloaded at startup so the picker opens without delay.
* IME input (Chinese, Japanese, Korean) is fully supported.
* Rich text formatting is preserved when opening and saving .fig files.
* See [Components](./components) for how text overrides work in component instances.
---
---
url: 'https://openpencil.dev/user-guide/pen-tool.md'
description: Drawing vector paths with bezier curves using the pen tool in OpenPencil.
---
# Pen Tool
The pen tool creates vector paths using a vector network data model, compatible with Figma's .fig format.
## Activating
Press P to activate the pen tool.
## Placing Points
* **Click** to place a corner point (straight-line segment)
* **Click + drag** to place a curve point with bezier tangent handles — the drag direction and length control the curve shape
* **Hold Space** while dragging (without releasing the mouse button) to move the point itself
Click multiple points to build a path segment by segment. A preview line extends from the last placed point to your cursor as you move.
## Closing a Path
Click on the **first point** of the path to close it into a loop. Closed paths can be filled.
## Open Paths
Press Escape to commit the current path as an open path. Open paths render as strokes only — they're not filled.
## Vector Networks
Paths in OpenPencil use vector networks — a more flexible model than simple point lists that supports branching paths and complex topology. This is the same model Figma uses, so paths round-trip perfectly in .fig files.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Pen tool | P | P |
| Commit open path | Escape | Escape |
## Tips
* The preview line always starts from the last placed point — it won't jump to (0,0).
* Drag longer when placing a curve point to make the curve wider.
* After creating a path, use the properties panel to adjust its fill, stroke, and effects.
---
---
url: 'https://openpencil.dev/user-guide/layers-and-pages.md'
description: 'Managing layers, pages, and the properties panel in OpenPencil.'
---
# Layers & Pages
The editor interface has three main panels: layers (left), canvas (center), and properties (right). All panels are resizable by dragging the dividers.
## Layers Panel
The layers panel on the left displays the document hierarchy as a tree.
### Tree View
Nodes are shown in a collapsible tree. Click the chevron next to a frame, group, or component to expand or collapse its children.
### Drag Reorder
Drag layers to reorder them. Nodes higher in the list render on top.
### Visibility Toggle
Click the eye icon next to any layer to hide or show it on the canvas. Hidden nodes remain in the tree.
### Rename
Double-click a layer name to rename it inline. Press Enter or click away to commit, Escape to cancel.
### Selection Sync
Clicking a layer in the panel selects the corresponding node on the canvas, and vice versa.
## Pages Panel
The pages panel shows all pages in the document.
* **Switch page** — click a page tab to make it active. The canvas switches to that page and restores its viewport position.
* **Add page** — click the add button to create a new page
* **Delete page** — remove the current page
* **Rename page** — double-click the page name for inline editing. Pressing Enter or Escape, or clicking away, commits the rename.
Each page has its own canvas and viewport state.
## Properties Panel
The properties panel on the right has three tabs:
### Design Tab
Shows the properties of the selected node(s), organized in sections:
* **Appearance** — opacity, corner radius (with independent corner toggle), visibility
* **Fill** — solid color, gradients (linear, radial, angular, diamond), image fills, variable bindings
* **Stroke** — color, width, cap, join, dash pattern
* **Effects** — drop shadow, inner shadow, layer blur, background blur, foreground blur
* **Typography** — font family, size, weight, B/I/U/S formatting buttons (visible for text nodes)
* **Layout** — [auto layout](./auto-layout) controls (visible for auto-layout frames)
* **Export** — scale, format, and export button (see [Exporting](./exporting))
When no nodes are selected, the Design tab shows page-level properties including the canvas background color.
### Code Tab
Displays the selected node as code with syntax highlighting, line numbers, and a copy button. A format toggle lets you switch between two output modes:
* **OpenPencil JSX** — custom component tree compatible with `renderJsx()` for programmatic round-trip
* **Tailwind CSS v4** — HTML with utility classes (`
`) ready to paste into React/Vue projects
### AI Tab
An AI chat interface (also toggled with ⌘J) that can create and modify design elements via natural language. Supports multiple AI models through OpenRouter.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Toggle AI chat | ⌘J | Ctrl + J |
## Mobile Layout
On mobile and small screens, the side panels are replaced by a swipeable bottom drawer. Tabs at the top of the drawer switch between Layers, Properties, Design, and Code views. The toolbar collapses to a compact horizontal strip with category switching.
## Tips
* Panel widths are saved automatically — they persist across reloads.
* Use the layers panel to find overlapping nodes that are hard to click on the canvas.
* The [context menu](./context-menu) provides additional actions for selected nodes.
* See [Selection & Manipulation](./selection-and-manipulation) for z-order shortcuts (]/\[) and visibility/lock toggles.
---
---
url: 'https://openpencil.dev/user-guide/exporting.md'
description: >-
Export images, SVG, and .fig subsets, and open .fig or .pen documents in
OpenPencil.
---
# Exporting
Export individual nodes as images or `.fig` subsets, and open full `.fig` or `.pen` documents.
## Image Export
Select a node and use the Export section in the properties panel.
### Export Settings
* **Scale** — 0.5×, 0.75×, 1×, 1.5×, 2×, 3×, or 4× (hidden for SVG — vectors are resolution-independent)
* **Format** — PNG (transparent background), JPG (white background), WEBP (transparent background), SVG (vector), `.fig` (native document subset)
You can add multiple export settings to export the same node at different scales or formats in one go. A live preview with a checkerboard background shows what will be exported.
### Export Methods
| Method | Mac | Windows / Linux |
|--------|-----|-----------------|
| Keyboard shortcut | ⇧⌘E | Shift + Ctrl + E |
| Context menu | Right-click → Export… | Right-click → Export… |
| Properties panel | Click "Export" button | Click "Export" button |
The exported file is saved via a native dialog (desktop) or browser download.
## Copy/Paste as
In addition to file export, you can copy the selection to the clipboard in multiple formats via the context menu (right-click → Copy/Paste as):
| Action | Shortcut (Mac) | Shortcut (Win/Linux) |
|--------|----------------|----------------------|
| Copy as text | — | — |
| Copy as SVG | — | — |
| Copy as PNG | ⇧⌘C | Shift + Ctrl + C |
| Copy as JSX | — | — |
* **Copy as text** — copies visible text content from the selection
* **Copy as SVG** — copies the selection as SVG markup (paste into code editors, Inkscape, etc.)
* **Copy as PNG** — renders at 2× and copies to the clipboard (ready to paste into Slack, Notion, etc.)
* **Copy as JSX** — copies the OpenPencil JSX representation (compatible with `renderJsx()`)
## .fig File Operations
OpenPencil uses the .fig format for full documents — the same binary format as Figma.
### Opening Files
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Open file | ⌘O | Ctrl + O |
A file picker dialog opens, filtered for `.fig` and `.pen` files. On the desktop app, this uses the native OS dialog.
### Saving Files
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Save | ⌘S | Ctrl + S |
| Save As | ⇧⌘S | Shift + Ctrl + S |
* **Save** overwrites the currently open file without a dialog
* **Save As** opens a save dialog to choose a new location
Saved files are compressed and include a thumbnail image for preview in file browsers.
### Round-trip Compatibility
Files exported from OpenPencil can be opened in Figma, and vice versa. The .fig format preserves all node types, properties, fills, strokes, effects, vector data, and layout settings.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Export selection | ⇧⌘E | Shift + Ctrl + E |
| Copy as PNG | ⇧⌘C | Shift + Ctrl + C |
| Open file | ⌘O | Ctrl + O |
| Save | ⌘S | Ctrl + S |
| Save As | ⇧⌘S | Shift + Ctrl + S |
## Tips
* Use 2× or 3× scale when exporting for high-DPI screens.
* JPG always uses a white background — use PNG or WEBP if you need transparency.
* Use SVG export when you need a vector format for further editing in Illustrator, Inkscape, or code.
* The thumbnail in exported .fig files enables preview in file browsers and Figma's file picker.
---
---
url: 'https://openpencil.dev/user-guide/auto-layout.md'
description: >-
Flex and grid layout in OpenPencil — direction, gap, padding, alignment, child
sizing, and CSS Grid tracks.
---
# Auto Layout
Auto layout positions children automatically within a frame. It supports two modes: **flex** (horizontal/vertical flow) and **grid** (rows and columns with track sizing).
## Enabling Auto Layout
* Select a frame and press ⇧A (Shift + A) to toggle auto layout on or off
* Select loose nodes (without a parent frame) and press ⇧A to wrap them in a new auto-layout frame
When wrapping a selection, nodes are sorted by visual position: left-to-right for horizontal layout, top-to-bottom for vertical.
## Layout Direction
Choose how children are arranged:
* **Horizontal** — children flow left to right
* **Vertical** — children flow top to bottom
* **Wrap** — children wrap to the next row/column when they run out of space
## Spacing
### Gap
The space between adjacent children. Set a single value that applies between all children.
### Padding
The space between the frame edge and its children. Set a uniform value for all sides, or expand to set each side independently (top, right, bottom, left).
## Alignment
### Justify (main axis)
Controls how children are distributed along the layout direction:
* **Start** — children pack to the beginning
* **Center** — children are centered
* **End** — children pack to the end
* **Space between** — children spread with equal space between them
### Align (cross axis)
Controls how children are positioned perpendicular to the layout direction:
* **Start** — children align to the start
* **Center** — children are centered
* **End** — children align to the end
* **Stretch** — children stretch to fill the cross axis
## Child Sizing
Each child in an auto-layout frame can have its own sizing mode:
* **Fixed** — uses the child's explicit width/height
* **Fill** — stretches to fill available space in the parent
* **Hug** — shrinks to fit the child's content
## Drag Reordering
Within an auto-layout frame, drag a child to reorder it among its siblings. A visual insertion indicator shows where the child will be dropped.
## Properties Panel
When an auto-layout frame is selected, the Layout section in the properties panel shows all auto-layout controls: direction, gap, padding, justify, and align.
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Toggle auto layout | ⇧A | Shift + A |
## CSS Grid
Grid layout arranges children in rows and columns with explicit track sizing.
### Enabling Grid
Select a frame with auto layout enabled and click the grid icon in the layout toolbar to switch from flex to grid.
### Track Sizing
Define column and row tracks with three sizing modes:
* **fr** — fractional unit, divides available space proportionally
* **px** — fixed pixel size
* **auto** — sizes to fit content
Example: three columns of `1fr 200px 1fr` creates a layout with a fixed center column and flexible sides.
### Grid Gaps
Set separate horizontal (column) and vertical (row) gaps between cells.
### Child Positioning
Children are placed into grid cells automatically in row order. You can override placement with column/row start and span values in the child's layout properties.
### JSX and Tailwind Export
Grid layouts export to JSX with Tailwind classes: `grid grid-cols-3`, `gap-x-4 gap-y-2`, `col-start-2 row-span-2`.
## Tips
* Auto layout recomputes immediately after creation, so the selection bounds update right away.
* Nest auto-layout frames for complex responsive layouts (e.g., a vertical frame containing horizontal rows).
* Use "Fill" sizing to make a child take up remaining space, like a flex-grow: 1 in CSS.
* Use grid for dashboard layouts, galleries, and forms — anything with a two-dimensional structure.
* See [Drawing Shapes](./drawing-shapes) for creating the frames that auto layout applies to.
* See [Components](./components) for using auto layout within reusable components.
---
---
url: 'https://openpencil.dev/user-guide/components.md'
description: >-
Creating reusable components, instances, component sets, overrides, and live
sync in OpenPencil.
---
# Components
Components are reusable design elements. Edit the main component and all its instances update automatically.
## Creating a Component
Select a frame or group and press ⌥⌘K (Ctrl + Alt + K). The selection becomes a reusable component.
If you select multiple nodes, they're wrapped in a new component positioned at their bounding box.
Components display a purple label with a diamond icon above them.
## Component Sets
Select two or more components and press ⇧⌘K (Shift + Ctrl + K) to combine them into a component set — a container with a dashed purple border and 40 px padding around its children. Component sets are useful for grouping variants (e.g., button states).
## Creating Instances
Right-click a component and select **Create instance** from the context menu. The instance appears 40 px to the right of the source component, visually identical.
Instance creation is available only through the context menu — there's no toolbar button.
## Detaching an Instance
Select an instance and press ⌥⌘B (Ctrl + Alt + B) to detach it. The instance becomes a regular frame with no link to the original component. All overrides are baked in.
## Go to Main Component
Right-click an instance and select **Go to main component**. The editor navigates to and selects the main component, switching pages if needed.
## Live Sync
When you edit a component, all its instances update automatically. Synced properties include:
* Width and height
* Fills, strokes, and effects
* Opacity and corner radii
* Layout properties (auto layout settings)
* Clips content setting
Sync triggers automatically after node updates, moves, and resizes within a component.
## Overrides
Instances can override specific properties without breaking the sync link. When a property is overridden on an instance, that property is skipped during sync — other properties continue to update from the main component.
### Overridable Properties
Child-level overrides support: name, text, font size, font weight, font family, plus all visual and layout properties (fills, strokes, effects, opacity, corner radii, size).
### New Children
When you add a child to a component, all existing instances gain a cloned copy automatically. Child order in instances always matches the component.
## Hit Testing
Components and instances are opaque containers — clicking on a child selects the component itself, not the child. **Double-click** to enter the component and select children inside it.
## Visual Treatment
| Element | Appearance |
|---------|------------|
| Component label | Purple with diamond icon, always visible |
| Instance label | Purple with diamond icon, always visible |
| Component set border | Dashed purple outline |
## Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|--------|-----|-----------------|
| Create component | ⌥⌘K | Ctrl + Alt + K |
| Create component set | ⇧⌘K | Shift + Ctrl + K |
| Detach instance | ⌥⌘B | Ctrl + Alt + B |
## Tips
* Editing text inside an instance creates an override — the text won't be overwritten when the component changes.
* Use component sets to organize variants (e.g., Primary/Secondary/Disabled button states).
* Double-click into a component before editing its children — single click selects the component container.
* See [Context Menu](./context-menu) for all component-related actions.
---
---
url: 'https://openpencil.dev/user-guide/variables.md'
description: 'Design variables, collections, modes, and fill bindings in OpenPencil.'
---
# Variables
Variables store reusable design tokens — colors, spacing values, and other properties — that can be bound to nodes. Change a variable's value and every node using it updates.
## Opening the Variables Dialog
With no nodes selected, the Design tab shows page-level properties including a Variables section with collection and variable counts. Click the settings icon to open the variables dialog.
## Collections
Variables are organized into collections. Each collection appears as a tab in the dialog.
* **Switch collection** — click a tab
* **Rename collection** — double-click the tab name
## Modes
Each collection can have multiple modes (e.g., Light and Dark). Modes appear as columns in the variables table. A variable has a value for each mode.
### Adding Collections and Modes
Create a new collection from the dialog toolbar. Add modes to an existing collection to support theme variants or responsive breakpoints.
## Managing Variables
The variables table uses resizable columns: Name, plus one column per mode.
* **Create variable** — click the "+ Create variable" button
* **Edit name** — click the variable name cell to edit inline
* **Edit value** — click any value cell to change it for that mode
* **Search** — type in the search bar to filter variables by name
### Color Variables
Color variables display an inline color input with a picker. Click the swatch to open the color picker and select a new color.
## Binding Variables to Fills
In the Fill section of the properties panel, use the variable picker to bind a color variable to a node's fill.
* **Bind** — select a color variable from the picker. The fill shows a purple badge with the variable name.
* **Detach** — click the detach button on the badge to remove the binding. The fill reverts to the resolved color value.
When the variable's value changes (or when switching modes), all bound fills update automatically.
## Tips
* Use collections to group related tokens (e.g., "Primitives" for raw colors, "Semantic" for role-based aliases, "Spacing" for layout values).
* Modes are useful for theme switching — define Light and Dark mode values in the same collection.
* Variables support aliases — a "Semantic" collection can reference values from a "Primitives" collection.
* See [Drawing Shapes](./drawing-shapes) for how fills and the color picker work.
---
---
url: 'https://openpencil.dev/programmable/sdk.md'
description: Build OpenPencil-powered editors with headless Vue composables and primitives.
---
# Vue SDK
`@open-pencil/vue` exists so OpenPencil can be more than a standalone design app.
The goal is to make OpenPencil a toolkit you can embed into other products, internal tools, and workflow-specific editors — not just a single default UI.
The OpenPencil app is one composition of that toolkit. The SDK is how you build a different one.
It gives you:
* injected editor context
* CanvasKit-backed canvas rendering
* selection, commands, menu, property-panel, and variables composables
* headless structural primitives like `PageListRoot`, `PropertyListRoot`, and `ToolbarRoot`
* built-in i18n primitives for menus, panels, dialogs, and custom locale pickers
## Start here
## Why the SDK exists
Different products and teams need different editing surfaces.
Sometimes you want a full design editor. Sometimes you want a focused canvas inside another app. Sometimes you want an internal workflow tool, a template editor, or an AI-assisted editing surface built around a narrow use case.
The SDK is the layer that makes those possible.
## Design principles
* **Headless first**: logic and structure, not app styling
* **Composable over wrapper**: use composables when there is no meaningful structural coordination
* **Intentional public API**: stable exports from `packages/vue/src/index.ts`
* **Framework-aware**: Vue integration over `@open-pencil/core`
## How to think about the package
The SDK has two main layers:
1. **Composables** for editor state and actions
2. **Primitives** for meaningful UI structure
If you only need editor state and actions, start with composables.
If you are building reusable editor UI building blocks, start with primitives.
## API sections
* [Components](/programmable/sdk/api/components/)
* [Composables](/programmable/sdk/api/composables/)
* [Advanced](/programmable/sdk/api/advanced/)
---
---
url: 'https://openpencil.dev/programmable/sdk/getting-started.md'
description: 'Set up @open-pencil/vue with createEditor, provideEditor, and a canvas.'
---
# SDK Getting Started
## Installation
```bash
bun add @open-pencil/core @open-pencil/vue canvaskit-wasm
```
The SDK lives in the monorepo today and is also published as `@open-pencil/vue`.
```ts
import { createEditor } from '@open-pencil/core/editor'
import { provideEditor, useCanvas } from '@open-pencil/vue'
```
## Mental model
There are three layers:
1. `@open-pencil/core` — framework-agnostic editor engine
2. `@open-pencil/vue` — Vue composables and headless primitives
3. your app — styling, routing, file flows, product-specific UI
## Minimal setup
### 1. Create an editor
```ts
import { createEditor } from '@open-pencil/core/editor'
const editor = createEditor({
width: 1200,
height: 800,
})
```
### 2. Provide it to Vue
```vue
```
You can think of this as the provider layer for the editor tree. The docs prefer `provideEditor()` directly because that is the current real API surface.
### 3. Attach a canvas
```vue
```
## Using composables
Once the editor is provided, child components can read selection and issue commands:
```ts
import { useEditorCommands, useSelectionState } from '@open-pencil/vue'
const selection = useSelectionState()
const commands = useEditorCommands()
```
## Basic example
```vue
Selected: {{ selectedCount }}
```
## Next steps
* [Architecture](./architecture)
* [API Reference](./api/)
* [useEditor](./api/composables/use-editor)
* [useCanvas](./api/composables/use-canvas)
* [useI18n](./api/composables/use-i18n)
---
---
url: 'https://openpencil.dev/programmable/sdk/architecture.md'
description: >-
Folder structure, public API boundaries, and composition patterns in
@open-pencil/vue.
---
# SDK Architecture
`@open-pencil/vue` is the Vue-facing layer over `@open-pencil/core`.
It does not own the editor model itself. It adapts the core editor into:
* Vue injection
* reactive composables
* headless structural primitives
* canvas and input wiring
## Folder structure
This package is organized by domain.
### Component families
* `Canvas/`
* `ColorPicker/`
* `FillPicker/`
* `FontPicker/`
* `GradientEditor/`
* `LayerTree/`
* `PageList/`
* `PropertyList/`
* `ScrubInput/`
* `Toolbar/`
These contain structural/headless primitives and local helpers.
### Controls
`controls/` contains property-panel and editor control composables:
* `usePosition`
* `useLayout`
* `useAppearance`
* `useTypography`
* `useExport`
* `useFillControls`
* `useStrokeControls`
* `useEffectsControls`
* `useNodeProps`
* `usePropScrub`
### Variables
`VariablesEditor/` contains variables-domain composables and state wiring.
### Selection
`selection/` contains selection-derived editor state and capabilities.
### Context
`context/` contains editor injection helpers:
* `EDITOR_KEY`
* `provideEditor`
* `useEditor`
### Internal
`internal/` contains cross-cutting utilities not intended as primary headless primitives.
## Public API philosophy
### Prefer composables
If the problem is mostly control logic, state derivation, or editor actions, expose a composable.
### Keep headless primitives for meaningful structure
Use component roots when they coordinate structure, children, slots, or context.
Examples:
* `PageListRoot`
* `PropertyListRoot`
* `ToolbarRoot`
### Avoid broad context-dump slots
Prefer focused slot props or direct composable usage over giant `v-slot="ctx"` payloads.
## App vs SDK responsibility
### SDK owns
* editor integration
* reusable headless logic
* reusable UI structure without styling assumptions
* canvas rendering integration
### App owns
* styling
* layout shells
* routing
* product file flows
* toasts, menus, and app-specific UX
## Practical rule of thumb
If a piece of logic could be reused in a different OpenPencil-based app without bringing app styling with it, it probably belongs in `@open-pencil/vue`.
## Related pages
* [SDK Getting Started](./getting-started)
* [API Reference](./api/)
---
---
url: 'https://openpencil.dev/programmable/sdk/guides/custom-editor-shell.md'
description: >-
Build your own editor shell with provideEditor, CanvasRoot, menus, panels, and
toolbars.
---
# Custom Editor Shell
A typical OpenPencil Vue app has three layers:
1. `@open-pencil/core` creates the editor
2. `@open-pencil/vue` adapts it into Vue composables and headless primitives
3. your app renders the shell, styling, and product UX
## Why this matters
The built-in OpenPencil app is only one possible shell.
You can build a very different one for a focused workflow: an embedded editor inside another product, an internal asset tool, a template editor, an annotation UI, or an AI-assisted editing surface with custom controls.
That is the main reason the SDK exists.
## Recommended composition
A practical shell often looks like this:
* provider at the top with `provideEditor()`
* canvas in the center
* page/layer navigation on one side
* properties on the other side
* menus and toolbars driven by composables
## Example
```vue
```
## Why this split works
* the SDK owns editor integration and reusable headless logic
* your app owns layout, styling, and product-specific actions
* composables can power menus and panels without extra wrapper components
## Related APIs
* [provideEditor](../api/composables/provide-editor)
* [useCanvas](../api/composables/use-canvas)
* [ToolbarRoot](../api/components/toolbar-root)
* [PageListRoot](../api/components/page-list-root)
* [LayerTreeRoot](../api/components/layer-tree-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/guides/property-panels.md'
description: Build property panels with control composables and headless list primitives.
---
# Property Panels
Property panels in `@open-pencil/vue` are intentionally composable-first.
If a panel mostly needs selection-derived values and update actions, prefer composables.
If a panel needs reusable array/list structure, use a headless primitive like `PropertyListRoot`.
## Common control composables
For standard property sections, start with:
* `usePosition()`
* `useLayout()`
* `useAppearance()`
* `useTypography()`
* `useExport()`
For list-style panels, use:
* `useFillControls()`
* `useStrokeControls()`
* `useEffectsControls()`
## Example: position panel
```vue
```
## Example: fills panel
```vue
{{ fill.type }}
Remove
Add fill
```
## Rule of thumb
* use composables for direct control logic
* use structural primitives when repeated list/tree/slot coordination is the hard part
## Related APIs
* [usePosition](../api/composables/use-position)
* [useLayout](../api/composables/use-layout)
* [useAppearance](../api/composables/use-appearance)
* [useTypography](../api/composables/use-typography)
* [useFillControls](../api/composables/use-fill-controls)
* [useStrokeControls](../api/composables/use-stroke-controls)
* [useEffectsControls](../api/composables/use-effects-controls)
* [PropertyListRoot](../api/components/property-list-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/guides/navigation-panels.md'
description: >-
Build page and layer sidebars with PageListRoot, LayerTreeRoot, and selection
state.
---
# Navigation Panels
OpenPencil sidebars usually combine two concerns:
* page navigation
* layer navigation
The Vue SDK provides headless primitives for both.
## Page navigation
Use `PageListRoot` or `usePageList()`.
```vue
{{ page.name }}
New page
```
## Layer navigation
Use `LayerTreeRoot` when you want SDK-managed tree structure but app-owned presentation.
```vue
```
## Practical pattern
A common layout is:
* pages at the top of the sidebar
* layers below
* details or inline rename controls embedded in your row components
## Related APIs
* [usePageList](../api/composables/use-page-list)
* [PageListRoot](../api/components/page-list-root)
* [LayerTreeRoot](../api/components/layer-tree-root)
* [useSelectionState](../api/composables/use-selection-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api.md'
description: >-
Reference documentation for @open-pencil/vue components, composables, and
advanced APIs.
---
# API Reference
The Vue SDK reference is organized into three sections.
## Suggested path
* Start with **Components** if you are building reusable editor UI primitives.
* Start with **Composables** if you are wiring editor state and actions.
* Use **Advanced** only when you need lower-level helpers or primitive contexts.
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components.md'
description: Component reference for headless Vue primitives in @open-pencil/vue.
---
# Components
`@open-pencil/vue` exposes headless structural primitives for canvas wiring, navigation UI, property panels, and focused input controls.
## Core editor primitives
## Property panel primitives
## Pickers and inputs
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/canvas-root.md'
description: Headless canvas primitive for OpenPencil rendering surfaces.
---
# CanvasRoot
`CanvasRoot` is the structural canvas primitive in `@open-pencil/vue`.
Use it when you want SDK-provided canvas structure and context with app-owned layout and styling.
## Related APIs
* [useCanvas](../composables/use-canvas)
* [useCanvasInput](../composables/use-canvas-input)
* [useTextEdit](../composables/use-text-edit)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/canvas-surface.md'
description: Canvas element primitive that binds to the nearest CanvasRoot context.
---
# CanvasSurface
`CanvasSurface` renders the actual `
` element used by the SDK canvas stack.
Use it inside `CanvasRoot` when you want SDK-managed canvas refs and rendering integration, but app-owned layout and styling.
## Related APIs
* [CanvasRoot](./canvas-root)
* [useCanvasContext](../advanced/use-canvas-context)
* [useCanvas](../composables/use-canvas)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/layer-tree-root.md'
description: Headless structural primitive for layer tree interfaces.
---
# LayerTreeRoot
`LayerTreeRoot` is the SDK primitive for rendering a layer tree with app-owned markup and styling.
Use it when you want reusable tree structure and interaction wiring without built-in presentation.
## Related APIs
* [useSelectionState](../composables/use-selection-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/layer-tree-item.md'
description: Headless row primitive for a single layer tree node.
---
# LayerTreeItem
`LayerTreeItem` renders one layer-tree row and exposes selection, expand, visibility, lock, and rename handlers through its default slot.
Use it when you want app-owned row markup with SDK-provided layer-tree behavior.
## Related APIs
* [LayerTreeRoot](./layer-tree-root)
* [useLayerTree](../advanced/use-layer-tree)
* [useLayerDrag](../advanced/use-layer-drag)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/toolbar-root.md'
description: Headless structural primitive for editor toolbar UIs.
---
# ToolbarRoot
`ToolbarRoot` is the headless toolbar primitive from `@open-pencil/vue`.
Use it when you want reusable toolbar structure and context with your own buttons, styling, and layout.
## Related APIs
* [useEditorCommands](../composables/use-editor-commands)
* [useSelectionCapabilities](../composables/use-selection-capabilities)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/toolbar-item.md'
description: Headless toolbar item primitive for a single editor tool.
---
# ToolbarItem
`ToolbarItem` exposes active-state and selection behavior for one toolbar tool.
Use it inside `ToolbarRoot` when you want custom button markup but shared tool-selection wiring.
## Related APIs
* [ToolbarRoot](./toolbar-root)
* [useToolbar](../advanced/use-toolbar)
* [useToolbarState](../advanced/use-toolbar-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/page-list-root.md'
description: Headless structural primitive for page list UIs.
---
# PageListRoot
`PageListRoot` is a headless structural primitive for page list interfaces.
It provides slot props for:
* pages
* current page id
* divider detection
* page actions like add, switch, rename, and delete
## Usage
Use it when you want SDK-provided page-list structure with app-specific rendering and styling.
## Basic example
```vue
```
## Related APIs
* [usePageList](../composables/use-page-list)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/property-list-root.md'
description: 'Headless structural primitive for fills, strokes, and effects list UIs.'
---
# PropertyListRoot
`PropertyListRoot` is a headless structural primitive for array-based property editors.
It is intended for property UIs like:
* fills
* strokes
* effects
It provides slot props for:
* current items
* mixed-state detection
* add/remove/update/patch operations
* visibility toggling per item
## Usage
```vue
Remove
Add fill
```
## Related APIs
* [SDK API Overview](../)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/property-list-item.md'
description: 'Headless item primitive for a single fills, strokes, or effects row.'
---
# PropertyListItem
`PropertyListItem` exposes update, patch, remove, and visibility handlers for one array item inside `PropertyListRoot`.
Use it when building custom list-row UIs for fills, strokes, or effects.
## Related APIs
* [PropertyListRoot](./property-list-root)
* [usePropertyList](../advanced/use-property-list)
* [useNodeProps](../advanced/use-node-props)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/color-picker-root.md'
description: Headless popover-based color picker primitive.
---
# ColorPickerRoot
`ColorPickerRoot` is a headless popover-based color picker primitive.
It provides:
* a trigger slot with swatch background styling
* a default trigger fallback
* a content slot with `color` and `update()`
## Props
## Events
## Slots
## Example
```vue
```
## Related APIs
* [ColorInputRoot](./color-input-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/color-input-root.md'
description: Headless color input helper with hex parsing and update helpers.
---
# ColorInputRoot
`ColorInputRoot` is a headless helper for color input UIs.
It derives a hex value from a color and exposes update helpers for hex and full-color changes.
## Props
## Events
## Slots
## Example
```vue
```
## Related APIs
* [ColorPickerRoot](./color-picker-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/fill-picker-root.md'
description: Headless popover-based fill picker primitive.
---
# FillPickerRoot
`FillPickerRoot` is a headless popover-based fill picker for solid, gradient, and image fills.
## Props
## Events
## Slots
### Trigger slot props
```ts
{
style: Record
}
```
### Default slot props
```ts
{
fill: Fill
category: 'SOLID' | 'GRADIENT' | 'IMAGE'
toSolid: () => void
toGradient: () => void
toImage: () => void
update: (fill: Fill) => void
}
```
## Example
```vue
{{ category }}
Solid
Gradient
```
## Related APIs
* [GradientEditorRoot](./gradient-editor-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/font-picker-root.md'
description: Headless searchable font picker built on Reka Combobox.
---
# FontPickerRoot
`FontPickerRoot` is a headless searchable font picker built on Reka UI Combobox primitives.
## Props
## Model
## Events
## Slots
## Example
```vue
{{ value }}
```
## Related APIs
* [useTypography](../composables/use-typography)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/gradient-editor-root.md'
description: Headless root primitive for gradient stop editing.
---
# GradientEditorRoot
`GradientEditorRoot` is a headless root primitive for gradient editing.
It owns:
* active stop state
* subtype switching
* stop add/remove/update logic
* active color editing
* derived bar background
## Props
## Events
## Slots
### Default slot props
```ts
{
stops: GradientStop[]
subtype: GradientSubtype
subtypes: Array<{ value: GradientSubtype; label: string }>
activeStopIndex: number
activeColor: Color
barBackground: string
setSubtype: (type: GradientSubtype) => void
selectStop: (index: number) => void
addStop: () => void
removeStop: (index: number) => void
updateStopPosition: (index: number, position: number) => void
updateStopColor: (index: number, hex: string) => void
updateStopOpacity: (index: number, opacity: number) => void
updateActiveColor: (color: Color) => void
dragStop: (index: number, position: number) => void
}
```
## Example
```vue
```
## Related APIs
* [GradientEditorBar](./gradient-editor-bar)
* [GradientEditorStop](./gradient-editor-stop)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/gradient-editor-bar.md'
description: Headless draggable bar primitive for gradient stops.
---
# GradientEditorBar
`GradientEditorBar` is the draggable bar primitive used inside gradient editors.
## Props
## Events
## Slots
### Default slot props
```ts
{
stops: GradientStop[]
activeStopIndex: number
barBackground: string
barRef: (el: unknown) => void
onStopPointerDown: (index: number, event: PointerEvent) => void
onPointerMove: (event: PointerEvent) => void
onPointerUp: () => void
draggingIndex: number | null
}
```
## Example
```vue
```
## Related APIs
* [GradientEditorRoot](./gradient-editor-root)
* [GradientEditorStop](./gradient-editor-stop)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/gradient-editor-stop.md'
description: Headless slot primitive for a single gradient stop row.
---
# GradientEditorStop
`GradientEditorStop` is a headless primitive for rendering and editing a single gradient stop.
## Props
## Events
## Slots
### Default slot props
```ts
{
stop: GradientStop
index: number
active: boolean
positionPercent: number
opacityPercent: number
hex: string
css: string
select: () => void
updatePosition: (position: number) => void
updateColor: (hex: string) => void
updateOpacity: (opacity: number) => void
remove: () => void
}
```
## Example
```vue
```
## Related APIs
* [GradientEditorRoot](./gradient-editor-root)
* [GradientEditorBar](./gradient-editor-bar)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/scrub-input-root.md'
description: Headless root primitive for drag-to-scrub numeric input.
---
# ScrubInputRoot
`ScrubInputRoot` is the headless root primitive for drag-to-scrub numeric input.
It manages:
* mixed-value display
* editing vs scrubbing state
* pointer-driven numeric scrubbing
* commit semantics for finished edits
## Props
## Model
## Events
## Slots
## Example
```vue
```
## Related APIs
* [ScrubInputField](./scrub-input-field)
* [ScrubInputDisplay](./scrub-input-display)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/scrub-input-field.md'
description: Input element primitive for ScrubInputRoot editing mode.
---
# ScrubInputField
`ScrubInputField` renders the editable input element for `ScrubInputRoot`.
It only renders while the scrub input is in editing mode.
## Usage
Use it inside a `ScrubInputRoot` subtree.
## Props and attrs
## Example
```vue
```
## Related APIs
* [ScrubInputRoot](./scrub-input-root)
* [ScrubInputDisplay](./scrub-input-display)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/scrub-input-display.md'
description: Read-only display primitive for ScrubInputRoot non-editing mode.
---
# ScrubInputDisplay
`ScrubInputDisplay` renders the non-editing display for `ScrubInputRoot`.
It only renders while the scrub input is not in editing mode.
## Usage
Use it inside a `ScrubInputRoot` subtree.
## Props and attrs
## Example
```vue
```
## Related APIs
* [ScrubInputRoot](./scrub-input-root)
* [ScrubInputField](./scrub-input-field)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/components/layout-controls-root.md'
description: Headless root primitive for auto-layout and sizing controls.
---
# LayoutControlsRoot
`LayoutControlsRoot` exposes the slot contract returned by `useLayout()` as a structural primitive.
Use it when you want a reusable layout-controls shell with app-owned markup.
## Related APIs
* [useLayout](../composables/use-layout)
* [Property Panels guide](../../guides/property-panels)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/components/appearance-controls-root.md
description: 'Headless root primitive for opacity, visibility, and corner-radius controls.'
---
# AppearanceControlsRoot
`AppearanceControlsRoot` exposes the slot contract returned by `useAppearance()` as a structural primitive.
Use it when you want reusable appearance controls with custom presentation.
## Related APIs
* [useAppearance](../composables/use-appearance)
* [Property Panels guide](../../guides/property-panels)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/components/position-controls-root.md
description: 'Headless root primitive for position, size, alignment, and transform controls.'
---
# PositionControlsRoot
`PositionControlsRoot` exposes position, size, rotation, align, flip, and rotate handlers for the current selection.
Use it when you want custom position controls without reimplementing editor wiring.
## Related APIs
* [usePosition](../composables/use-position)
* [Property Panels guide](../../guides/property-panels)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/components/typography-controls-root.md
description: 'Headless root primitive for font, alignment, and formatting controls.'
---
# TypographyControlsRoot
`TypographyControlsRoot` exposes typography state and handlers from `useTypography()` as a structural primitive.
Use it when you want custom typography controls with SDK-managed font and formatting behavior.
## Related APIs
* [useTypography](../composables/use-typography)
* [FontPickerRoot](./font-picker-root)
* [Property Panels guide](../../guides/property-panels)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables.md'
description: Core composable APIs in @open-pencil/vue.
---
# Composables
These are the main composables most `@open-pencil/vue` consumers will use.
## Context and canvas
* [provideEditor](./provide-editor)
* [useEditor](./use-editor)
* [useCanvas](./use-canvas)
* [useCanvasInput](./use-canvas-input)
* [useTextEdit](./use-text-edit)
## Selection and commands
* [useSelectionState](./use-selection-state)
* [useSelectionCapabilities](./use-selection-capabilities)
* [useEditorCommands](./use-editor-commands)
* [useMenuModel](./use-menu-model)
## Property panels
* [usePosition](./use-position)
* [useLayout](./use-layout)
* [useAppearance](./use-appearance)
* [useTypography](./use-typography)
* [useExport](./use-export)
* [useFillControls](./use-fill-controls)
* [useStrokeControls](./use-stroke-controls)
* [useEffectsControls](./use-effects-controls)
## Variables, navigation, and localization
* [useVariablesEditor](./use-variables-editor)
* [usePageList](./use-page-list)
* [useI18n](./use-i18n)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/provide-editor.md'
description: Provide an OpenPencil editor instance to a Vue subtree using injection.
---
# provideEditor
`provideEditor(editor)` makes an OpenPencil editor available to descendant composables and headless primitives through Vue injection.
This is the foundation for `useEditor()`.
## Usage
```ts
import { provideEditor } from '@open-pencil/vue'
provideEditor(editor)
```
## Basic example
```vue
```
## Notes
The current SDK uses `provideEditor()` and `useEditor()` directly. Some older examples and error messages still refer to an `OpenPencilProvider` component, but the injection model is the real API surface to prefer in docs and app code.
## Related APIs
* [useEditor](./use-editor)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-editor.md'
description: Access the current injected OpenPencil editor instance.
---
# useEditor
`useEditor()` returns the current injected OpenPencil editor.
It is the main entry point for SDK composables and headless primitives that need editor access.
## Usage
`useEditor()` must be called inside a subtree where `provideEditor(editor)` has already been called.
```ts
import { useEditor } from '@open-pencil/vue'
const editor = useEditor()
```
## Basic example
```vue
Current page: {{ pageId }}
```
## Practical examples
### Read selected nodes
```ts
const editor = useEditor()
const selected = editor.getSelectedNodes()
```
### Trigger commands
```ts
const editor = useEditor()
editor.zoomToFit()
editor.undoAction()
```
## Error behavior
If called outside an editor provider tree, `useEditor()` throws with a helpful message.
That is intentional — this API should fail loudly when the editor context is missing.
## Related APIs
* [provideEditor](./provide-editor)
* [useCanvas](./use-canvas)
* [useSelectionState](./use-selection-state)
* [useEditorCommands](./use-editor-commands)
## Type
```ts
function useEditor(): Editor
```
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-canvas.md'
description: >-
Attach CanvasKit-backed rendering to a canvas element for an OpenPencil
editor.
---
# useCanvas
`useCanvas()` connects an editor to a real `` element.
It handles:
* CanvasKit initialization
* surface creation
* render scheduling
* resize handling
* optional ruler visibility
* renderer readiness callback
## Usage
```ts
import { ref } from 'vue'
import { useCanvas, useEditor } from '@open-pencil/vue'
const canvasRef = ref(null)
const editor = useEditor()
useCanvas(canvasRef, editor)
```
## Basic example
```vue
```
## Practical examples
### Disable rulers for an embedded preview
```ts
useCanvas(canvasRef, editor, {
showRulers: false,
})
```
### Keep drawing buffer for screenshots
```ts
useCanvas(canvasRef, editor, {
preserveDrawingBuffer: true,
})
```
## Notes
* `useCanvas()` is renderer-facing and browser-only in practice
* it is responsible for the live canvas pipeline, not app-level file flows
* it should usually be paired with `useCanvasInput()` for interaction handling
## Related APIs
* [useEditor](./use-editor)
* [useCanvasInput](./use-canvas-input)
* [useTextEdit](./use-text-edit)
## Type
```ts
interface UseCanvasOptions {
showRulers?: boolean
preserveDrawingBuffer?: boolean
onReady?: () => void
}
function useCanvas(
canvasRef: Ref,
editor: Editor,
options?: UseCanvasOptions,
): void
```
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-canvas-input.md'
description: >-
Wire canvas pointer input, dragging, selection, resize, rotation, and tool
behavior.
---
# useCanvasInput
`useCanvasInput()` connects pointer and mouse interaction to the editor canvas.
It handles interaction concerns like:
* selection
* dragging
* resize
* rotation
* panning
* pen/draw flows
* text editing interaction
* scope-aware hit testing
## Usage
This composable is typically paired with `useCanvas()` and hit-test helpers from the renderer.
```ts
useCanvasInput(
canvasRef,
editor,
hitTestSectionTitle,
hitTestComponentLabel,
hitTestFrameTitle,
)
```
## Basic example
```ts
const canvas = useCanvas(canvasRef, editor)
useCanvasInput(
canvasRef,
editor,
canvas.hitTestSectionTitle,
canvas.hitTestComponentLabel,
canvas.hitTestFrameTitle,
)
```
## Practical examples
### Track cursor movement in canvas space
```ts
useCanvasInput(
canvasRef,
editor,
hitTestSectionTitle,
hitTestComponentLabel,
hitTestFrameTitle,
(cx, cy) => {
console.log(cx, cy)
},
)
```
## Notes
This composable is lower-level than most panel logic. It is best suited for editor shells and canvas containers.
## Related APIs
* [useCanvas](./use-canvas)
* [useEditor](./use-editor)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-text-edit.md'
description: >-
Manage DOM text editing, composition, formatting, and syncing for canvas text
nodes.
---
# useTextEdit
`useTextEdit()` bridges DOM text input and the editor’s canvas text editing model.
It coordinates:
* textarea-backed text input
* IME composition
* caret blinking
* delete/backspace behavior
* formatting commands like bold/italic/underline
* syncing text changes back into the graph
## Usage
```ts
useTextEdit(canvasRef, editor)
```
## Basic example
Use this in the canvas owner component together with `useCanvas()` and `useCanvasInput()`.
## Practical examples
### Support formatting shortcuts
`useTextEdit()` already handles keyboard formatting actions like bold, italic, and underline while text editing is active.
### Keep canvas and text editor in sync
It updates graph text and style runs as the user types or edits formatted ranges.
## Notes
This is a canvas/editor integration composable, not a generic text-field composable.
## Related APIs
* [useCanvas](./use-canvas)
* [useCanvasInput](./use-canvas-input)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-selection-state.md'
description: >-
Reactive selection-derived editor state for current node, count, and selection
type.
---
# useSelectionState
`useSelectionState()` exposes reactive selection-derived state from the current editor.
Use it when you need to render UI based on:
* whether anything is selected
* how many nodes are selected
* the primary selected node
* whether the current selection is an instance, component, or group
## Usage
```ts
import { useSelectionState } from '@open-pencil/vue'
const selection = useSelectionState()
```
## Basic example
```vue
No selection
{{ selectedCount }} selected
· instance
```
## What it returns
Useful values include:
* `selectedIds`
* `hasSelection`
* `selectedNode`
* `selectedCount`
* `selectedNodeType`
* `isInstance`
* `isComponent`
* `isGroup`
* `canCreateComponentSet`
## Practical examples
### Show instance-only actions
```ts
const { isInstance } = useSelectionState()
```
### Enable component-set creation UI
```ts
const { canCreateComponentSet } = useSelectionState()
```
## Related APIs
* [useSelectionCapabilities](./use-selection-capabilities)
* [useEditorCommands](./use-editor-commands)
* [useEditor](./use-editor)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/composables/use-selection-capabilities.md
description: Derive command-friendly booleans for selection-driven UI and actions.
---
# useSelectionCapabilities
`useSelectionCapabilities()` exposes reactive booleans for whether common editor actions are currently allowed.
Use it when building:
* menus
* toolbars
* keyboard shortcuts
* action buttons
* contextual panels
## Usage
```ts
import { useSelectionCapabilities } from '@open-pencil/vue'
const caps = useSelectionCapabilities()
```
## Basic example
```vue
Duplicate
Delete
Make component
```
## Practical examples
### Gate menu entries
```ts
const { canMoveToPage, canGoToMainComponent } = useSelectionCapabilities()
```
### Enable zoom commands only when useful
```ts
const { canZoomToSelection } = useSelectionCapabilities()
```
## Related APIs
* [useSelectionState](./use-selection-state)
* [useEditorCommands](./use-editor-commands)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-editor-commands.md'
description: 'Build menus, actions, and command-driven UI on top of the editor.'
---
# useEditorCommands
`useEditorCommands()` exposes a command-oriented layer over editor actions.
It is useful when building:
* app menus
* context menus
* toolbars
* keyboard-command adapters
* page-move submenus
## Usage
```ts
import { useEditorCommands } from '@open-pencil/vue'
const { commands, menuItem, runCommand, moveSelectionToPage, otherPages } = useEditorCommands()
```
## Basic example
```ts
const { menuItem } = useEditorCommands()
const editMenu = [
menuItem('edit.undo', '⌘Z'),
menuItem('edit.redo', '⇧⌘Z'),
{ separator: true },
menuItem('selection.delete'),
]
```
## Practical examples
### Run a command directly
```ts
const { runCommand } = useEditorCommands()
runCommand('selection.duplicate')
```
### Build a “move to page” submenu
```ts
const { otherPages, moveSelectionToPage } = useEditorCommands()
const items = otherPages.value.map(page => ({
label: page.name,
action: () => moveSelectionToPage(page.id),
}))
```
## Related APIs
* [useMenuModel](./use-menu-model)
* [useSelectionState](./use-selection-state)
* [useEditor](./use-editor)
## Main types
```ts
type EditorCommandId =
| 'edit.undo'
| 'edit.redo'
| 'selection.selectAll'
| 'selection.duplicate'
| 'selection.delete'
| 'selection.group'
| 'selection.ungroup'
| 'selection.createComponent'
| 'selection.createComponentSet'
| 'selection.createInstance'
| 'selection.detachInstance'
| 'selection.goToMainComponent'
| 'selection.wrapInAutoLayout'
| 'selection.bringToFront'
| 'selection.sendToBack'
| 'selection.toggleVisibility'
| 'selection.toggleLock'
| 'selection.moveToPage'
| 'view.zoom100'
| 'view.zoomFit'
| 'view.zoomSelection'
```
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-menu-model.md'
description: Build app and canvas menu models from the current editor state.
---
# useMenuModel
`useMenuModel()` builds higher-level menu structures on top of editor commands and selection state.
It is useful when you want ready-to-render menu groups instead of composing commands manually.
## Usage
```ts
import { useMenuModel } from '@open-pencil/vue'
const { appMenu, canvasMenu, selectionLabelMenu } = useMenuModel()
```
## Basic example
```ts
const { canvasMenu } = useMenuModel()
```
Render `canvasMenu.value` into your context menu component.
## Practical examples
### App-style top menu
`appMenu` groups entries into:
* Edit
* View
* Object
* Arrange
### Context menu with page moves
`canvasMenu` includes dynamic items like “Move to page” based on current selection and available pages.
### Selection labels
`selectionLabelMenu` exposes context-sensitive labels like:
* `Hide` / `Show`
* `Lock` / `Unlock`
## Related APIs
* [useEditorCommands](./use-editor-commands)
* [useSelectionState](./use-selection-state)
* [useSelectionCapabilities](./use-selection-capabilities)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-position.md'
description: >-
Read and update selected node position, size, rotation, alignment, and
flipping.
---
# usePosition
`usePosition()` is a control composable for position-related UI.
It exposes selected-node values like:
* `x`
* `y`
* `width`
* `height`
* `rotation`
and actions like:
* align
* flip
* rotate
* scrub/update numeric properties
## Usage
```ts
import { usePosition } from '@open-pencil/vue'
const position = usePosition()
```
## Basic example
```ts
const { x, y, width, height, rotation, updateProp, commitProp } = usePosition()
```
## Practical examples
### Align selected nodes
```ts
position.align('horizontal', 'center')
position.align('vertical', 'min')
```
### Flip selection
```ts
position.flip('horizontal')
position.flip('vertical')
```
### Rotate selection
```ts
position.rotate(90)
```
## Related APIs
* [useLayout](./use-layout)
* [useAppearance](./use-appearance)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-layout.md'
description: 'Work with auto-layout, sizing, padding, alignment, and grid tracks.'
---
# useLayout
`useLayout()` is the main control composable for layout-related panels.
It exposes state and actions for:
* flex vs grid mode
* width/height sizing
* padding
* alignment
* grid template track editing
## Usage
```ts
import { useLayout } from '@open-pencil/vue'
const layout = useLayout()
```
## Basic example
```ts
const {
isGrid,
isFlex,
widthSizing,
heightSizing,
setWidthSizing,
setHeightSizing,
} = useLayout()
```
## Practical examples
### Toggle between uniform and individual padding UI
```ts
layout.toggleIndividualPadding()
```
### Update grid tracks
```ts
layout.updateGridTrack('gridTemplateColumns', 0, { sizing: 'FIXED', value: 240 })
layout.addTrack('gridTemplateRows')
```
### Change alignment
```ts
layout.setAlignment('CENTER', 'MAX')
```
## Related APIs
* [usePosition](./use-position)
* [useEditor](./use-editor)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-appearance.md'
description: >-
Control visibility, opacity, and corner radius state for the current
selection.
---
# useAppearance
`useAppearance()` is the appearance-focused control composable for property panels.
It exposes selection-derived UI state for:
* visibility
* opacity
* corner radius
* independent corner radii
## Usage
```ts
import { useAppearance } from '@open-pencil/vue'
const appearance = useAppearance()
```
## Basic example
```ts
const {
visibilityState,
opacityPercent,
cornerRadiusValue,
toggleVisibility,
toggleIndependentCorners,
} = useAppearance()
```
## Practical examples
### Toggle selection visibility
```ts
appearance.toggleVisibility()
```
### Edit per-corner radii
```ts
appearance.updateCornerProp('topLeftRadius', 12)
appearance.commitCornerProp('topLeftRadius', 12, 8)
```
## Related APIs
* [SDK API Overview](../)
* [useLayout](./use-layout)
* [useTypography](./use-typography)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-typography.md'
description: >-
Read and update font family, weight, size, alignment, and formatting for text
nodes.
---
# useTypography
`useTypography()` is the text-property control composable for text editing panels.
It exposes:
* font family
* font weight
* font size
* formatting state
* missing-font status
* helpers for changing family, weight, alignment, and decorations
## Usage
```ts
import { useTypography } from '@open-pencil/vue'
const typography = useTypography()
```
## Basic example
```ts
const {
fontFamily,
fontWeight,
fontSize,
activeFormatting,
setFamily,
setWeight,
setAlign,
} = useTypography()
```
## Practical examples
### Load and switch a font family
```ts
const typography = useTypography({
loadFont: async (family, style) => {
await myFontLoader(family, style)
},
})
```
### Toggle formatting
```ts
typography.toggleBold()
typography.toggleItalic()
typography.toggleDecoration('UNDERLINE')
```
## Related APIs
* [useTextEdit](./use-text-edit)
* [useSelectionState](./use-selection-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-export.md'
description: Manage export settings like scale and format for the current selection.
---
# useExport
`useExport()` is the export-panel composable for selected nodes.
It manages:
* export settings rows
* selected node ids
* export name labeling
* supported scales and formats
## Usage
```ts
import { useExport } from '@open-pencil/vue'
const exportState = useExport()
```
## Basic example
```ts
const {
settings,
nodeName,
scales,
formats,
addSetting,
updateScale,
updateFormat,
} = useExport()
```
## Practical examples
### Add another export preset
```ts
exportState.addSetting()
```
### Change the first export to 2x WEBP
```ts
exportState.updateScale(0, 2)
exportState.updateFormat(0, 'WEBP')
```
## Related APIs
* [useSelectionState](./use-selection-state)
* [useEditor](./use-editor)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-fill-controls.md'
description: Fill-panel composable with default fill behavior.
---
# useFillControls
`useFillControls()` is the fill-property composable used by fill editing UIs.
It adds a reusable default fill value.
## Usage
```ts
import { useFillControls } from '@open-pencil/vue'
const fills = useFillControls()
```
## What it gives you
It exposes:
* `defaultFill`
## Practical examples
### Add a new fill row
```ts
propertyList.add(fills.defaultFill)
```
## Related APIs
* [PropertyListRoot](../components/property-list-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-stroke-controls.md'
description: >-
Stroke-panel helpers for alignment, side selection, and per-side stroke
weights.
---
# useStrokeControls
`useStrokeControls()` is the stroke-property composable used by stroke editing panels.
It provides:
* stroke align options
* side presets like all, top, bottom, left, right, custom
* default stroke data
* helpers for per-side border weights
## Usage
```ts
import { useStrokeControls } from '@open-pencil/vue'
const strokes = useStrokeControls()
```
## Basic example
```ts
const { alignOptions, sideOptions, currentAlign, currentSides, selectSide } = useStrokeControls()
```
## Practical examples
### Set stroke alignment
```ts
strokes.updateAlign('INSIDE', activeNode)
```
### Limit a stroke to one side
```ts
strokes.selectSide('TOP', activeNode)
```
## Related APIs
* [PropertyListRoot](../components/property-list-root)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/composables/use-effects-controls.md
description: >-
Effects-panel helpers for shadows, blurs, expansion state, and scrub/commit
flows.
---
# useEffectsControls
`useEffectsControls()` is the effects-property composable used by effects panels.
It provides helpers for:
* default effects
* shadow vs blur logic
* expanded item state
* scrub-preview editing
* commit-on-finish updates
* effect type and color changes
## Usage
```ts
import { useEffectsControls } from '@open-pencil/vue'
const effects = useEffectsControls()
```
## Basic example
```ts
const { effectOptions, createDefaultEffect, toggleExpand, scrubEffect, commitEffect } = useEffectsControls()
```
## Practical examples
### Add a default effect
```ts
const effect = effects.createDefaultEffect()
```
### Preview scrub changes, then commit
```ts
effects.scrubEffect(node, index, { radius: 12 })
effects.commitEffect(node, index, { radius: 12 })
```
## Related APIs
* [PropertyListRoot](../components/property-list-root)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/composables/use-variables-editor.md
description: 'Compose the variables dialog state, table columns, and TanStack table wiring.'
---
# useVariablesEditor
`useVariablesEditor()` is a higher-level variables-domain composable for building a variables dialog or editor screen.
It combines:
* variables dialog state
* variables table columns
* TanStack Vue Table wiring
* collection/mode helpers
## Usage
```ts
const variables = useVariablesEditor({
colorInput: ColorInput,
icons,
fallbackIcon,
deleteIcon,
})
```
## What it returns
It includes the lower-level dialog/table state plus:
* `columns`
* `table`
* `hasCollections`
## Practical examples
### Build a variables dialog
Use `useVariablesEditor()` when you want one composable that already wires the table and action handlers together.
## Related APIs
* [SDK API Overview](../)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-page-list.md'
description: 'Read pages and drive page switching, creation, deletion, and renaming.'
---
# usePageList
`usePageList()` is the page-management composable behind page list UIs.
It exposes:
* `pages`
* `currentPageId`
* `switchPage`
* `addPage`
* `deletePage`
* `renamePage`
## Usage
```ts
import { usePageList } from '@open-pencil/vue'
const pageList = usePageList()
```
## Basic example
```ts
const { pages, currentPageId, switchPage, addPage } = usePageList()
```
## Practical examples
### Switch pages
```ts
switchPage(pageId)
```
### Create a new page
```ts
addPage()
```
## Related APIs
* [PageListRoot](../components/page-list-root)
* [useMenuModel](./use-menu-model)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced.md'
description: Lower-level and specialized APIs in @open-pencil/vue.
---
# Advanced
These APIs are public, but they are more specialized than the main component and composable surface.
## Selection and scene helpers
* [useNodeProps](./use-node-props)
* [useSceneComputed](./use-scene-computed)
* [usePropScrub](./use-prop-scrub)
## Picker, variables, locale, and editor internals
* [useColorVariableBinding](./use-color-variable-binding)
* [useFillPicker](./use-fill-picker)
* [useGradientStops](./use-gradient-stops)
* [useFontPicker](./use-font-picker)
* [useOkHCL](./use-okhcl)
* [useVariables](./use-variables)
* [useVariablesDialogState](./use-variables-dialog-state)
* [useVariablesTable](./use-variables-table)
* [Locale APIs](./locale-apis)
* [useToolbarState](./use-toolbar-state)
* [useNodeFontStatus](./use-node-font-status)
## Editor-shell utilities
* [useLayerDrag](./use-layer-drag)
* [useInlineRename](./use-inline-rename)
* [useCanvasDrop](./use-canvas-drop)
* [extractImageFilesFromClipboard](./extract-image-files-from-clipboard)
* [useViewportKind](./use-viewport-kind)
* [toolCursor](./tool-cursor)
## Primitive context helpers
* [useCanvasContext](./use-canvas-context)
* [useLayerTree](./use-layer-tree)
* [useToolbar](./use-toolbar)
* [usePropertyList](./use-property-list)
* [useScrubInput](./use-scrub-input)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-node-props.md'
description: Low-level selection and mixed-value helper for property panels.
---
# useNodeProps
`useNodeProps()` is the low-level property-panel helper behind higher-level composables like `useAppearance`, `useLayout`, and `useTypography`.
Use it when you need mixed-value detection, multi-selection updates, array-item editing, or undo-aware property commits.
## Related APIs
* [useAppearance](../composables/use-appearance)
* [useLayout](../composables/use-layout)
* [useTypography](../composables/use-typography)
* [PropertyListRoot](../components/property-list-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-scene-computed.md'
description: Convenience wrapper for scene-derived computed state.
---
# useSceneComputed
`useSceneComputed(fn)` is a thin computed wrapper used to make scene-backed derived state explicit in higher-level composables.
Use it when you want intent-revealing computed state that clearly depends on editor scene data.
## Related APIs
* [useSelectionState](../composables/use-selection-state)
* [useSelectionCapabilities](../composables/use-selection-capabilities)
* [useNodeProps](./use-node-props)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/advanced/use-color-variable-binding.md
description: Variable-binding helper for fill and stroke color editors.
---
# useColorVariableBinding
`useColorVariableBinding(kind)` exposes search, binding, and unbinding helpers for color variables used by fill and stroke editors.
Use it when building color UIs that need to connect fills or strokes to design variables.
## Usage
```ts
import { useColorVariableBinding } from '@open-pencil/vue'
const fillBinding = useColorVariableBinding('fills')
const strokeBinding = useColorVariableBinding('strokes')
```
## Related APIs
* [useFillControls](../composables/use-fill-controls)
* [useStrokeControls](../composables/use-stroke-controls)
* [FillPickerRoot](../components/fill-picker-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-fill-picker.md'
description: Low-level fill-category and conversion helper.
---
# useFillPicker
`useFillPicker(fill, onUpdate)` returns derived fill category state plus conversion helpers for switching between solid, gradient, and image fills.
Use it when building custom fill pickers beyond `FillPickerRoot`.
## Related APIs
* [FillPickerRoot](../components/fill-picker-root)
* [useFillControls](../composables/use-fill-controls)
* [useGradientStops](./use-gradient-stops)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-gradient-stops.md'
description: Gradient-stop state and mutation helper for fill editors.
---
# useGradientStops
`useGradientStops(fill, onUpdate)` manages active-stop state, subtype switching, stop dragging, and color or opacity updates for gradient fills.
Use it when building custom gradient editors beyond the packaged primitives.
## Related APIs
* [GradientEditorRoot](../components/gradient-editor-root)
* [GradientEditorBar](../components/gradient-editor-bar)
* [GradientEditorStop](../components/gradient-editor-stop)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-font-picker.md'
description: Searchable font-picker state and selection helper.
---
# useFontPicker
`useFontPicker(options)` manages available font families, search state, open state, and family selection for font-picker UIs.
Use it when building custom font-picking UIs beyond `FontPickerRoot`.
## Related APIs
* [FontPickerRoot](../components/font-picker-root)
* [useTypography](../composables/use-typography)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-prop-scrub.md'
description: Low-level helper for drag-to-scrub property updates with commit support.
---
# usePropScrub
`usePropScrub(editor)` coordinates live property updates during scrubbing and commits undo-aware changes when the interaction finishes.
Use it when building numeric controls that scrub selected node properties directly.
## Related APIs
* [ScrubInputRoot](../components/scrub-input-root)
* [useNodeProps](./use-node-props)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-layer-drag.md'
description: Drag-and-drop wiring helper for layer tree reordering.
---
# useLayerDrag
`useLayerDrag(editor, indentPerLevel?)` wires pragmatic-drag-and-drop behavior for layer tree rows and maps drop instructions to editor reorder operations.
Use it when extending or replacing the default layer-tree drag UI.
## Related APIs
* [LayerTreeRoot](../components/layer-tree-root)
* [LayerTreeItem](../components/layer-tree-item)
* [useLayerTree](./use-layer-tree)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-inline-rename.md'
description: Inline rename state and focus-management helper.
---
# useInlineRename
`useInlineRename(onCommit)` manages editing state, focus, outside-click handling, and keyboard behavior for inline rename flows.
Use it in custom page lists, layer trees, or similar rename-in-place UIs.
## Related APIs
* [PageListRoot](../components/page-list-root)
* [LayerTreeItem](../components/layer-tree-item)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-toolbar-state.md'
description: Presentation-oriented toolbar state helper for mobile category paging.
---
# useToolbarState
`useToolbarState()` returns mobile-category paging state and helpers like `goPrev()` and `goNext()` for responsive toolbar shells.
Use it when building toolbar layouts on top of `ToolbarRoot`.
## Related APIs
* [ToolbarRoot](../components/toolbar-root)
* [ToolbarItem](../components/toolbar-item)
* [useToolbar](./use-toolbar)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-node-font-status.md'
description: Missing-font status helper for text nodes.
---
# useNodeFontStatus
`useNodeFontStatus(node)` returns missing-font information for a text-node getter.
Use it in typography panels and warnings that need to surface unavailable font families.
## Related APIs
* [useTypography](../composables/use-typography)
* [TypographyControlsRoot](../components/typography-controls-root)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-canvas-drop.md'
description: Image file drag-and-drop helper for canvas surfaces.
---
# useCanvasDrop
`useCanvasDrop(canvasRef, editor)` wires dragenter, dragover, dragleave, and drop handling so image files dropped on the canvas are placed into the scene.
Use it when building custom canvas surfaces that accept image drops.
## Related APIs
* [CanvasRoot](../components/canvas-root)
* [CanvasSurface](../components/canvas-surface)
* [extractImageFilesFromClipboard](./extract-image-files-from-clipboard)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/advanced/extract-image-files-from-clipboard.md
description: Clipboard utility for extracting accepted image files.
---
# extractImageFilesFromClipboard
`extractImageFilesFromClipboard(event)` filters clipboard files down to accepted image types used by the canvas drop flow.
Use it when implementing paste-from-clipboard image workflows.
## Related APIs
* [useCanvasDrop](./use-canvas-drop)
* [useCanvas](../composables/use-canvas)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/tool-cursor.md'
description: Helper that resolves the cursor string for an editor tool.
---
# toolCursor
`toolCursor(tool, override?)` maps an editor tool to the cursor the SDK should use, while still allowing an explicit override.
Use it when building custom canvas shells or tool UIs that need consistent cursor behavior.
## Related APIs
* [useCanvas](../composables/use-canvas)
* [useEditorCommands](../composables/use-editor-commands)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-canvas-context.md'
description: Primitive context helper for CanvasRoot descendants.
---
# useCanvasContext
`useCanvasContext()` reads the local canvas context provided by `CanvasRoot`.
Use it inside descendants like `CanvasSurface` or your own custom canvas children.
## Related APIs
* [CanvasRoot](../components/canvas-root)
* [CanvasSurface](../components/canvas-surface)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-layer-tree.md'
description: Primitive context helper for LayerTreeRoot descendants.
---
# useLayerTree
`useLayerTree()` reads the local layer-tree context provided by `LayerTreeRoot`.
Use it inside custom layer-tree descendants that need access to tree items, selection state, expansion state, or row actions.
## Related APIs
* [LayerTreeRoot](../components/layer-tree-root)
* [LayerTreeItem](../components/layer-tree-item)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-toolbar.md'
description: Primitive context helper for ToolbarRoot descendants.
---
# useToolbar
`useToolbar()` reads the local toolbar context provided by `ToolbarRoot`.
Use it inside toolbar descendants that need access to tools, active state, or tool selection helpers.
## Related APIs
* [ToolbarRoot](../components/toolbar-root)
* [ToolbarItem](../components/toolbar-item)
* [useToolbarState](./use-toolbar-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-property-list.md'
description: Primitive context helper for PropertyListRoot descendants.
---
# usePropertyList
`usePropertyList()` reads the local property-list context provided by `PropertyListRoot`.
Use it inside descendants that need current items, mixed-state info, or row-level handlers for fills, strokes, or effects.
## Related APIs
* [PropertyListRoot](../components/property-list-root)
* [PropertyListItem](../components/property-list-item)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-scrub-input.md'
description: Primitive context helper for ScrubInputRoot descendants.
---
# useScrubInput
`useScrubInput()` reads the local scrub-input context provided by `ScrubInputRoot`.
Use it inside `ScrubInputField`, `ScrubInputDisplay`, or custom descendants that need editing and scrubbing state.
## Related APIs
* [ScrubInputRoot](../components/scrub-input-root)
* [ScrubInputField](../components/scrub-input-field)
* [ScrubInputDisplay](../components/scrub-input-display)
---
---
url: 'https://openpencil.dev/programmable.md'
description: >-
AI chat, CLI, JSX renderer, MCP server, and other automation surfaces built on
the OpenPencil editor engine.
---
# Automation
OpenPencil treats design files as data. Every operation available in the editor — creating shapes, setting fills, managing auto-layout, exporting assets — is also available from the terminal, from AI agents, and from code. No plugins to install, no API keys, no waiting list.
The editor UI and the automation interfaces use the same engine. If you can do it by clicking, you can do it by scripting.
## The bigger idea
OpenPencil is not just meant to be a design app.
It is also meant to be a toolkit: something you can embed into other products, wrap with your own UI, and use to build editing workflows that fit your own domain.
That is why the automation surface matters. The app, the CLI, the AI tools, the JSX renderer, the MCP server, and the SDK all build on the same underlying editor engine.
## AI Chat
The built-in assistant has access to 87 tools that cover the full surface of the editor. Describe what you want in natural language — "add a 16px drop shadow to all buttons", "create a card component with dark mode variant", "export every frame on this page at 2×".
[AI Chat →](./ai-chat)
## Collaboration
Real-time multiplayer editing over peer-to-peer WebRTC. No server, no account. Share a room link and edit together with live cursors and follow mode. Document state syncs via CRDT, so edits merge automatically even on flaky connections.
[Collaboration →](./collaboration)
## Vue SDK
Build OpenPencil-powered editors with the same Vue SDK the app uses internally. The SDK exposes editor context, canvas wiring, selection state, command models, property-panel composables, and headless primitives.
[Vue SDK →](./sdk/)
## JSX Renderer
Describe UI as JSX — the same syntax LLMs already know from React. A single call can create an entire component tree with frames, text, auto-layout, fills, and strokes. Compact, declarative, and diffable.
Going the other direction, export any selection back to JSX with Tailwind classes — useful for handing off to development or feeding designs back into an LLM.
[JSX Renderer →](./jsx-renderer)
## CLI
Inspect, lint, export, and analyze design documents without opening the editor. List pages, search nodes, extract design tokens, catch layout or accessibility issues, and render to PNG — all from the terminal with machine-readable JSON output.
The CLI also connects to the running desktop app via RPC, so you can script the editor while you're using it.
[Inspecting Files](./cli/inspecting) · [Exporting](./cli/exporting) · [Analyzing Designs](./cli/analyzing) · [Scripting](./cli/scripting)
## MCP Server
Connect Claude Code, Cursor, Windsurf, or any MCP-compatible client to OpenPencil. The server exposes 90 tools for reading, creating, and modifying designs — the same tools the built-in AI chat uses. Runs over stdio or HTTP with session support.
[MCP Server →](./mcp-server)
## Why Open?
Figma is a closed platform. Their MCP server is read-only. CDP browser access was killed in version 126. Design files live in a proprietary format on someone else's servers. Plugin development requires a custom runtime with limited APIs.
OpenPencil is the alternative: open source, MIT licensed, every operation scriptable, data stored locally. Your design files are yours — inspect them, transform them, pipe them into CI, feed them to an LLM. No permission needed.
---
---
url: 'https://openpencil.dev/reference/cli.md'
description: 'Complete reference for all openpencil commands, options, and flags.'
---
# CLI Reference
All commands accept a `.fig` file as a positional argument. When omitted, the CLI connects to the running desktop app via RPC.
## info
Show document info — pages, node counts, fonts, file size.
```sh
openpencil info [file] [--json]
```
| Option | Description |
|--------|-------------|
| `--json` | Output as JSON |
## tree
Print the node hierarchy.
```sh
openpencil tree [file] [options]
```
| Option | Description |
|--------|-------------|
| `--page` | Page name (default: first page) |
| `--depth` | Max depth (default: unlimited) |
| `--json` | Output as JSON |
## find
Search nodes by name or type.
```sh
openpencil find [file] [options]
```
| Option | Description |
|--------|-------------|
| `--name` | Node name (partial match, case-insensitive) |
| `--type` | Node type: `FRAME`, `TEXT`, `RECTANGLE`, `INSTANCE`, etc. |
| `--page` | Page name (default: all pages) |
| `--limit` | Max results (default: 100) |
| `--json` | Output as JSON |
## node
Show detailed properties of a node.
```sh
openpencil node [file] --id [--json]
```
| Option | Description |
|--------|-------------|
| `--id` | **Required.** Node ID (e.g. `1:23`) |
| `--json` | Output as JSON |
## pages
List all pages in the document.
```sh
openpencil pages [file] [--json]
```
| Option | Description |
|--------|-------------|
| `--json` | Output as JSON |
## variables
List design variables and collections.
```sh
openpencil variables [file] [options]
```
| Option | Description |
|--------|-------------|
| `--collection` | Filter by collection name |
| `--type` | Filter by type: `COLOR`, `FLOAT`, `STRING`, `BOOLEAN` |
| `--json` | Output as JSON |
## export
Export to PNG, JPG, WEBP, SVG, or JSX.
```sh
openpencil export [file] [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--format` | `-f` | `png` (default), `jpg`, `webp`, `svg`, `jsx` |
| `--output` | `-o` | Output file path (default: `.`) |
| `--scale` | `-s` | Export scale (default: 1) |
| `--quality` | `-q` | Quality 0–100, JPG/WEBP only (default: 90) |
| `--page` | | Page name (default: first page) |
| `--node` | | Node ID to export (default: all top-level nodes) |
| `--style` | | JSX style: `openpencil` (default), `tailwind` |
| `--thumbnail` | | Export page thumbnail instead of full render |
| `--width` | | Thumbnail width (default: 1920) |
| `--height` | | Thumbnail height (default: 1080) |
## eval
Execute JavaScript with the Figma Plugin API.
```sh
openpencil eval [file] [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--code` | `-c` | JavaScript code to execute |
| `--stdin` | | Read code from stdin |
| `--write` | `-w` | Write changes back to the input file |
| `--output` | `-o` | Write to a different file |
| `--json` | | Output as JSON |
| `--quiet` | `-q` | Suppress output |
## analyze colors
Analyze color palette usage across the document.
```sh
openpencil analyze colors [file] [options]
```
| Option | Description |
|--------|-------------|
| `--limit` | Max colors to show (default: 30) |
| `--threshold` | Distance threshold for clustering similar colors, 0–50 (default: 15) |
| `--similar` | Show similar color clusters |
| `--json` | Output as JSON |
## analyze typography
Analyze font family, size, and weight distribution.
```sh
openpencil analyze typography [file] [options]
```
| Option | Description |
|--------|-------------|
| `--group-by` | Group by: `family`, `size`, `weight` (default: show all styles) |
| `--limit` | Max styles to show (default: 30) |
| `--json` | Output as JSON |
## analyze spacing
Analyze gap and padding values across auto-layout frames.
```sh
openpencil analyze spacing [file] [options]
```
| Option | Description |
|--------|-------------|
| `--grid` | Base grid size to check against (default: 8) |
| `--json` | Output as JSON |
## analyze clusters
Find repeated node patterns — potential components.
```sh
openpencil analyze clusters [file] [options]
```
| Option | Description |
|--------|-------------|
| `--limit` | Max clusters to show (default: 20) |
| `--min-size` | Min node size in px (default: 30) |
| `--min-count` | Min instances to form a cluster (default: 2) |
| `--json` | Output as JSON |
---
---
url: 'https://openpencil.dev/programmable/cli/inspecting.md'
description: >-
Browse node trees, search by name or type, and dig into properties from the
terminal.
---
# Inspecting Files
The CLI lets you explore design documents without opening the editor. Every command also works on the live app — just omit the file argument.
::: tip Install
```sh
npm install -g @open-pencil/cli
# or
brew install open-pencil/tap/open-pencil
```
:::
## Document Info
Get a quick overview — page count, total nodes, fonts used, file size:
```sh
openpencil info design.fig
```
## Node Tree
Print the full node hierarchy:
```sh
openpencil tree design.fig
```
```
[0] [page] "Getting started" (0:46566)
[0] [section] "" (0:46567)
[0] [frame] "Body" (0:46568)
[0] [frame] "Introduction" (0:46569)
[0] [frame] "Introduction Card" (0:46570)
[0] [frame] "Guidance" (0:46571)
```
## Find Nodes
Search by type:
```sh
openpencil find design.fig --type TEXT
```
Search by name:
```sh
openpencil find design.fig --name "Button"
```
Both flags can be combined to narrow results further.
## Query with XPath
Use XPath selectors to find nodes by type, attributes, and tree structure:
```sh
openpencil query design.fig "//FRAME"
```
### Useful patterns
**By type:**
```sh
openpencil query design.fig "//TEXT" # All text nodes
openpencil query design.fig "//COMPONENT" # All components
openpencil query design.fig "//INSTANCE" # All instances
```
**By attributes:**
```sh
openpencil query design.fig "//FRAME[@width < 300]" # Frames under 300px wide
openpencil query design.fig "//*[@cornerRadius > 0]" # Rounded corners
openpencil query design.fig "//*[@visible = false]" # Hidden nodes
openpencil query design.fig "//TEXT[@fontSize >= 24]" # Large text
openpencil query design.fig "//*[@opacity < 1]" # Semi-transparent nodes
```
**By name and text content:**
```sh
openpencil query design.fig "//TEXT[contains(@name, 'Button')]" # Name contains 'Button'
openpencil query design.fig "//TEXT[contains(@text, 'Hello')]" # Text content contains 'Hello'
```
**By hierarchy:**
```sh
openpencil query design.fig "//SECTION//TEXT" # Text inside sections
openpencil query design.fig "//FRAME/TEXT" # Direct text children of frames
openpencil query design.fig "//COMPONENT_SET//INSTANCE" # Instances inside component sets
```
### Queryable attributes
`name`, `width`, `height`, `x`, `y`, `visible`, `opacity`, `cornerRadius`, `fontSize`, `fontFamily`, `fontWeight`, `layoutMode`, `itemSpacing`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `strokeWeight`, `rotation`, `locked`, `blendMode`, `text`, `lineHeight`, `letterSpacing`
### Example output
```
Found 5 nodes
[0] [frame] "Logo 92×32" (0:9)
[1] [frame] "logo-short-6 31×32" (0:10)
[2] [frame] "wrapper 128×73" (0:20)
[3] [frame] "pen-drawing 148×52" (0:21)
[4] [frame] "surprised-emoji 32×32" (0:26)
```
## Node Details
Inspect all properties of a specific node by its ID:
```sh
openpencil node design.fig --id 1:23
```
## Pages
List all pages in the document:
```sh
openpencil pages design.fig
```
## Variables
List design variables and their collections:
```sh
openpencil variables design.fig
```
## Live App Mode
When the desktop app is running, omit the file argument — the CLI connects via RPC and operates on the live canvas:
```sh
openpencil tree # inspect the live document
openpencil eval -c "..." # query the editor
```
## Lint Designs
Check documents for naming, layout, structure, and accessibility issues:
```sh
openpencil lint design.fig
openpencil lint design.pen --preset strict
openpencil lint design.fig --rule color-contrast
openpencil lint design.fig --list-rules
```
Use `--json` for machine-readable output.
## JSON Output
All commands support `--json` for machine-readable output — pipe into `jq`, feed to CI scripts, or process with other tools:
```sh
openpencil tree design.fig --json | jq '.[] | .name'
```
---
---
url: 'https://openpencil.dev/programmable/cli/exporting.md'
description: >-
Export document content to PNG, JPG, WEBP, SVG, `.fig`, or JSX, and convert
between document formats.
---
# Exporting
Export designs from the terminal — raster images, vectors, `.fig` subsets, or JSX code.
## Image Export
```sh
openpencil export design.fig # PNG (default)
openpencil export design.fig -f jpg -s 2 -q 90 # JPG at 2×, quality 90
openpencil export design.fig -f webp -s 3 # WEBP at 3×
openpencil export design.fig -f svg # SVG vector
openpencil export design.fig -f fig --page "Page 1" # export one page as .fig
openpencil export design.fig -f fig --node 1:23 # export one node as .fig
```
Options:
* `-f` — format: `png`, `jpg`, `webp`, `svg`, `jsx`
* `-s` — scale: `1`–`4`
* `-q` — quality: `0`–`100` (JPG/WEBP only)
* `-o` — output path
* `--page` — page name
* `--node` — specific node ID
## JSX Export
Export as JSX with Tailwind utility classes:
```sh
openpencil export design.fig -f jsx --style tailwind
```
Output:
```html
Card Title
Description text
```
Also supports `--style openpencil` for the native JSX format (see [JSX Renderer](../jsx-renderer)).
## Thumbnails
```sh
openpencil export design.fig --thumbnail --width 1920 --height 1080
```
## Live App Mode
Omit the file to export from the running app:
```sh
openpencil export -f png # screenshot the current canvas
```
---
---
url: 'https://openpencil.dev/programmable/cli/analyzing.md'
description: 'Audit colors, typography, spacing, and repeated patterns in .fig files.'
---
# Analyzing Designs
The `analyze` commands audit an entire design system from the terminal — find inconsistencies, extract the real palette, spot components waiting to be extracted.
## Colors
```sh
openpencil analyze colors design.fig
```
Finds every color in the file, counts usage, and shows a visual histogram:
```
#1d1b20 ██████████████████████████████ 17155×
#49454f ██████████████████████████████ 9814×
#ffffff ██████████████████████████████ 8620×
#6750a4 ██████████████████████████████ 3967×
```
## Typography
```sh
openpencil analyze typography design.fig
```
Lists every font family, size, and weight combination with usage counts. Useful for spotting one-off text styles that should be consolidated.
## Spacing
```sh
openpencil analyze spacing design.fig
```
Audits gap and padding values across auto-layout frames. Helps identify spacing scale inconsistencies — e.g. a stray `13px` gap among otherwise `8/16/24` values.
## Clusters
```sh
openpencil analyze clusters design.fig
```
Finds repeated node patterns that could be extracted into components:
```
3771× frame "container" (100% match)
size: 40×40, structure: Frame > [Frame]
2982× instance "Checkboxes" (100% match)
size: 48×48, structure: Instance > [Frame]
```
## JSON Output
All analyze commands support `--json` for machine-readable output:
```sh
openpencil analyze colors design.fig --json
```
Pipe into `jq`, feed into CI checks, or use in scripts that enforce design token budgets.
---
---
url: 'https://openpencil.dev/programmable/cli/scripting.md'
description: >-
Execute JavaScript with a Figma-compatible Plugin API to query, batch-modify,
and generate designs.
---
# Scripting
`openpencil eval` runs JavaScript against an OpenPencil document with a Figma-compatible `figma` global. Use it for headless batch edits, inspection, fixture setup, and automation without opening the editor UI.
## Basic usage
```sh
openpencil eval design.fig -c "return figma.currentPage.children.length"
```
The `-c` flag accepts JavaScript. If the code does not start with `return`, OpenPencil wraps it in an async function and returns the value from that function when present.
```sh
openpencil eval design.fig -c "
const frame = figma.createFrame()
frame.name = 'Card'
frame.resize(300, 200)
frame.layoutMode = 'VERTICAL'
frame.itemSpacing = 12
return { id: frame.id, name: frame.name }
"
```
## Query nodes
```sh
openpencil eval design.fig -c "
return figma.currentPage
.findAll((node) => node.type === 'FRAME' && node.name.includes('Button'))
.map((button) => ({
id: button.id,
name: button.name,
width: button.width,
height: button.height
}))
"
```
## Modify and save
Use `--write` / `-w` to write changes back to the input file:
```sh
openpencil eval design.fig -c "
figma.currentPage.children.forEach((node) => {
node.opacity = 0.5
})
" --write
```
Use `--output` / `-o` to write to a new file:
```sh
openpencil eval design.fig -c "figma.currentPage.name = 'Updated'" -o updated.fig
```
## Read scripts from stdin
```sh
cat transform.js | openpencil eval design.fig --stdin --write
```
## Live app mode
Omit the file path to run against the currently open document in the desktop app:
```sh
openpencil eval -c "return figma.currentPage.name"
```
The desktop app must be running with a document open.
## Output
By default, non-TTY output is JSON. Use `--json` to force JSON output:
```sh
openpencil eval design.fig -c "return figma.currentPage.children.map((n) => n.name)" --json
```
Use `--quiet` / `-q` to suppress output when only writing a file.
## Supported API surface
The API is intentionally close to Figma's Plugin API, but it maps to OpenPencil's scene graph and file format.
### Document and pages
* `figma.root`
* `figma.currentPage`
* `figma.currentPage.selection`
* `figma.getNodeById(id)`
* `figma.createPage()`
### Node creation
* `figma.createFrame()`
* `figma.createRectangle()`
* `figma.createEllipse()`
* `figma.createText()`
* `figma.createLine()`
* `figma.createPolygon()`
* `figma.createStar()`
* `figma.createVector()`
* `figma.createComponent()`
* `figma.createSection()`
### Tree operations
* `node.children`
* `node.parent`
* `node.appendChild(child)`
* `node.insertChild(index, child)`
* `node.clone()`
* `node.remove()`
* `node.findAll(callback?)`
* `node.findOne(callback)`
* `node.findChild(callback)`
* `node.findChildren(callback?)`
* `figma.group(nodes, parent)`
* `figma.ungroup(node)`
### Components
* `figma.createComponentFromNode(node)`
* `component.createInstance()`
* `instance.mainComponent`
### Variables
* `figma.getLocalVariables(type?)`
* `figma.getVariableById(id)`
* `figma.getLocalVariableCollections()`
* `figma.getVariableCollectionById(id)`
* `figma.createVariable(name, type, collectionId, value?)`
* `figma.setVariableValue(variableId, modeId, value)`
* `figma.deleteVariable(id)`
* `figma.createVariableCollection(name)`
* `figma.deleteVariableCollection(id)`
* `figma.bindVariable(nodeId, field, variableId)`
* `figma.unbindVariable(nodeId, field)`
### Properties
Common node properties are readable/writable through the proxy, including:
* Geometry: `x`, `y`, `width`, `height`, `rotation`, `resize(width, height)`
* Appearance: `fills`, `strokes`, `effects`, `opacity`, `visible`, `locked`, `blendMode`, `clipsContent`
* Radius: `cornerRadius`, `topLeftRadius`, `topRightRadius`, `bottomRightRadius`, `bottomLeftRadius`
* Text: `characters`, `fontSize`, `fontName`, `fontWeight`, alignment, line height, letter spacing, style-run helpers
* Auto-layout: `layoutMode`, `primaryAxisAlignItems`, `counterAxisAlignItems`, `itemSpacing`, padding, sizing, and layout positioning fields
* Stroke helpers: `strokeWeight`, `strokeAlign`, `dashPattern`
### Utilities
* `figma.mixed`
* `figma.createImage(data)`
* `figma.loadFontAsync(fontName)` no-ops because OpenPencil does not gate text edits on plugin font loading
* `figma.listAvailableFontsAsync()` returns host-provided fonts when available
* `figma.notify(message)` logs a warning in headless mode
* `figma.viewport`
## Not yet Figma-compatible
These Figma APIs are not exposed as compatible helpers yet:
* `node.exportAsync()`
* `node.setBoundVariable(field, variable)`
* `node.detachInstance()`
* `figma.combineAsVariants(components, parent)`
* Figma style APIs such as `figma.createPaintStyle()` / `figma.createTextStyle()`
* Full vector boolean operation parity
Use OpenPencil CLI export commands, core tools, or direct scene-graph helpers where available.
---
---
url: 'https://openpencil.dev/programmable/jsx-renderer.md'
description: >-
Create designs with JSX — the syntax LLMs already know from millions of React
components.
---
# JSX Renderer
OpenPencil uses JSX as its design creation language. LLMs have seen millions of React components — describing a layout as `` is natural, no special training needed. Every token matters when an AI agent performs dozens of operations, and JSX is the most compact declarative representation.
JSX is also diffable. When an AI modifies a design, the change is a JSX diff — readable, reviewable, version-controllable.
## Creating Designs
The `render` tool (available in AI chat, MCP, and CLI eval) accepts JSX:
```jsx
Card Title
Description text
```
In the MCP server and AI chat, the `render` tool accepts JSX strings directly. In the CLI, use the `export` command to go the other direction — [exporting designs as JSX](./cli/exporting).
## Elements
All node types are available as JSX elements:
| Element | Creates | Aliases |
|---------|---------|---------|
| ` ` | Frame (container, supports auto-layout) | `` |
| `` | Rectangle | `` |
| `` | Ellipse / circle | |
| `` | Text node (children become text content) | |
| `` | Line | |
| `` | Star | |
| `` | Polygon | |
| `` | Vector path | |
| `` | Group | |
| `` | Section | |
## Style Props
Compact shorthand props inspired by Tailwind's naming.
### Layout
| Prop | Description |
|------|-------------|
| `flex` | `"row"` or `"col"` — enables auto-layout |
| `gap` | Space between children |
| `wrap` | Wrap children to next line |
| `rowGap` | Counter-axis spacing when wrapping |
| `justify` | `"start"`, `"end"`, `"center"`, `"between"` |
| `items` | `"start"`, `"end"`, `"center"`, `"stretch"` |
| `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl` | Padding |
### Size & Position
| Prop | Description |
|------|-------------|
| `w`, `h` | Width/height — number, `"fill"`, or `"hug"` |
| `minW`, `maxW`, `minH`, `maxH` | Size constraints |
| `x`, `y` | Position |
### Appearance
| Prop | Description |
|------|-------------|
| `bg` | Background fill (hex color) |
| `fill` | Alias for `bg` |
| `stroke` | Stroke color |
| `strokeWidth` | Stroke width (default: 1) |
| `rounded` | Corner radius (or `roundedTL`, `roundedTR`, `roundedBL`, `roundedBR`) |
| `cornerSmoothing` | iOS-style smooth corners (0–1) |
| `opacity` | 0–1 |
| `shadow` | Drop shadow (e.g. `"0 4 8 #00000040"`) |
| `blur` | Layer blur radius |
| `rotate` | Rotation in degrees |
| `blendMode` | Blend mode |
| `overflow` | `"hidden"` or `"visible"` |
### Typography
| Prop | Description |
|------|-------------|
| `size` / `fontSize` | Font size |
| `font` / `fontFamily` | Font family |
| `weight` / `fontWeight` | `"bold"`, `"medium"`, `"normal"`, or number |
| `color` | Text color |
| `textAlign` | `"left"`, `"center"`, `"right"`, `"justified"` |
## Exporting to JSX
Convert existing designs back to JSX:
```sh
openpencil export design.fig -f jsx # OpenPencil format
openpencil export design.fig -f jsx --style tailwind # Tailwind classes
```
The round-trip works: export a design as JSX, modify the code, render it back.
## Visual Diffing
Because designs are representable as JSX, changes become code diffs:
```diff
- Old Title
+ New Title
Description
```
This makes design changes reviewable in pull requests, trackable in version control, and auditable in CI.
---
---
url: 'https://openpencil.dev/programmable/mcp-server.md'
description: >-
Connect Claude Code, Cursor, Windsurf, and other MCP clients to OpenPencil for
AI-assisted design inspection and editing.
---
# MCP Server
OpenPencil includes an MCP (Model Context Protocol) server that lets AI coding tools — Claude Code, Cursor, Windsurf, etc. — read and modify designs through the running app.
Two transports: **stdio** for MCP clients, **HTTP** for browser extensions and scripts.
## Install
```sh
npm install -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
The stdio server connects to the running OpenPencil app via WebSocket (port 7601). Make sure the desktop app is open with a document loaded.
### Claude Code
Install the MCP package and register it with Claude Code:
```sh
npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp
```
Check the connection:
```sh
claude mcp list
```
Claude Code asks before using each MCP tool unless you allow the server's tools. To auto-approve OpenPencil tools only, add this to `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": ["mcp__open-pencil__*"]
}
}
```
This is narrower than `--permission-mode bypassPermissions`, which skips prompts for every tool. You can also approve tools interactively from Claude's prompt by choosing “Yes, and don't ask again”.
Example prompt:
```text
Use the open-pencil MCP server to inspect the current page and create a small hero section on the canvas.
```
### Other MCP clients
Add to your MCP config (for example `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
Or run from source without installing:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/stdio.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/stdio.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/index.ts` / `npx tsx packages/mcp/src/index.ts`
Security defaults (HTTP transport):
* Binds to `127.0.0.1` by default (`HOST` to override)
* `eval` tool is disabled
* File operations are limited to `OPENPENCIL_MCP_ROOT` (defaults to current working directory)
* CORS is disabled by default; set `OPENPENCIL_MCP_CORS_ORIGIN` to allow one origin
* Optional auth token: `OPENPENCIL_MCP_AUTH_TOKEN` (client sends `Authorization: Bearer ` or `x-mcp-token`)
Server starts on port 7600 (override with `PORT` env var). Endpoints:
* `GET /health` — server status
* `POST /mcp` — MCP Streamable HTTP (SSE). Sessions via `mcp-session-id` header.
## Workflow
1. **Open** — `open_file` to load an existing `.fig`, or `new_document` for a blank canvas
2. **Read** — `get_page_tree`, `find_nodes`, `get_node`, `list_pages`
3. **Create** — `create_shape`, `render` (JSX)
4. **Modify** — `set_fill`, `set_stroke`, `set_layout`, `update_node`, `set_effects`
5. **Structure** — `reparent_node`, `group_nodes`, `clone_node`, `delete_node`
6. **Save** — `save_file` to write back to `.fig`
## AI Agent Skill
Teach your AI coding agent to use OpenPencil tools:
```sh
npx skills add open-pencil/skills@open-pencil
```
Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports [skills](https://skills.sh). The skill covers the CLI, MCP tools, JSX rendering, eval, and the running app's automation bridge.
## Tools (90)
### Document
| Tool | Description |
|------|-------------|
| `open_file` | Open a `.fig` file for editing |
| `save_file` | Save the current document to a `.fig` file |
| `new_document` | Create a new empty document |
### Read
| Tool | Description |
|------|-------------|
| `get_selection` | Get currently selected nodes |
| `get_page_tree` | Get the full node tree of the current page |
| `get_current_page` | Get the current page name and ID |
| `get_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `get_components` | List all components in the document |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
| `list_fonts` | List fonts used in the current page |
| `page_bounds` | Get bounding box of all objects on the current page |
| `node_bounds` | Get bounding box of a node |
| `node_ancestors` | Get ancestor chain of a node |
| `node_children` | Get direct children of a node |
| `node_tree` | Get the subtree rooted at a node |
| `node_bindings` | Get variable bindings on a node |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (`FRAME`, `RECTANGLE`, `ELLIPSE`, `TEXT`, `LINE`, `STAR`, `POLYGON`, `SECTION`) |
| `create_vector` | Create a vector node from a path string |
| `create_slice` | Create an export slice |
| `create_page` | Create a new page |
| `render` | Render JSX to design nodes — create entire component trees in one call |
| `create_component` | Convert a frame/group into a component |
| `create_instance` | Create an instance of a component |
| `node_to_component` | Convert an existing node into a component in-place |
### Modify
| Tool | Description |
|------|-------------|
| `set_fill` | Set fill color (hex) |
| `set_stroke` | Set stroke color, weight, alignment |
| `set_effects` | Add shadow or blur effects |
| `update_node` | Update position, size, opacity, corner radius, text, font |
| `set_layout` | Set auto-layout (flexbox) — direction, spacing, padding, alignment |
| `set_constraints` | Set resize constraints |
| `set_rotation` | Set rotation angle in degrees |
| `set_opacity` | Set opacity (0–1) |
| `set_radius` | Set corner radius (uniform or per-corner) |
| `set_minmax` | Set min/max width and height constraints |
| `set_text` | Set text content of a `TEXT` node |
| `set_font` | Set font family and weight |
| `set_font_range` | Set font properties on a character range |
| `set_text_resize` | Set text auto-resize mode (fixed/auto-width/auto-height) |
| `set_visible` | Show or hide a node |
| `set_blend` | Set blend mode |
| `set_locked` | Lock or unlock a node |
| `set_stroke_align` | Set stroke alignment (inside/center/outside) |
| `set_text_properties` | Set text layout: alignment, auto-resize, text case, decoration, truncation |
| `set_layout_child` | Configure auto-layout child: sizing, grow, alignment, absolute positioning |
| `node_move` | Move a node to a new position |
| `node_resize` | Resize a node |
| `node_replace_with` | Replace a node with another node |
| `arrange` | Align or distribute selected nodes |
### Structure
| Tool | Description |
|------|-------------|
| `delete_node` | Delete a node |
| `clone_node` | Duplicate a node |
| `rename_node` | Rename a node |
| `reparent_node` | Move a node into a different parent |
| `select_nodes` | Select nodes by ID |
| `group_nodes` | Group nodes |
| `ungroup_node` | Ungroup a group |
| `flatten_nodes` | Flatten nodes into a single vector |
| `boolean_union` | Boolean union of two or more nodes |
| `boolean_subtract` | Boolean subtraction |
| `boolean_intersect` | Boolean intersection |
| `boolean_exclude` | Boolean exclusion |
### Vector Path
| Tool | Description |
|------|-------------|
| `path_get` | Get the path data of a vector node |
| `path_set` | Set the path data of a vector node |
| `path_scale` | Scale a vector path |
| `path_flip` | Flip a vector path horizontally or vertically |
| `path_move` | Translate a vector path |
### Export
| Tool | Description |
|------|-------------|
| `export_image` | Export nodes as PNG, JPG, or WEBP. Returns base64-encoded image data |
| `export_svg` | Export nodes as SVG markup |
### Viewport
| Tool | Description |
|------|-------------|
| `viewport_get` | Get current viewport position and zoom level |
| `viewport_set` | Set viewport position and zoom |
| `viewport_zoom_to_fit` | Zoom viewport to fit specified nodes |
### Variables
| Tool | Description |
|------|-------------|
| `get_variable` | Get a variable by ID or name |
| `find_variables` | Find variables by name pattern or type |
| `create_variable` | Create a new variable in a collection |
| `set_variable` | Set a variable value in a mode |
| `delete_variable` | Delete a variable |
| `bind_variable` | Bind a variable to a node property |
| `get_collection` | Get a variable collection by ID or name |
| `create_collection` | Create a new variable collection |
| `delete_collection` | Delete a variable collection |
### Analyze
| Tool | Description |
|------|-------------|
| `analyze_colors` | Analyze color palette usage across the document |
| `analyze_typography` | Analyze font/size/weight distribution |
| `analyze_spacing` | Analyze gap and padding values |
| `analyze_clusters` | Detect repeated patterns (potential components) |
### Diff
| Tool | Description |
|------|-------------|
| `diff_create` | Create a snapshot of the current document state |
| `diff_show` | Show differences between the current state and a snapshot |
### Navigation
| Tool | Description |
|------|-------------|
| `switch_page` | Switch to a page by name or ID |
### Escape Hatch
| Tool | Description |
|------|-------------|
| `eval` | Execute JavaScript with full Figma Plugin API access |
Note: `eval` is available over stdio, but disabled in HTTP mode for security.
---
---
url: 'https://openpencil.dev/programmable/ai-chat.md'
description: Built-in AI assistant with 90+ tools for creating and modifying designs.
---
# AI Chat
Press ⌘J (Ctrl + J) to open the AI assistant. Describe what you want — it creates shapes, sets styles, manages layout, works with components, and analyzes your design.
## Setup
1. Open the AI chat panel (⌘J)
2. Click the settings icon
3. Choose a provider and enter your API key
4. Select a model
### Supported Providers
| Provider | Models | Setup |
|----------|--------|-------|
| **OpenRouter** | Claude, GPT, Gemini, DeepSeek, Qwen, and others | API key from [openrouter.ai](https://openrouter.ai) |
| **Anthropic** | Claude Sonnet 4.6, Claude Opus 4.6 | API key from [console.anthropic.com](https://console.anthropic.com) |
| **OpenAI** | GPT-5.3 Codex, GPT-4.1, o3, o4-mini | API key from [platform.openai.com](https://platform.openai.com) |
| **Google AI** | Gemini 3.1 Pro, Gemini 3 Flash | API key from [aistudio.google.dev](https://aistudio.google.dev) |
| **Z.ai** | GLM-5.1, GLM-5, GLM-4.7, GLM-4.5 family | API key from [docs.z.ai](https://docs.z.ai/devpack/quick-start) |
| **MiniMax** | MiniMax M2.7, M2.7-highspeed, M2.5, M2.1 | API key from [platform.minimax.io](https://platform.minimax.io/user-center/basic-information/interface-key) |
| **OpenAI-compatible** | Any endpoint with OpenAI API format | Custom base URL + key. Supports Completions and Responses API toggle. |
| **Anthropic-compatible** | Any endpoint with Anthropic API format | Custom base URL + key |
No backend, no subscription — your key talks directly to the provider.
## What It Can Do
The assistant has 90+ tools across these categories:
* **Create** — frames, shapes, text, components, pages. Renders JSX for complex layouts.
* **Style** — fills, strokes, effects, opacity, corner radius, blend modes.
* **Layout** — auto-layout, grid, alignment, spacing, sizing.
* **Components** — create components, instances, component sets. Manage overrides.
* **Variables** — create/edit variables, collections, modes. Bind to fills.
* **Query** — find nodes, XPath selectors, read properties, list pages, fonts, selection.
* **Inspect** — `get_jsx` for JSX roundtrip view, `diff_jsx` for structural diffs, `describe` for semantic role and design issue detection.
* **Analyze** — color palette, typography audit, spacing consistency, cluster detection.
* **Export** — PNG, SVG, JSX with Tailwind classes. Vision-based verification via `export_image`.
* **Vector** — boolean operations, path manipulation.
## Visual Verification
The assistant can verify its work visually. After creating or modifying designs, it uses `export_image` to capture a screenshot and checks the result against the original request. This catches layout issues, missing elements, and color mismatches that text-only responses would miss.
## Example Prompts
* "Create a card with a title, description, and a blue button"
* "Make all buttons on this page use the same border radius"
* "What fonts are used in this file?"
* "Change the background of the selected frame to a gradient from blue to purple"
* "Export the selected frame as SVG"
* "Find all text nodes with font size less than 12"
* "Describe the selected component — what role does it look like?"
* "Show me the JSX for this frame"
## Tips
* Select nodes before asking — the assistant knows what's selected.
* Be specific about colors, sizes, and positions for precise results.
* The assistant can modify multiple nodes in one message.
* Use "undo" in the editor if you don't like the result — AI mutations support full undo.
* All layout is recomputed automatically after each tool execution.
---
---
url: 'https://openpencil.dev/programmable/collaboration.md'
description: 'Real-time collaborative editing via P2P WebRTC — no server, no account.'
---
# Collaboration
Edit designs together in real time. Peers connect directly — no server relays your data, no account required.
## Sharing a Room
1. Click the share button in the top-right corner
2. Copy the generated link (`app.openpencil.dev/share/`)
3. Send it to your collaborators
Anyone with the link can join. The room stays active as long as at least one participant has the page open.
## What Syncs
* **Document changes** — every edit (shapes, text, properties, layout) syncs instantly
* **Cursors** — see where each collaborator is pointing, with their name and color
* **Selections** — highlighted selections are visible to everyone
## Follow Mode
Click a collaborator's avatar in the top bar to follow their viewport. Your canvas pans and zooms to match their view. Click again to stop following.
## How It Works
Peers connect directly via WebRTC — your design data goes straight from browser to browser, never through a central server. The document state uses a CRDT (conflict-free replicated data type), so concurrent edits merge automatically without conflicts.
The room persists locally — if you refresh the page, you rejoin with the same state.
## Tips
* Works in the browser and the desktop app
* Room IDs are cryptographically random — only people with the link can join
* Stale cursors are cleaned up automatically when someone disconnects
---
---
url: 'https://openpencil.dev/reference/keyboard-shortcuts.md'
description: >-
Figma-compatible keyboard shortcut reference for OpenPencil tools, editing
commands, view controls, and layer actions.
---
# Keyboard Shortcuts
Full Figma-compatible shortcut map. ✅ = implemented.
::: tip Symbol Legend
⌘ = Cmd (Mac) / Ctrl (Win/Linux) · ⇧ = Shift · ⌥ = Alt/Option · ⌃ = Ctrl · ⌫ = Backspace
:::
## Tools
| Key | Tool | Status |
|-----|------|--------|
| V | Move/Select | ✅ |
| K | Scale | 🔲 |
| H | Hand | ✅ |
| F | Frame | ✅ |
| S | Section | ✅ |
| R | Rectangle | ✅ |
| O | Ellipse | ✅ |
| L | Line | ✅ |
| — | Polygon | ✅ (flyout only) |
| — | Star | ✅ (flyout only) |
| ⇧L | Arrow | 🔲 |
| P | Pen | ✅ |
| ⇧P | Pencil | 🔲 |
| T | Text | ✅ |
| C | Comment | 🔲 |
| I | Eyedropper | 🔲 |
## File
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘N | New Tab | ✅ |
| ⌘T | New Tab | ✅ |
| ⌘O | Open File | ✅ |
| ⌘W | Close Tab | ✅ |
| ⌘S | Save | ✅ |
| ⇧⌘S | Save As | ✅ |
| ⇧⌘E | Export… | ✅ |
## Edit
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘Z | Undo | ✅ |
| ⇧⌘Z | Redo | ✅ |
| ⌘X | Cut | ✅ |
| ⌘C | Copy | ✅ |
| ⌘V | Paste | ✅ |
| ⇧⌘V | Paste Over Selection | 🔲 |
| ⌘D | Duplicate | ✅ |
| ⌫ | Delete | ✅ |
| ⌘A | Select All | ✅ |
| ⇧⌘A | Select Inverse | 🔲 |
| ⇧⌘C | Copy as PNG | ✅ |
| ⌥⌘C | Copy Properties | 🔲 |
| ⌥⌘V | Paste Properties | 🔲 |
## View
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘' | Pixel Grid | 🔲 |
| ⌃G | Layout Guides | 🔲 |
| ⇧R | Rulers | 🔲 |
| ⌘\ | Show/Hide UI | ✅ |
| ⌘= | Zoom In | ✅ |
| ⌘- | Zoom Out | ✅ |
| ⌘0 | Zoom to 100% | ✅ |
| ⌘1 | Zoom to Fit | ✅ |
| ⌘2 | Zoom to Selection | ✅ |
| ⇧1 | Zoom to Fit (alt) | ✅ |
| ⇧2 | Zoom to Selection (alt) | ✅ |
| ⌘J | Toggle AI Chat | ✅ |
## Object
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌥⌘G | Frame Selection | 🔲 |
| ⌘G | Group Selection | ✅ |
| ⇧⌘G | Ungroup | ✅ |
| ⇧A | Add Auto Layout | ✅ |
| ⌥⌘K | Create Component | ✅ |
| ⇧⌘K | Create Component Set | ✅ |
| ⌥⌘B | Detach Instance | ✅ |
| ⌘] | Bring Forward | 🔲 |
| ⌥⌘] | Bring to Front | 🔲 |
| ] | Bring to Front | ✅ |
| ⌘\[ | Send Backward | 🔲 |
| ⌥⌘\[ | Send to Back | 🔲 |
| \[ | Send to Back | ✅ |
| ⇧H | Flip Horizontal | 🔲 |
| ⇧V | Flip Vertical | 🔲 |
| ⇧⌘H | Toggle Visibility | ✅ |
| ⇧⌘L | Toggle Lock | ✅ |
| ⌘E | Flatten | 🔲 |
## Text
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘B | Bold | ✅ |
| ⌘I | Italic | ✅ |
| ⌘U | Underline | ✅ |
| ⇧⌘X | Strikethrough | 🔲 |
## Canvas Interaction
| Input | Action | Status |
|-------|--------|--------|
| Click | Select node | ✅ |
| Shift+Click | Add/remove from selection | ✅ |
| Alt+Drag | Duplicate and move | ✅ |
| Shift+Drag (draw) | Constrain to square/circle | ✅ |
| Shift+Drag (resize) | Maintain aspect ratio | ✅ |
| Shift+Drag (rotate) | Snap to 15° | ✅ |
| Middle mouse drag | Pan | ✅ |
| Scroll | Pan | ✅ |
| Ctrl+Scroll / Pinch | Zoom | ✅ |
| Double-click text | Edit text inline | ✅ |
| Drag onto frame | Reparent into frame | ✅ |
| Escape | Deselect / Cancel | ✅ |
---
---
url: 'https://openpencil.dev/reference/node-types.md'
description: >-
Reference for OpenPencil scene graph node types, Figma Kiwi schema mappings,
and supported engine node behavior.
---
# Node Types
The scene graph supports 28 node types from Figma's Kiwi schema. Each node is identified by a GUID (`sessionID:localID`) and has a parent reference via `parentIndex`. The OpenPencil engine's `NodeType` union currently uses 18 of these types.
## Type Table
28 Figma schema types + 1 synthetic engine type. Types marked ✅ are in the engine's `NodeType` union (18 total).
| Type | ID | Description | Engine |
|------|----|-------------|--------|
| `DOCUMENT` | 1 | Root node, one per file | — |
| `CANVAS` | 2 | Page | ✅ |
| `GROUP` | 3 | Group container | ✅ |
| `FRAME` | 4 | Primary container (artboard), supports auto-layout | ✅ |
| `BOOLEAN_OPERATION` | 5 | Union/subtract/intersect/exclude result | ✅ |
| `VECTOR` | 6 | Freeform vector path | ✅ |
| `STAR` | 7 | Star shape | ✅ |
| `LINE` | 8 | Line | ✅ |
| `ELLIPSE` | 9 | Ellipse/circle, supports arc data | ✅ |
| `RECTANGLE` | 10 | Rectangle | ✅ |
| `REGULAR_POLYGON` | 11 | Regular polygon (3–12 sides, engine uses `POLYGON`) | ✅ |
| `ROUNDED_RECTANGLE` | 12 | Rectangle with smooth corners | ✅ |
| `TEXT` | 13 | Text with rich formatting | ✅ |
| `SLICE` | 14 | Export region | |
| `SYMBOL` | 15 | Component (main, engine uses `COMPONENT`) | ✅ |
| `INSTANCE` | 16 | Component instance | ✅ |
| `STICKY` | 17 | FigJam sticky note | |
| `SHAPE_WITH_TEXT` | 18 | FigJam shape | ✅ |
| `CONNECTOR` | 19 | Connector line between nodes | ✅ |
| `CODE_BLOCK` | 20 | FigJam code block | |
| `WIDGET` | 21 | Plugin widget | |
| `STAMP` | 22 | FigJam stamp | |
| `MEDIA` | 23 | Video/GIF | |
| `HIGHLIGHT` | 24 | FigJam highlight | |
| `SECTION` | 25 | Canvas section (organizational, top-level only) | ✅ |
| `SECTION_OVERLAY` | 26 | Section overlay | |
| `WASHI_TAPE` | 27 | FigJam washi tape | |
| `VARIABLE` | 28 | Variable definition node | |
| `COMPONENT_SET` | — | Variant group container (synthetic, mapped from `SYMBOL`) | ✅ |
### Engine NodeType Union (18 types)
The engine's `NodeType` uses simplified names. Some differ from the Kiwi schema:
* `COMPONENT` → Kiwi `SYMBOL` (ID 15)
* `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
* `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
type NodeType =
| 'CANVAS' | 'FRAME' | 'RECTANGLE' | 'ROUNDED_RECTANGLE'
| 'ELLIPSE' | 'TEXT' | 'LINE' | 'STAR' | 'POLYGON'
| 'VECTOR' | 'BOOLEAN_OPERATION' | 'GROUP' | 'SECTION'
| 'COMPONENT' | 'COMPONENT_SET' | 'INSTANCE'
| 'CONNECTOR' | 'SHAPE_WITH_TEXT'
```
## Node Hierarchy
```
Document
├── Canvas (Page 1)
│ ├── Section (top-level only, title pill, auto-adopts siblings)
│ │ ├── Frame
│ │ │ └── ...children
│ │ └── Rectangle
│ ├── Frame
│ │ ├── Rectangle
│ │ ├── Text
│ │ └── Frame (nested)
│ │ ├── Ellipse
│ │ └── Instance (→ references Component)
│ ├── Component
│ │ └── ...children
│ ├── Group
│ │ └── ...children
│ └── BooleanOperation
│ └── ...operand shapes
└── Canvas (Page 2)
└── ...
```
## Core Properties
Every node carries these fields (subset of `NodeChange`):
### Identity & Tree
* `guid` — unique identifier (`sessionID:localID`)
* `type` — node type enum
* `name` — display name
* `phase` — `CREATED` or `REMOVED`
* `parentIndex` — parent GUID + position string for z-ordering
### Transform
* `size` — width/height vector
* `transform` — 2×3 affine matrix
* `rotation` — degrees
### Appearance
* `fillPaints[]` — fill colors/gradients/images
* `strokePaints[]` — stroke colors
* `effects[]` — shadows, blurs
* `opacity` — 0–1
* `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
* `strokeWeight` — stroke thickness
* `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
* `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
* `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
* `dashPattern[]` — dash/gap lengths
### Corners
* `cornerRadius` — uniform radius
* `cornerSmoothing` — squircle amount (0–1)
* Per-corner radii when `rectangleCornerRadiiIndependent` is true
### Visibility
* `visible` — show/hide
* `locked` — prevent editing
## Type-Specific Properties
### Text
`fontSize`, `fontName`, `lineHeight`, `letterSpacing`, `textAlignHorizontal`, `textAlignVertical`, `textAutoResize`, `textData` (characters, style overrides, baselines, glyphs)
### Vector
`vectorData` (vectorNetworkBlob, normalizedSize), `fillGeometry[]`, `strokeGeometry[]`, `handleMirroring`, `arcData`
### Layout (Frame)
`stackMode`, `stackSpacing`, `stackPadding`, `stackJustify`, `stackCounterAlign`, `stackPrimarySizing`, `stackCounterSizing`, `stackChildPrimaryGrow`, `stackChildAlignSelf`
### Component
`symbolData`, `componentKey`, `componentPropDefs[]`, `symbolDescription`
### Instance
`overriddenSymbolID`, `symbolData.symbolOverrides[]`, `componentPropRefs[]`, `componentPropAssignments[]`
## Paint
```typescript
interface Fill {
type: 'SOLID' | 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' |
'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' | 'IMAGE'
color: Color // {r, g, b, a} 0–1 floats
opacity: number // 0–1
visible: boolean
blendMode?: BlendMode
gradientStops?: GradientStop[] // for gradients
gradientTransform?: GradientTransform // 2×3 matrix
imageHash?: string // for image fills
imageScaleMode?: 'FILL' | 'FIT' | 'CROP' | 'TILE'
imageTransform?: GradientTransform
}
```
## Effect
```typescript
interface Effect {
type: 'DROP_SHADOW' | 'INNER_SHADOW' | 'LAYER_BLUR' |
'BACKGROUND_BLUR' | 'FOREGROUND_BLUR'
color: Color
offset: { x: number; y: number }
radius: number
spread: number
visible: boolean
}
```
## Stroke
```typescript
interface Stroke {
color: Color
weight: number
opacity: number
visible: boolean
align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
cap?: 'NONE' | 'ROUND' | 'SQUARE' | 'ARROW_LINES' | 'ARROW_EQUILATERAL'
join?: 'MITER' | 'BEVEL' | 'ROUND'
dashPattern?: number[]
}
```
---
---
url: 'https://openpencil.dev/reference/scene-graph.md'
description: >-
Technical reference for OpenPencil's flat scene graph, node lookup, tree
relationships, mutations, events, and traversal APIs.
---
# Scene Graph
## In-Memory Representation
Nodes live in a flat `Map` keyed by `GUID` string. The tree structure is maintained via `parentIndex` references. This gives O(1) lookup by ID and efficient traversal.
```typescript
interface SceneGraph {
nodes: Map
root: string
getNode(id: string): Node
getChildren(id: string): Node[]
getParent(id: string): Node | null
getPages(): Node[]
createNode(type: NodeType, parent: string, props: Partial): Node
updateNode(id: string, changes: Partial): void
deleteNode(id: string): void
moveNode(id: string, newParent: string, position: string): void
addPage(name: string): Node
deletePage(id: string): void
renamePage(id: string, name: string): void
findByType(type: NodeType): Node[]
findByName(pattern: string): Node[]
hitTest(point: Vector, canvas: string): Node | null
getNodesInRect(rect: Rect, canvas: string): Node[]
}
```
## Pages
Documents support multiple pages (`CANVAS` nodes as direct children of the `DOCUMENT` root). Each page has its own child tree and independent viewport state (panX, panY, zoom, pageColor). The editor tracks `currentPageId` and renders only the active page's children.
## Sections
`SECTION` nodes are top-level organizational containers (direct children of `CANVAS` only). They cannot nest inside frames or groups. Creating a section auto-adopts overlapping siblings. Sections display a title pill with luminance-adaptive text color.
## Hover State
The editor state tracks `hoveredNodeId` — the node currently under the cursor. The renderer draws a shape-aware hover outline (following actual geometry for ellipses, rounded rects, vectors) for visual feedback before selection.
## Undo/Redo
The system uses Figma's **inverse command** pattern. Each undo entry contains the forward changes and their automatically-computed inverse:
| Operation | Forward | Inverse |
|-----------|---------|---------|
| Create node | `{guid, phase: CREATED, ...props}` | `{guid, phase: REMOVED}` |
| Delete node | `{guid, phase: REMOVED}` | `{guid, phase: CREATED, ...allProps}` |
| Change prop | `{guid, fill: "#F00"}` | `{guid, fill: "#00F"}` |
| Move node | `{guid, parentIndex: newParent}` | `{guid, parentIndex: oldParent}` |
Before applying any change, affected fields are snapshotted. The snapshot becomes the inverse.
**Batching** — operations like drag-to-move produce hundreds of position changes per second. These are debounced into a single undo entry. `beginBatch`/`commitBatch` wraps multi-step operations.
## Layout Engine (Yoga)
Figma's auto-layout properties map to Yoga flexbox:
| Figma Property | Yoga Equivalent |
|---|---|
| `stackMode: HORIZONTAL` | `flexDirection: row` |
| `stackMode: VERTICAL` | `flexDirection: column` |
| `stackSpacing` | `gap` |
| `stackPadding` | `padding` |
| `stackJustify: MIN/CENTER/MAX/SPACE_BETWEEN` | `justifyContent` |
| `stackCounterAlign` | `alignItems` |
| `stackPrimarySizing: FIXED/HUG/FILL` | width/height + flex-grow |
| `stackChildPrimaryGrow` | `flexGrow` |
| `stackChildAlignSelf` | `alignSelf` |
| `stackPositioning: ABSOLUTE` | `position: absolute` |
## Hit Testing
Given a point in canvas coordinates, the scene graph returns the topmost visible node at that position. The algorithm:
1. Traverse visible nodes in reverse z-order (top to bottom)
2. Transform the test point into each node's local coordinate system
3. Check if the point is within the node's bounds (including rotation)
4. Return the first match
For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect the given rectangle.
## Extended Fill Types
Fills support six types: `SOLID`, `GRADIENT_LINEAR`, `GRADIENT_RADIAL`, `GRADIENT_ANGULAR`, `GRADIENT_DIAMOND`, and `IMAGE`. Gradient fills carry `gradientStops` (color + position pairs) and a `gradientTransform` (2×3 matrix). Image fills reference blob data via `imageHash` with scale modes (`FILL`, `FIT`, `CROP`, `TILE`).
## Extended Stroke Properties
Strokes support `cap` (`NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL`), `join` (`MITER`, `BEVEL`, `ROUND`), and `dashPattern` (array of dash/gap lengths) in addition to the base `color`, `weight`, `opacity`, `visible`, and `align` properties.
## Coordinate System
Nodes store position and size relative to their parent. To get absolute (canvas) coordinates, walk up the parent chain applying transforms. The renderer uses this to draw nested frames with correct positioning.
Rotation is stored in degrees and applied as part of the 2×3 transform matrix. Snap guides and hit testing account for rotation when computing visual bounds.
---
---
url: 'https://openpencil.dev/reference/file-format.md'
description: >-
Technical reference for OpenPencil .fig and .pen document formats, Kiwi binary
structure, import pipeline, and export behavior.
---
# File Format
## .fig File Structure
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
| Offset | Content |
|--------|---------|
| 0 | Magic header `fig-kiwi` (8 bytes) |
| 8 | Version (4 bytes, uint32 LE) |
| 12 | Schema length (4 bytes, uint32 LE) |
| 16 | Compressed Kiwi schema |
| … | Message length (4 bytes, uint32 LE) |
| … | Compressed Kiwi message — `NodeChange[]` (entire document) |
| … | Blob data — images, vector networks, fonts |
## Import Pipeline
```
.fig file → parse header → decompress Zstd → decode Kiwi schema
→ decode message → NodeChange[] → build SceneGraph
→ resolve blob refs → render on canvas
```
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → compress (Zstd/deflate)
→ build ZIP (header + schema + message + thumbnail.png)
→ write .fig file
```
Export uses ⌘S (Save) and ⇧⌘S (Save As) with native OS dialogs on the desktop app. The exported file includes a `thumbnail.png` required by Figma for file preview.
Compression uses Zstd via Tauri Rust command on desktop, with deflate fallback in the browser.
## Kiwi Binary Codec
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
| Module | Purpose |
|--------|---------|
| `kiwi-schema` | Kiwi parser (from [evanw/kiwi](https://github.com/nicolo-ribaudo/kiwi)), patched for ESM and sparse field IDs |
| `codec.ts` | Encode/decode messages using the Kiwi schema |
| `protocol.ts` | Wire format parsing and message type detection |
| `schema.ts` | 194 message/enum/struct definitions |
### Sparse Field IDs
Figma's schema uses non-contiguous field IDs (e.g. 1, 2, 5, 10 with gaps). The kiwi-schema parser handles this correctly.
### Compression
`.fig` files use Zstd compression for both the schema and message payloads. Decompression uses the `fzstd` library. For export, Zstd compression is offloaded to a Tauri Rust command on the desktop app (better performance, correct frame headers). In the browser, deflate via `fflate` is used as a fallback.
## Supported Formats
| Format | Open / Read | Save / Write | Export |
|--------|-------------|--------------|--------|
| `.fig` (Figma) | ✅ | ✅ | ✅ |
| `.pen` (Pencil) | ✅ | — | — |
| `.png` | — | — | ✅ |
| `.jpg` | — | — | ✅ |
| `.webp` | — | — | ✅ |
| `.svg` | — | — | ✅ |
| `.jsx` | — | — | ✅ |
## Clipboard Format
Copy/paste uses the same Kiwi binary encoding:
1. **Copy** — encode selected `NodeChange[]` to Kiwi binary, compress, write to clipboard as `application/x-figma-design` MIME type
2. **Paste** — read clipboard, decompress, decode Kiwi binary, create nodes in scene graph
Encoding happens synchronously in the copy event handler (not async Clipboard API) for browser compatibility. This enables bidirectional clipboard between OpenPencil and Figma.
---
---
url: 'https://openpencil.dev/guide/getting-started.md'
---
# Getting Started
## Try Online
OpenPencil runs in the browser — no installation required. Open [app.openpencil.dev](https://app.openpencil.dev) to start designing.
If you want to build on top of it instead of only using the default app, see the [Programmable](/programmable/) section and the [Vue SDK](/programmable/sdk/).
## Download Desktop App
Pre-built binaries for macOS, Windows, and Linux are available on the [releases page](https://github.com/open-pencil/open-pencil/releases/latest).
| Platform | Download |
|----------|----------|
| macOS (Apple Silicon) | `.dmg` (aarch64) |
| macOS (Intel) | `.dmg` (x64) |
| Windows (x64) | `.msi` / `.exe` |
| Windows (ARM) | `.msi` / `.exe` |
| Linux (x64) | `.AppImage` / `.deb` |
## macOS via Homebrew
```sh
brew install open-pencil/tap/open-pencil
```
This installs the latest signed release for macOS (Apple Silicon and Intel). The tap is auto-updated on each release.
## Building from Source
### Prerequisites
* [Bun](https://bun.sh/) (package manager and runtime)
* [Rust](https://rustup.rs/) (for desktop app only)
## Installation
```sh
git clone https://github.com/open-pencil/open-pencil.git
cd open-pencil
bun install
```
## Development Server
```sh
bun run dev
```
Opens the editor at `http://localhost:1420`.
## Available Scripts
| Command | Description |
|---------|-------------|
| `bun run dev` | Dev server with HMR |
| `bun run build` | Production build |
| `bun run check` | Lint (oxlint) + typecheck (tsgo) |
| `bun run test` | E2E visual regression (Playwright) |
| `bun run test:update` | Regenerate screenshot baselines |
| `bun run test:unit` | Unit tests (bun:test) |
| `bun run docs:dev` | Documentation dev server |
| `bun run docs:build` | Build documentation site |
## Desktop App (Tauri)
The desktop app requires Rust and platform-specific prerequisites.
### macOS
```sh
xcode-select --install
cargo install tauri-cli --version "^2"
bun run tauri dev
```
### Windows
1. Install [Rust](https://rustup.rs/) with `stable-msvc` toolchain:
```sh
rustup default stable-msvc
```
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++" workload
3. WebView2 is pre-installed on Windows 10 (1803+) and Windows 11
4. Run:
```sh
bun run tauri dev
```
### Linux
Install system dependencies (Debian/Ubuntu):
```sh
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
```
Then:
```sh
bun run tauri dev
```
### Build for Distribution
```sh
bun run tauri build # Current platform
bun run tauri build --target universal-apple-darwin # macOS universal
```
---
---
url: 'https://openpencil.dev/guide/features.md'
---
# Features
## Figma .fig Files
Open and save native Figma files directly. The import/export pipeline uses the same Kiwi binary codec as Figma — 194 schema definitions, ~390 fields per node. Save with ⌘S, Save As with ⇧⌘S.
**Copy & paste with Figma** — select nodes in Figma, ⌘C, switch to OpenPencil, ⌘V. Fills, strokes, auto-layout, text, effects, corner radii, and vector networks are preserved. Works both ways.
## Drawing & Editing
* **Shapes** — Rectangle (R), Ellipse (O), Line (L), Polygon, Star
* **Pen tool** — vector networks (not simple paths), bezier curves with tangent handles
* **Text** — canvas-native editing with IME support, double-click to enter edit mode
* **Rich text** — per-character bold (⌘B), italic (⌘I), underline (⌘U), strikethrough
* **Auto-layout** — flexbox and CSS Grid via Yoga WASM: direction, gap, padding, justify, align, child sizing, grid tracks. ⇧A to toggle
* **Components** — create (⌥⌘K), component sets (⇧⌘K), instances with override support, live sync
* **Variables** — design tokens with collections, modes (Light/Dark), color/float/string/boolean types, variable binding
* **Sections** — organizational containers with auto-adopting children and title pills
## Properties Panel
Context-sensitive Design | Code | AI tabs:
* **Appearance** — opacity, corner radius (uniform or per-corner), visibility
* **Fill** — solid, gradient (linear/radial/angular/diamond), image
* **Stroke** — color, weight, align (inside/center/outside), per-side weights, cap, join, dash
* **Effects** — drop shadow, inner shadow, layer blur, background blur, foreground blur
* **Typography** — font picker with virtual scroll and search, weight, size, alignment, style buttons
* **Layout** — auto-layout controls when enabled
* **Export** — scale, format (PNG/JPG/WEBP/SVG), live preview
## Rendering
Skia (CanvasKit WASM) — the same rendering engine as Figma:
* Gradient fills (linear, radial, angular, diamond)
* Image fills with scale modes
* Effects with per-node caching
* Arc data (partial ellipses, donuts)
* Viewport culling and paint reuse
* Snap guides with rotation-aware alignment
* Canvas rulers with selection badges
* Hover highlight that follows actual geometry
## Undo/Redo
Every operation is undoable — creation, deletion, moves, resizes, property changes, reparenting, layout changes, variable operations. Uses an inverse-command pattern. ⌘Z / ⇧⌘Z.
## Multi-Page Documents
Add, delete, rename pages. Each page has independent viewport state. Double-click to rename inline.
## Multi-File Tabs
Open multiple documents in tabs. ⌘T new tab, ⌘W close, ⌘O open file.
## Export
* **Image** — PNG, JPG, WEBP at configurable scale (0.5×–4×). Via panel, context menu, or ⇧⌘E
* **SVG** — shapes, text with style runs, gradients, effects, blend modes
* **Tailwind JSX** — HTML with Tailwind v4 utility classes, ready for React or Vue
* **Copy as** — text, SVG, PNG (⇧⌘C), or JSX via context menu
CLI: `openpencil export design.fig -f jsx --style tailwind`
## AI Chat
Press ⌘J to open the AI assistant. 90+ tools that can create shapes, set styles, manage layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Connect Anthropic, OpenAI, Google AI, OpenRouter, or any compatible endpoint.
Tool calls display as collapsible timeline entries. Visual verification — the assistant renders its work and checks it against your request. Full undo support for all AI mutations.
See [AI Chat](/programmable/ai-chat) for setup and provider details.
## MCP Server
Connect Claude Code, Cursor, Windsurf, or any MCP client to read and write `.fig` files headlessly. 90+ tools. Two transports: stdio and HTTP.
```sh
npm install -g @open-pencil/mcp
```
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
See [MCP Tools reference](/programmable/mcp-server) for the full tool list.
## CLI
Inspect, export, and analyze `.fig` files from the terminal:
```sh
openpencil tree design.fig # Node tree
openpencil find design.fig --type TEXT # Search
openpencil export design.fig -f png # Render
openpencil analyze colors design.fig # Color audit
openpencil analyze clusters design.fig # Repeated patterns
openpencil eval design.fig -c "..." # Figma Plugin API
```
When the desktop app is running, omit the file to control the live editor via RPC:
```sh
openpencil tree # Live document
openpencil export -f png # Screenshot canvas
```
All commands support `--json`. Install: `npm install -g @open-pencil/cli` (or `bun add -g @open-pencil/cli`).
## Real-Time Collaboration
P2P via WebRTC — no server required. Share a link and edit together.
* Live cursors with colored arrows and name pills
* Presence avatars
* Follow mode — click a peer to follow their viewport
* Local persistence via IndexedDB
* Secure room IDs via `crypto.getRandomValues()`
## Desktop & Web
**Desktop** — Tauri v2, ~7 MB. macOS (signed & notarized), Windows, Linux. Native menus, offline, autosave.
**Web** — runs at [app.openpencil.dev](https://app.openpencil.dev), installable as a PWA on mobile with touch-optimized UI.
**Homebrew:**
```sh
brew install open-pencil/tap/open-pencil
```
## Google Fonts Fallback
When a font isn't available locally, OpenPencil fetches it from Google Fonts automatically. No manual installation needed when opening .fig files with unfamiliar fonts.
---
---
url: 'https://openpencil.dev/guide/architecture.md'
---
# Architecture
## System Overview
```mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
UI["Vue 3 UI Toolbar · Panels · Properties Layers · Color Picker"]
Skia["Skia CanvasKit (WASM, 7MB) Vector rendering · Text shaping Effects · Export"]
subgraph Core["Core Engine (TS)"]
SG[SceneGraph] --- Layout[Layout - Yoga]
SG --- Selection
Undo[Undo/Redo] --- Constraints
Constraints --- HitTest[Hit Testing]
end
subgraph FileFormat["File Format Layer"]
FigIO[".fig import/export"] --- Kiwi[Kiwi codec]
Kiwi --- SVG[SVG export]
end
end
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
## Editor Layout
The UI follows Figma's UI3 layout — toolbar at the bottom, navigation on the left, properties on the right:
* **Navigation panel (left)** — Layers tree, pages panel
* **Canvas (center)** — Infinite canvas with CanvasKit rendering, zoom/pan
* **Properties panel (right)** — Context-sensitive sections: Appearance, Fill, Stroke, Typography, Layout, Position
* **Toolbar (bottom)** — Tool selection: Select, Frame, Section, Rectangle, Ellipse, Line, Text, Pen, Hand
## Components
### Rendering (CanvasKit WASM)
The same rendering engine as Figma. CanvasKit provides GPU-accelerated 2D drawing with vector shapes, text shaping via Paragraph API, effects (shadows, blurs, blend modes), and export (PNG, SVG). The 7MB WASM binary loads at startup and creates a GPU surface on the HTML canvas.
The renderer is split into focused modules in `packages/core/src/renderer/`: scene traversal, overlays, fills, strokes, shapes, effects, rulers, labels, and remote cursors.
### Scene Graph
Flat `Map` keyed by GUID strings. Tree structure via `parentIndex` references. Provides O(1) lookup, efficient traversal, hit testing, and rectangular area queries for marquee selection.
The graph emits typed events via nanoevents: `node:created`, `node:updated`, `node:deleted`, `node:reparented`, `node:reordered`. Subsystems subscribe to these instead of manual call-site wiring — the editor uses them for render invalidation and microtask-batched component instance sync, while the collab system uses them for Yjs propagation.
See [Scene Graph Reference](/reference/scene-graph) for internals.
### Layout Engine (Yoga WASM)
Meta's Yoga provides CSS flexbox and grid layout computation via a [fork](https://github.com/open-pencil/yoga/tree/grid) with CSS Grid support. A thin adapter maps Figma property names to Yoga equivalents:
| Figma Property | Yoga Equivalent |
|---|---|
| `stackMode: HORIZONTAL` | `flexDirection: row` |
| `stackMode: VERTICAL` | `flexDirection: column` |
| `stackSpacing` | `gap` |
| `stackPadding` | `padding` |
| `stackJustify` | `justifyContent` |
| `stackChildPrimaryGrow` | `flexGrow` |
### File Format (Kiwi Binary)
Reuses Figma's Kiwi binary codec with 194 message/enum/struct definitions. Import: parse header → Zstd decompress → Kiwi decode → `NodeChange`\[] → scene graph. Export reverses the process with thumbnail generation.
See [File Format Reference](/reference/file-format) for details.
### AI & Tools
Tools are defined once in `packages/core/src/tools/`, split by domain: read, create, modify, structure, variables, vector, analyze. Each tool has typed params and an `execute(figma, args)` function. Adapters convert them for:
* **AI chat** — valibot schemas, multi-provider (Anthropic, OpenAI, Google AI, OpenRouter, compatible endpoints)
* **MCP server** — zod schemas, stdio + HTTP transports
* **CLI** — available via the `eval` command
90+ core tools + 3 MCP file management tools. Includes XPath query (`query_nodes`), JSX inspection (`get_jsx`, `diff_jsx`), semantic description (`describe`), and vision-based verification (`export_image` returns images to the model).
### Undo/Redo
Inverse-command pattern. Before applying any change, affected fields are snapshotted. The snapshot becomes the inverse operation. Batching groups rapid changes (like drag) into single undo entries.
### Clipboard
Figma-compatible bidirectional clipboard. Encodes/decodes Kiwi binary (same format as .fig files) via native browser copy/paste events. Handles vector path scaling, instance children, component set detection, and override application.
### P2P Collaboration
Real-time peer-to-peer collaboration via Trystero (WebRTC) + Yjs CRDT. No server relay — signaling over MQTT public brokers, STUN/TURN for NAT traversal. Awareness protocol provides live cursors, selections, and presence. Local persistence via y-indexeddb.
### CLI-to-App RPC Bridge
When the desktop app is running, CLI commands connect to it via WebSocket instead of requiring a .fig file. The automation server runs on `127.0.0.1:7600` (HTTP) and `127.0.0.1:7601` (WebSocket). Commands execute against the live editor state, enabling automation scripts and AI agents to interact with the running app.
## What's Next
### Full figma-use Tool Set
The MCP server currently exposes 90 tools. The reference implementation in [figma-use](https://github.com/dannote/figma-use) has 118. The remaining tools cover advanced layout constraints, prototype connections, advanced component property editing, and bulk document operations.
### CI Design Tooling
The headless CLI already supports `analyze colors/typography/spacing/clusters`. Next: GitHub Actions integration for automated design linting and visual regression in PRs.
### Prototyping
Frame-to-frame transitions, interaction triggers (click, hover, drag), overlay management, and fullscreen preview mode.
### Windows Code Signing
macOS binaries are signed and notarized since v0.6.0. Windows Authenticode signing via Azure Code Signing is planned to remove the SmartScreen warning.
---
---
url: 'https://openpencil.dev/guide/tech-stack.md'
---
# Tech Stack
## Core Technologies
| Layer | Technology | Why |
|-------|-----------|-----|
| **Rendering** | Skia CanvasKit WASM | Same engine as Figma — proven performance, GPU-accelerated, pixel-perfect |
| **UI Framework** | Vue 3 + VueUse | Reactive composition API, excellent TypeScript support |
| **Components** | Reka UI | Headless, accessible UI primitives (tree, slider, etc.) |
| **Styling** | Tailwind CSS 4 | Utility-first, fast iteration, dark theme |
| **Layout** | Yoga WASM | CSS flexbox and grid engine from Meta, battle-tested in React Native |
| **File Format** | Kiwi binary + Zstd | Figma's own format — compact, fast parsing, .fig compatible |
| **Collaboration** | Trystero + Yjs | P2P WebRTC via MQTT signaling, CRDT sync, y-indexeddb persistence |
| **Color** | culori | Color space conversions (HSV, RGB, hex) |
| **AI/MCP** | MCP SDK + Hono | 90+ tools for AI coding tools, stdio + HTTP transports |
| **JSX Transform** | Sucrase | Lightweight (201 KB) JSX → JS, synchronous, browser-compatible |
| **Events** | nanoevents | 108 bytes, typed event emitter for SceneGraph mutations |
| **Desktop** | Tauri v2 | ~5MB native app (vs Electron's ~100MB), Rust backend |
| **Build** | Vite 7 | Fast HMR, native ES modules |
| **Testing** | Playwright + bun:test | Visual regression (E2E) + fast unit tests |
| **Linting** | oxlint | Rust-based, orders of magnitude faster than ESLint |
| **Formatting** | oxfmt | Rust-based formatter |
| **Type Checking** | typescript-go (tsgo) | Native Go implementation of TypeScript type checker |
## Key Dependencies
```json
{
"canvaskit-wasm": "^0.40.0",
"vue": "^3.5.29",
"yoga-layout": "npm:@open-pencil/yoga-layout@3.3.0-grid.2",
"nanoevents": "^9.1.0",
"sucrase": "^3.35.1",
"reka-ui": "^2.8.2",
"tailwindcss": "^4.2.1",
"culori": "^4.0.2",
"fzstd": "^0.1.1",
"fflate": "^0.8.2",
"trystero": "^0.20.0",
"yjs": "^13.6.24",
"y-indexeddb": "^9.0.12"
}
```
## Why Not...
### Why not SVG rendering?
SVG is slow for complex documents. Every node is a DOM element — 10,000 nodes means 10,000 DOM nodes with layout, paint, and compositing overhead. CanvasKit draws everything to a single GPU surface. (Penpot still defaults to SVG rendering but has an opt-in Rust/Skia WASM renderer in development.)
### Why not Electron (like Figma desktop)?
Tauri v2 uses the system webview (~5MB) instead of bundling Chromium (~100MB). The Rust backend provides native performance for file I/O and system integration.
### Why not React (like the original plan)?
The project migrated from React to Vue 3 early in development. Vue's reactivity system and VueUse composables proved more ergonomic for the editor's state management needs.
### Why not custom layout engine?
Yoga is maintained by Meta, battle-tested across billions of React Native devices, and implements the CSS flexbox spec. Building a custom engine would be months of work to reach the same correctness level.
## Additional Technologies
| Technology | Purpose | Status |
|-----------|---------|--------|
| CSS Grid in Yoga | Grid-based auto layout | Shipped via [Yoga fork](https://github.com/open-pencil/yoga/tree/grid) (`@open-pencil/yoga-layout`) |
---
---
url: 'https://openpencil.dev/guide/comparison.md'
---
# Open Pencil vs Penpot: Architecture & Performance Comparison
Why compare? OpenPencil exists because closed design platforms control what's possible. Understanding architectural differences shows what an open, local-first alternative can do differently.
::: info Penpot's WASM renderer
Penpot 2.x includes a Rust/Skia WASM renderer (`render-wasm/v1`) that can be enabled via server flags or the `?wasm=true` URL parameter. The old SVG renderer remains the default. This page covers both.
:::
## 1. Scale & Codebase Size
| Metric | Open Pencil | Penpot |
|--------|-------------|--------|
| Total LOC | **~26,000** | **~299,000** |
| Source files | ~143 | ~2,900 |
| Languages | TypeScript, Vue | Clojure, ClojureScript, Rust, JS, SQL, SCSS |
| Rendering engine | ~3,200 LOC (TS, 10 files) | 22,000 LOC (Rust/Skia WASM) |
| UI code | ~4,500 LOC | ~175,000 LOC (CLJS + SCSS) |
| Backend | None (local-first) | 32,600 LOC + 151 SQL files |
| LOC ratio | **1×** | **~11×** |
Open Pencil is **~11× smaller** — and that's the whole point. It's not a simplification; it's a fundamentally different architecture.
## 2. Architecture
### Open Pencil: Monolithic Client
```
┌─────────────────────────────────┐
│ Tauri (native shell) │
│ ┌───────────────────────────┐ │
│ │ Vue 3 + TypeScript │ │
│ │ ┌─────────┐ ┌──────────┐│ │
│ │ │ Editor │ │ Kiwi ││ │
│ │ │ Store │ │ Codec ││ │
│ │ └────┬─────┘ └──────────┘│ │
│ │ │ │ │
│ │ ┌────▼────────────────┐ │ │
│ │ │ Scene Graph (TS) │ │ │
│ │ │ Map │ │ │
│ │ └────┬────────────────┘ │ │
│ │ │ │ │
│ │ ┌────▼────┐ ┌──────────┐│ │
│ │ │ Skia │ │ Yoga ││ │
│ │ │CanvasKit│ │ Layout ││ │
│ │ │ (WASM) │ │ (WASM) ││ │
│ │ └─────────┘ └──────────┘│ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
```
**Everything in one process.** No server, no database, no Docker. The scene graph is a flat `Map` in TypeScript. Rendering calls Skia CanvasKit directly from TS. Layout is Yoga WASM called synchronously.
### Penpot: Distributed Client-Server
```
┌───────────────────────────────────────────────────────┐
│ Docker Compose │
│ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Frontend │ │ Backend │ │ Exporter │ │
│ │ ClojureScript│ │ Clojure │ │ (Chromium) │ │
│ │ shadow-cljs │ │ JVM │ │ │ │
│ │ ┌─────────┐ │ │ ┌────────┐ │ └──────────────┘ │
│ │ │render- │ │ │ │Postgres│ │ │
│ │ │wasm │ │ │ │Valkey │ │ ┌──────────────┐ │
│ │ │(Rust→ │ │ │ │ MinIO │ │ │ MCP │ │
│ │ │ Skia │ │ │ └────────┘ │ │ Server │ │
│ │ │ WASM) │ │ │ │ └──────────────┘ │
│ │ └─────────┘ │ │ │ │
│ └──────────────┘ └─────────────┘ │
└───────────────────────────────────────────────────────┘
```
**5+ services minimum.** PostgreSQL for persistence, Redis (Valkey) for pub/sub and caching, MinIO for asset storage, a JVM backend, a Node.js exporter (headless Chromium for server-side rendering), plus the ClojureScript frontend. Dev setup requires Docker Compose with custom networking.
### Verdict: Architecture
Open Pencil's single-process architecture eliminates:
* Network latency between frontend and backend
* Serialization/deserialization overhead at service boundaries
* Container orchestration complexity
* Database query overhead for every operation
Penpot's architecture is optimized for **multi-user server-hosted deployments**. Open Pencil is optimized for **instant local performance**.
## 3. Rendering Pipeline
### Open Pencil: TS → CanvasKit WASM (direct)
```typescript
// renderer.ts — direct CanvasKit calls from TypeScript
renderSceneToCanvas(canvas, graph, pageId) {
// Iterate nodes, build Skia paths/paints, draw
this.fillPaint.setColor(...)
canvas.drawRRect(rrect, this.fillPaint)
}
```
* **1 boundary crossing:** TS → WASM (CanvasKit)
* Scene graph lives in JS heap — no serialization to render
* \~3,200 LOC renderer (split into 10 focused files: scene, overlays, fills, strokes, shapes, effects, rulers, labels)
### Penpot: JS (compiled from CLJS) → Rust WASM → Skia
Penpot 2.x includes a Rust/Skia WASM renderer (`render-wasm/v1`), opt-in via server flags or `?wasm=true`. When enabled, shapes are rendered through:
```
ClojureScript (compiled to JS)
→ decompose to primitives + binary-pack into WASM linear memory
→ Rust WASM (via Emscripten C FFI)
→ skia-safe (Rust Skia bindings)
→ Skia (WebGL)
```
When disabled (default), shapes render as an SVG DOM tree via React/Reagent — each shape is a DOM element.
* **1 boundary crossing** (JS → WASM), same as Open Pencil — but with explicit serialization overhead: UUIDs split to 4×u32, transforms to 6×f32, fills/strokes binary-packed, base props batched into a 104-byte struct per shape
* Tile-based rendering system with interest areas
* 11 separate render surfaces (fills, strokes, shadows, etc.)
* Global mutable state via `unsafe { STATE.as_mut() }` pattern
* 22,000 LOC Rust render engine
Penpot's tile system (`TileViewbox`, `TileTextureCache`, `TILE_SIZE_MULTIPLIER`) pre-renders tiles around the viewport and caches textures (up to 1024 entries).
Open Pencil re-renders the full viewport every frame because CanvasKit called directly from TS is fast enough to not need tiling.
### Verdict: Rendering
| Aspect | Open Pencil | Penpot |
|--------|-------------|--------|
| JS→WASM boundary | Direct (TS objects) | Binary-packed (104-byte base props struct) |
| Rendering model | Immediate/full redraw | Tile-cached |
| Surface management | 1 surface | 11 surfaces |
| Memory overhead | Low (no tile cache) | High (1024 tile cache) |
| Code complexity | ~3,200 LOC (10 files) | 22,000 LOC |
| Unsafe code | None | `unsafe` global state |
When Penpot's WASM renderer is enabled, both projects use Skia via JS→WASM. Open Pencil calls CanvasKit directly with TS objects. Penpot decomposes ClojureScript data into binary-packed structs, writes them to WASM linear memory, and renders through a 22,000 LOC Rust engine. When WASM is disabled (default), Penpot renders shapes as an SVG DOM tree. For small-to-medium documents, the direct CanvasKit path is faster. Penpot's tile system may win on extremely large canvases (100K+ shapes) where only a small viewport is visible — but the overhead is significant.
## 4. Scene Graph & Data Model
### Open Pencil
```typescript
// Flat map, O(1) lookup
nodes: Map
// 29 node types from Figma's Kiwi schema
// ~390 fields per NodeChange (Figma-compatible)
```
* TypeScript interfaces with strict types
* GUIDs match Figma's `sessionID:localID` format
* Direct property access — no indirection layers
### Penpot
```clojure
;; 20+ type definition files in common/src/app/common/types/
;; shapes_builder.cljc, shapes_helpers.cljc
;; Separate type systems for: color, component, container, fills,
;; grid, modifiers, objects_map, page, path, etc.
```
* Data spread across `common/` (49,600 LOC of .cljc)
* Separate geometry modules for flex layout (~6 files), grid layout (~5 files), constraints, bounds, corners, effects
* Runtime schema validation (Malli)
* Data must cross CLJS→Rust boundary for rendering
### Verdict: Data Model
Open Pencil reuses Figma's proven schema (194 Kiwi definitions) directly in TypeScript — zero translation. Penpot maintains its own type system across Clojure/ClojureScript/Rust, requiring manual sync between all three.
## 5. Layout Engine
### Open Pencil: Yoga WASM (314 LOC)
```typescript
import Yoga from 'yoga-layout'
// Direct mapping: Figma stack* fields → Yoga flex properties
const root = Yoga.Node.create()
root.setFlexDirection(FlexDirection.Row)
root.calculateLayout()
applyYogaLayout(graph, frame, yogaRoot)
```
314 lines total. Synchronous, in-process.
### Penpot: Dual Implementation
1. **ClojureScript** (common): `flex_layout/` (6 files), `grid_layout/` (5+ files) — custom implementations
2. **Rust WASM**: `flex_layout.rs` (741 LOC), `grid_layout.rs` (843 LOC) — reimplemented from scratch
Penpot maintains **two independent layout engines** (CLJS and Rust) that must produce identical results.
### Verdict: Layout
Open Pencil delegates to a battle-tested library (Yoga, used by React Native on billions of devices) in 314 lines. Penpot maintains ~3,000+ LOC of custom layout code duplicated across two languages.
## 6. File Format & Figma Compatibility
### Open Pencil
* **Native Kiwi binary format** — same serialization as Figma uses internally
* Direct `.fig` file import via extracted Kiwi codec (2,178 LOC schema + 551 LOC codec)
* Figma clipboard paste support (reads Figma's Kiwi binary from the clipboard)
* Wire-compatible with Figma's multiplayer protocol
### Penpot
* **ZIP archive** (`.penpot` files) containing JSON manifests, per-file JSON data, binary assets, and thumbnails (v3 format)
* SVG used for default rendering and export (opt-in WASM renderer available)
* No native `.fig` import
* Three format versions (v1 legacy Transit, v2, v3 JSON-in-ZIP) with migration system
### Verdict: File Format
Open Pencil has a significant advantage — it can read Figma files natively and even paste Figma clipboard data. Penpot requires manual export/import and cannot open `.fig` files.
## 7. State Management & Undo
### Open Pencil
```typescript
// 110 LOC — inverse command pattern
class UndoManager {
apply(entry: UndoEntry) { entry.forward(); this.undoStack.push(entry) }
undo() { entry.inverse(); this.redoStack.push(entry) }
}
```
110 lines. Forward/inverse closures that capture minimal state. Batch support for multi-step operations.
### Penpot
State management uses Potok (a Redux-like library for ClojureScript atoms). Events implement `UpdateEvent` (pure state→state) or `WatchEvent` (side effects via RxJS). Undo stores inverse change vectors (max 50 entries), with transactions to group rapid changes and auto-expiry after 20 seconds.
### Verdict: State
Open Pencil's approach is simpler and lower overhead. Penpot's approach is more suitable for collaboration (changes are serializable), but at the cost of complexity.
## 8. Developer Experience
| Metric | Open Pencil | Penpot |
|--------|-------------|--------|
| Dev setup | `bun install && bun dev` | Docker Compose + JVM + Node + Rust toolchain |
| Hot reload | Vite HMR (~50ms) | shadow-cljs (seconds) |
| Type checking | TypeScript (strict) | Runtime (Malli schemas) |
| Build time | <5s (Vite) | Minutes (JVM startup + CLJS compile + Rust WASM) |
| First contribution barrier | Low (TS/Vue) | High (Clojure + Rust + Docker) |
| Desktop | Tauri v2 (~5MB) | N/A (browser-only) |
| Hiring pool | Massive (TS/Vue devs) | Tiny (ClojureScript + Rust) |
## 9. Performance Characteristics
| Scenario | Open Pencil | Penpot |
|----------|-------------|--------|
| Cold start | <2s (WASM load) | 10s+ (server + client + WASM) |
| Operation latency | <1ms (in-process) | 10-50ms (network round-trip) |
| Render frame | Direct Skia call | CLJS→JS→WASM FFI→Skia |
| Memory baseline | ~50MB (browser tab) | ~300MB+ (JVM + Postgres + Valkey + browser) |
| Offline capability | Full (local-first) | None (server-dependent) |
| 10K shapes render | One pass, no caching | Tile-based with 11 surfaces |
## 10. What Penpot Does Better
1. **Server-side collaboration** — centralized multi-user editing with WebSockets, user accounts, and access control (Open Pencil uses P2P via Trystero + Yjs — no server, but also no access control or persistence beyond the session)
2. **PDF export** — headless Chromium export service for PDF rendering (OpenPencil exports SVG but not PDF yet)
3. **Plugin system** — full plugin API with sandboxed execution
4. **Design tokens** — native design token support
5. **CSS Grid layout** — custom implementation (Open Pencil uses Yoga fork with grid support)
6. **Self-hosting** — Docker-based deployment for teams
7. **Maturity** — years of production usage, battle-tested at scale
## 11. Scripting & Extensibility
OpenPencil ships with an [`eval` command](/programmable/cli/scripting) that provides a Figma-compatible Plugin API for headless scripting — batch operations, automated testing, and AI-driven modifications all run without the GUI. On top of that, **90 AI tools** are available via built-in chat, MCP server (stdio + HTTP), and the CLI — covering read, create, modify, structure, variables, vector path, analyze (color/typography/spacing/clusters), diff, boolean operations, and arrangement. Penpot has a plugin system with sandboxed execution but no headless scripting API or MCP integration.
## Summary
| Dimension | Winner | Why |
|-----------|--------|-----|
| **Architecture simplicity** | Open Pencil | Single process vs 5+ services |
| **Rendering performance** | Open Pencil | Direct CanvasKit vs SVG DOM (default) or binary-packed WASM |
| **Code maintainability** | Open Pencil | ~26K LOC in 1 language vs 299K in 4+ languages |
| **Figma compatibility** | Open Pencil | Native Kiwi codec vs no .fig support |
| **Developer onboarding** | Open Pencil | TS/Vue vs Clojure/Rust/Docker |
| **Desktop experience** | Open Pencil | Tauri native vs browser-only |
| **Layout engine** | Open Pencil | Yoga (proven) vs custom dual implementation |
| **Collaboration** | Tie | Penpot: server-based with access control; Open Pencil: P2P via Trystero + Yjs, zero hosting |
| **Self-hosting** | Penpot | Docker-ready vs desktop-only |
| **Ecosystem maturity** | Penpot | Years of production vs early stage |
Open Pencil is architecturally leaner — a single-process CanvasKit renderer in ~26K LOC of TypeScript, Figma-compatible by design. Penpot is a full-stack platform with ~299K LOC across Clojure, ClojureScript, Rust, and SCSS, plus a Docker service fleet. Both now offer real-time collaboration (different architectures: P2P vs server). Penpot has a plugin ecosystem and server-side PDF export; Open Pencil has Figma-compatible headless scripting, **90 AI/MCP tools**, SVG export, and a native desktop app.
---
---
url: 'https://openpencil.dev/guide/figma-comparison.md'
---
# Figma Feature Matrix
Feature-by-feature comparison of Figma Design capabilities with Open Pencil's current implementation status.
::: tip Status Legend
✅ Supported — feature works end-to-end · 🟡 Partial — core behavior exists, some sub-features missing · 🔲 Not yet implemented
:::
**Coverage:** 94 of 158 Figma feature items addressed — 76 ✅ fully supported, 18 🟡 partial, 64 🔲 not yet. Last updated: 2026-03-07.
## Interface & Navigation
| Feature | Status | Notes |
|---------|--------|-------|
| Toolbar with design tools | ✅ | Bottom toolbar (UI3 style): Select, Frame, Section, Rectangle, Ellipse, Line, Text, Hand, Pen |
| Layers panel (left sidebar) | ✅ | Tree view with expand/collapse, drag reorder, visibility toggle; resizable width |
| Pages panel | ✅ | Add, delete, rename pages; per-page viewport state |
| Properties panel (right sidebar) | ✅ | Sections: Appearance, Fill, Stroke, Effects, Typography, Layout, Position; resizable width |
| Zoom & pan | ✅ | Ctrl + scroll, pinch, ⌘+ / ⌘− / ⌘0 (100%) / ⌘1 (fit) / ⌘2 (selection), Space + drag, middle mouse, hand tool (H) |
| Canvas rulers | ✅ | Top/left rulers with selection highlight bands and coordinate badges |
| Canvas background color | ✅ | Per-page background via properties panel |
| Canvas guides | 🔲 | Figma supports draggable guides from rulers |
| Actions menu / command palette | 🔲 | Figma's quick actions search |
| Context menu | ✅ | Right-click with clipboard, z-order, grouping, component, visibility, lock, move-to-page actions |
| Keyboard shortcuts | 🟡 | Core shortcuts + components + z-order + visibility/lock implemented; Scale, Arrow, Pencil, flip, text formatting not yet wired |
| Find and replace | 🔲 | Text search/replace across document |
| Layer outlines view | 🔲 | Wireframe view of all layers |
| Custom file thumbnails | 🔲 | Thumbnail generated on export, but no custom thumbnail picker |
| Nudge value settings | 🔲 | Default 1px/10px; Figma allows custom small/big nudge values |
| App menu (browser mode) | ✅ | File, Edit, View, Object, Text, Arrange menus; Tauri uses native menus |
| AI tools | 🟡 | 90 tools via OpenRouter + MCP server; no AI-generated images or AI-powered search yet |
## Layers & Shapes
| Feature | Status | Notes |
|---------|--------|-------|
| Shape tools (Rectangle, Ellipse, Line, Polygon, Star) | ✅ | All basic shape types; polygon side count and star inner radius configurable |
| Frames | ✅ | Clip content, independent coordinate system |
| Groups | ✅ | ⌘G to group, ⇧⌘G to ungroup |
| Sections | ✅ | Title pills, auto-adopt overlapping nodes, luminance-adaptive text |
| Arc tool (arcs, semi-circles, rings) | ✅ | arcData with start/end angle and inner radius |
| Pencil (freehand) tool | 🔲 | Figma's freehand drawing tool |
| Masks | 🔲 | Shape masks for clipping layers |
| Layer types & hierarchy | ✅ | 17 node types, flat Map + parent-child tree |
| Select layers | ✅ | Click, shift-click, marquee selection |
| Alignment & position | ✅ | Position, rotation, dimensions in properties panel |
| Copy & paste objects | ✅ | Standard clipboard + Figma Kiwi binary format; Copy as text/SVG/PNG/JSX |
| Scale layers proportionally | 🟡 | Shift-resize constrains proportions; no dedicated Scale tool (K) |
| Lock & unlock layers | ✅ | ⇧⌘L toggles lock; locked nodes can't be selected/moved from canvas |
| Toggle layer visibility | ✅ | Eye icon in layers panel + ⇧⌘H keyboard shortcut |
| Rename layers | ✅ | Double-click inline rename in layers panel; Enter/Escape/blur to commit |
| Bring to front / Send to back | ✅ | ] and \[ keyboard shortcuts; also in context menu |
| Move to page | ✅ | Move selected nodes between pages via context menu |
| Constraints (responsive resize) | 🔲 | Pin edges/center for parent resize behavior |
| Smart selection (distribute/align) | 🔲 | Evenly space and align multi-selection |
| Layout guides (columns, rows, grid) | 🔲 | Column/row/grid overlay guides on frames |
| Measure distances between layers | 🔲 | Alt-hover to show distances |
| Edit objects in bulk | ✅ | Multi-selection properties panel: edit position, size, appearance, fill, stroke, effects across multiple nodes; shared values display normally, differing values show "Mixed" |
| Identify matching objects | 🔲 | Find similar layers |
| Copy/paste properties | 🔲 | Copy fill/stroke/effects between layers |
| Parent-child relationships | ✅ | Full hierarchy with parentIndex, reparenting via drag |
## Vector Tools
| Feature | Status | Notes |
|---------|--------|-------|
| Vector networks | ✅ | Figma-compatible model, not simple paths |
| Pen tool | ✅ | Corner points, bezier curves, open/closed paths |
| Edit vector layers | 🟡 | Creation works; advanced vertex editing (bend, delete points, join) limited |
| Boolean operations (Union, Subtract, Intersect, Exclude) | 🔲 | Combine shapes with boolean ops |
| Flatten layers | 🔲 | Merge vector paths into single path |
| Convert strokes to paths | 🔲 | Outline Stroke command |
| Convert text to paths | 🔲 | Flatten text to vector outlines |
| Shape builder tool | 🔲 | Interactive boolean tool |
| Offset path | 🔲 | Inset/outset a vector path |
| Simplify path | 🔲 | Reduce vector point count |
## Text & Typography
| Feature | Status | Notes |
|---------|--------|-------|
| Text tool & inline editing | ✅ | Canvas-native editing, phantom textarea, cursor/selection/word select, drag to select, double/triple-click, rich text style runs (⌘B / I / U, **S** button) |
| Text rendering (Paragraph API) | ✅ | CanvasKit Paragraph for shaping, line-breaking, metrics |
| Font loading (system fonts) | ✅ | Inter default, font-kit in Tauri with OnceLock cache + preloading, queryLocalFonts in browser |
| Font family & weight | ✅ | FontPicker with virtual scroll, search, CSS preview; weight selection in properties panel |
| Font size & line height | ✅ | Editable in typography section |
| Text alignment | 🟡 | Basic alignment; Figma has vertical alignment and auto-width/height modes |
| Text styles | 🟡 | Per-selection bold/italic/underline/strikethrough (⌘B / I / U, **S** button); not yet reusable named text style presets |
| Text resizing modes (auto, fixed, hug) | 🔲 | Figma's auto-width, auto-height, fixed-size text modes |
| Bulleted & numbered lists | 🔲 | List formatting in text |
| Links in text | 🔲 | Hyperlinks within text content |
| Emojis & smart symbols | 🔲 | Emoji rendering and special characters |
| OpenType features | 🔲 | Ligatures, stylistic alternates, tabular figures |
| Variable fonts | 🔲 | Adjustable font axes (weight, width, slant) |
| CJK text support | 🔲 | Chinese, Japanese, Korean text rendering |
| RTL text support | 🔲 | Right-to-left text layout |
| Icon fonts | 🔲 | Special handling for icon font glyphs |
## Color, Gradients & Images
| Feature | Status | Notes |
|---------|--------|-------|
| Color picker (HSV) | ✅ | HSV square, hue slider, alpha slider, hex input |
| Solid fills | ✅ | Hex color with opacity |
| Linear gradient | ✅ | Gradient stops, transform handles |
| Radial gradient | ✅ | Rendered via CanvasKit shaders |
| Angular gradient | ✅ | Sweep/conic gradient support |
| Diamond gradient | ✅ | Four-point diamond gradient |
| Image fills | ✅ | Decoded from blob data with scale modes (fill, fit, crop, tile) |
| Pattern fills | 🔲 | Repeating image/pattern fills |
| Blend modes | 🔲 | Layer and fill blend modes (multiply, screen, overlay, etc.) |
| Add images & videos | 🟡 | Image fills rendered; no drag-and-drop image import or video support |
| Image property adjustment | 🔲 | Exposure, contrast, saturation, etc. |
| Crop an image | 🔲 | Interactive image cropping |
| Eyedropper tool | 🔲 | Sample colors from canvas |
| Mixed selection color editing | 🔲 | Adjust colors across heterogeneous selection |
| Color models (RGB, HSL, HSB, Hex) | 🟡 | HSV + Hex in picker; no HSL or RGB mode toggle |
## Effects & Properties
| Feature | Status | Notes |
|---------|--------|-------|
| Drop shadow | ✅ | Offset, blur radius, color via CanvasKit filters |
| Inner shadow | ✅ | Inset shadow effect |
| Layer blur | ✅ | Gaussian blur on layer |
| Background blur | ✅ | Blur content behind layer |
| Foreground blur | ✅ | Blur in foreground |
| Stroke weight | ✅ | Configurable in properties panel |
| Stroke cap (round, square, arrow) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Stroke join (miter, bevel, round) | ✅ | All three join types |
| Dash patterns | ✅ | Dash-on/dash-off stroke pattern |
| Stroke alignment | ✅ | Inside/Center/Outside with clip-based rendering matching Figma behavior |
| Individual stroke weights per side | ✅ | Top/Right/Bottom/Left with side selector dropdown |
| Corner radius | ✅ | Uniform and per-corner radius with independent toggle in properties panel |
| Corner smoothing (iOS-style) | 🔲 | Figma's continuous corner rounding |
| Multiple fills/strokes per layer | 🔲 | Figma allows stacking fills and strokes |
## Auto Layout
| Feature | Status | Notes |
|---------|--------|-------|
| Horizontal & vertical flow | ✅ | Yoga WASM flexbox engine |
| Toggle auto layout (⇧A) | ✅ | Toggle on frame or wrap selection |
| Gap (spacing between children) | ✅ | Configurable in properties panel |
| Padding (uniform & per-side) | ✅ | All four sides independently |
| Justify content | ✅ | Start, center, end, space-between |
| Align items | ✅ | Start, center, end, stretch |
| Child sizing (fixed, fill, hug) | ✅ | Per-child sizing modes |
| Wrap | ✅ | Flex wrap for multi-line layout |
| Grid auto layout flow | ✅ | CSS Grid via Yoga fork — column/row tracks, gaps, spans |
| Combined flows (nested) | ✅ | Nested auto-layout frames with different directions |
| Drag reorder within auto layout | ✅ | Visual insertion indicator |
| Min/max width and height | 🔲 | Figma supports min/max constraints on auto-layout children |
## Components & Design Systems
| Feature | Status | Notes |
|---------|--------|-------|
| Create components | 🟡 | ⌥⌘K creates from frame/group or wraps selection; no component properties UI yet |
| Component sets | 🟡 | ⇧⌘K combines components; dashed purple border; no variant property editing |
| Component instances | 🟡 | Create instance from context menu with child cloning and componentId mapping; live sync from component; no override editing UI |
| Variants | 🔲 | Variant switching and property-based selection |
| Component properties | 🔲 | Boolean, text, instance swap properties |
| Override propagation | ✅ | Changes to main component propagate to all instances; overrides preserved |
| Variables (color, number, string, boolean) | 🟡 | `COLOR` full UI (dialog, TanStack Table, inline editing, undo/redo, demo collections); `FLOAT`/STRING/BOOLEAN defined but no editing UI |
| Variable collections & modes | 🟡 | Collections, modes, activeMode switching work; no variable-driven theming UI yet |
| Styles (color, text, effect, layout) | 🔲 | Reusable named style presets |
| Libraries (publish, share, update) | 🔲 | Shared component/style libraries |
| Detach instance | ✅ | ⌥⌘B converts instance back to frame |
| Go to main component | ✅ | Navigate to source component, cross-page |
## Prototyping
| Feature | Status | Notes |
|---------|--------|-------|
| Prototype connections | 🔲 | Not supported yet |
| Triggers (click, hover, drag, etc.) | 🔲 | Not supported yet |
| Actions (navigate, overlay, scroll, etc.) | 🔲 | Not supported yet |
| Animations & transitions | 🔲 | Not supported yet |
| Smart animate | 🔲 | Auto-animate matching layers |
| Overlays | 🔲 | Modal/popover prototyping |
| Scroll & overflow behavior | 🔲 | Scrollable frames in prototypes |
| Prototype flows | 🔲 | Named starting points |
| Variables in prototypes | 🔲 | Conditional logic with variables |
| Easing & spring animations | 🔲 | Custom animation curves |
| Present & play prototypes | 🔲 | Fullscreen prototype viewer |
## Import & Export
| Feature | Status | Notes |
|---------|--------|-------|
| .fig file import | ✅ | Full Kiwi codec: 194 definitions, ~390 fields per `NodeChange` |
| .fig file export | ✅ | Kiwi encoding + Zstd compression + thumbnail generation; `COMPONENT`/COMPONENT\_SET mapped to `SYMBOL` for round-trip |
| Save / Save As | ✅ | ⌘S / ⇧⌘S; native dialogs (Tauri), File System Access API (Chrome/Edge), download fallback (Safari) |
| Figma clipboard (paste) | ✅ | Decode Kiwi binary from Figma clipboard |
| Figma clipboard (copy) | ✅ | Encode Kiwi binary that Figma can read |
| Sketch file import | 🔲 | .sketch file parsing |
| Image/SVG/PDF export | 🟡 | PNG/JPG/WEBP/SVG export ✅; PDF export 🔲 |
| Version history | 🔲 | Browse and restore previous versions |
| Copy assets between tools | ✅ | Figma clipboard (Kiwi binary), Copy as text/SVG/PNG/JSX |
## Plugin API & Scripting
| Feature | Status | Notes |
|---------|--------|-------|
| Eval command with Figma Plugin API | ✅ | Headless JavaScript execution with figma global object matching Figma's plugin surface |
## Collaboration & Dev Mode
| Feature | Status | Notes |
|---------|--------|-------|
| Comments (pin, thread, resolve) | 🔲 | Not supported yet |
| Real-time multiplayer | ✅ | P2P via Trystero + Yjs CRDT, cursors, follow mode; no server required |
| Cursor chat | 🔲 | Inline chat bubbles at cursor |
| Branching & merging | 🔲 | Version branches for design files |
| Dev Mode (inspect) | 🟡 | Code tab shows JSX representation of selection; no CSS properties or handoff specs |
| Code Connect | 🔲 | Link design components to code |
| Code snippets | 🟡 | JSX export with syntax highlighting and copy; no CSS/Swift/Kotlin snippets |
| Tailwind CSS v4 export | ✅ | Export as HTML with Tailwind utility classes from Code panel, CLI, or programmatically |
| Figma for VS Code | 🔲 | Editor plugin integration |
| MCP server | ✅ | @open-pencil/mcp with stdio + HTTP transports; 87 core tools + 3 file management tools = 90 total |
| CLI tools | ✅ | Headless CLI: info, tree, find, export, analyze, node, pages, variables, eval; MCP server with stdio + HTTP |
## Figma Draw
| Feature | Status | Notes |
|---------|--------|-------|
| Illustration tools | 🔲 | Figma Draw's specialized drawing tools |
| Pattern transforms | 🔲 | Create repeating patterns with transforms |
---
---
url: 'https://openpencil.dev/development/contributing.md'
---
# Contributing
## Project Structure
```
packages/
core/ @open-pencil/core — engine (zero DOM deps)
src/ Scene graph, renderer, layout, codec, kiwi, types
cli/ @open-pencil/cli — headless CLI for .fig operations
src/commands/ info, tree, find, export, eval, analyze
mcp/ @open-pencil/mcp — MCP server for AI tools
src/ stdio + HTTP (Hono) transports, 87 tools
src/
components/ Vue SFCs (canvas, panels, toolbar, color picker)
properties/ Property panel sections (Appearance, Fill, Stroke, etc.)
composables/ Canvas input, keyboard shortcuts, rendering hooks
stores/ Editor state (Vue reactivity)
engine/ Re-export shims from @open-pencil/core
kiwi/ Re-export shims from @open-pencil/core
types.ts Shared types (re-exported from core)
constants.ts UI colors, defaults, thresholds
desktop/ Tauri v2 (Rust + config)
tests/
e2e/ Playwright visual regression
engine/ Unit tests (bun:test)
docs/ VitePress documentation site
```
## Development Setup
```sh
bun install
bun run dev # Editor at localhost:1420
bun run docs:dev # Docs at localhost:5173
```
## Code Style
### Tooling
| Tool | Command | Purpose |
|------|---------|---------|
| oxlint | `bun run lint` | Linting (Rust-based, fast) |
| oxfmt | `bun run format` | Code formatting |
| tsgo | `bun run typecheck` | Type checking (Go-based TypeScript checker) |
Run all checks:
```sh
bun run check
```
### Conventions
* **File names** — kebab-case (`scene-graph.ts`, `use-canvas-input.ts`)
* **Components** — PascalCase Vue SFCs (`EditorCanvas.vue`, `ScrubInput.vue`)
* **Constants** — SCREAMING\_SNAKE\_CASE
* **Functions/variables** — camelCase
* **Types/interfaces** — PascalCase
### AI Agent Conventions
Developers and AI agents working on the codebase should read `AGENTS.md` in the repo root ([view on GitHub](https://github.com/open-pencil/open-pencil/blob/master/AGENTS.md)). Covers rendering, scene graph, components & instances, layout, UI, file format, Tauri conventions, and known issues.
## Making Changes
1. Implement the change
2. Run `bun run check` and `bun run test`
3. Submit a pull request
## Key Files
Core engine source lives in `packages/core/src/`. App-specific editor, document, AI, collaboration, shell, demo, and automation code lives under `src/app/*`; the Vue SDK owns reusable canvas/composable code under `packages/vue/src/`.
| File | Purpose |
|------|---------|
| `packages/core/src/scene-graph/` | Scene graph: nodes, variables, instances, hit testing |
| `packages/core/src/canvas/renderer.ts` | CanvasKit rendering pipeline |
| `packages/core/src/layout.ts` | Yoga layout adapter |
| `packages/core/src/scene-graph/undo.ts` | Undo/redo manager |
| `packages/core/src/clipboard.ts` | Figma-compatible clipboard |
| `packages/core/src/vector/` | Vector network model |
| `packages/core/src/io/formats/raster/render.ts` | Offscreen image export (PNG/JPG/WEBP) |
| `packages/core/src/kiwi/binary/codec.ts` | Kiwi binary encoder/decoder |
| `packages/core/src/kiwi/fig-import.ts` | .fig file import logic |
| `packages/cli/src/index.ts` | CLI entry point |
| `packages/core/src/tools/` | Unified tool definitions split by domain (read, create, modify, structure, variables, vector, analyze) |
| `packages/core/src/figma-api/` | Figma Plugin API implementation |
| `packages/mcp/src/server.ts` | MCP server factory |
| `packages/cli/src/commands/` | CLI commands (info, tree, find, export, eval, analyze) |
| `src/app/editor/session/create.ts` | Editor session assembly |
| `packages/vue/src/canvas/CanvasRoot.vue` | Canvas rendering composable |
| `packages/vue/src/canvas/useCanvasInput.ts` | Mouse/touch input handling |
| `src/app/shell/keyboard/use.ts` | Keyboard shortcut handling |
---
---
url: 'https://openpencil.dev/development/testing.md'
---
# Testing
## Overview
| Type | Framework | Command | Location |
| --------------------- | ---------- | -------------------- | --------------- |
| E2E visual regression | Playwright | `bun run test` | `tests/e2e/` |
| Figma CDP reference | Playwright | `bun run test:figma` | `tests/figma/` |
| Unit tests | bun:test | `bun run test:unit` | `tests/engine/` |
## E2E Visual Regression
Playwright creates shapes on the canvas and compares screenshots against baseline PNGs.
```sh
bun run test # Run tests, compare against baselines
bun run test:update # Regenerate baseline screenshots
```
### How It Works
1. Tests load the editor in a headless browser
2. The editor signals readiness via a `data-ready` HTML attribute
3. Tests create shapes via the editor's API
4. Screenshots are taken and compared against baselines using `toMatchSnapshot`
5. Page is reused across test cases for speed (~2s total)
### No-Chrome Test Mode
The editor supports a test mode that hides UI chrome (toolbar, panels) for clean screenshot capture. Activated via URL parameter.
## Figma CDP Reference Tests
A separate Playwright project connects to Figma via Chrome DevTools Protocol to capture reference screenshots for pixel-perfect comparison.
```sh
bun run figma:debug # Launch Figma with debugging port
bun run test:figma # Connect to Figma, capture references
```
Requires Figma desktop app running with `--remote-debugging-port=9222`.
## Unit Tests
Engine unit tests use bun:test and target < 50ms execution:
```sh
bun run test:unit
```
Tests cover:
* Scene graph CRUD operations, parent-child relationships, z-ordering, hit testing
* **Fig-import pipeline** — node type mapping, transforms, fills/strokes/effects, gradients, images, arcs, nested hierarchies (`tests/engine/io/fig/import/legacy/*.test.ts`)
* **Layout computation** — Yoga auto-layout: direction, gap, padding, justify, align, child sizing (fixed/fill/hug), cross-axis sizing, wrap, nested layouts (`tests/engine/layout/`)
### Writing Unit Tests
```typescript
import { describe, expect, it } from 'bun:test'
import { SceneGraph } from '@open-pencil/core/scene-graph'
describe('SceneGraph', () => {
it('creates and retrieves a node', () => {
const sg = new SceneGraph()
const node = sg.createNode('RECTANGLE', sg.root, { name: 'Test' })
expect(sg.getNode(node.guid)).toBeDefined()
})
})
```
## E2E Test Coverage
| Test file | Scope |
| -------------------------------- | --------------------------------------------------------------- |
| `tests/e2e/layers-panel.spec.ts` | Layers panel tree structure, visibility toggles, selection sync |
| `tests/e2e/visual.spec.ts` | Visual regression screenshots for shapes and rendering |
## Test Helpers
| File | Purpose |
| ------------------------- | -------------------------------------- |
| `tests/helpers/canvas.ts` | Canvas setup and interaction utilities |
| `tests/helpers/figma.ts` | Figma CDP connection helpers |
## Performance Targets
| Metric | Target |
| --------------------- | ----------------------------- |
| E2E suite total | < 3s |
| Unit test suite total | < 50ms |
| Screenshot comparison | toMatchSnapshot (pixel-level) |
---
---
url: 'https://openpencil.dev/development/roadmap.md'
description: OpenPencil product roadmap and Figma compatibility tracking.
---
# Roadmap
OpenPencil is moving toward production-grade Figma compatibility while keeping design documents programmable, local-first, and fast on large files.
## Current focus
* Improve `.fig` import/export fidelity against real Figma files and Figma's own rendering.
* Keep large design systems responsive in the browser and desktop app.
* Treat the scene graph as a programmable design document: every important read, write, export, diff, and validation operation should be reachable through UI, CLI, MCP, and SDK surfaces.
* Keep files on the user's machine unless collaboration is explicitly enabled.
## Near-term work
### Figma fidelity
* Preserve and round-trip more Figma metadata safely.
* Add visual regression coverage for full multi-page `.fig` documents. `tools/visual-oracles/src/export-fixtures.ts` exports current smoke fixture pages to `/tmp` for manual comparison without committing large images; `tests/fixtures/figma-oracles/visual-comparison-report.json` records the current Figma-vs-OpenPencil oracle diff findings.
* Close high-impact renderer gaps: remaining mask edge cases, blend isolation, pattern fills, and broader variable-font fixtures.
* Improve boolean operation editing/export now that imported Figma `BOOLEAN_OPERATION` nodes remain boolean operations.
### Editor depth
* Complete variable inspector coverage for common numeric/text/layout fields.
* Improve component and instance editing: variant switching, property editing, and override inspection.
* Add first-class layout grid and guide rendering/editing.
* Expand vector editing workflows without regressing imported vector fidelity.
### Agent workflows
* Polish the official `SKILL.md` guidance for OpenPencil so agents use the full inspect → act → render/measure → compare → iterate loop instead of relying on one-shot prompting.
* Publish tested AI workflow recipes for common tasks: create from prompt, edit a selected design, compare against a screenshot or Figma reference, fix visual regressions, extract tokens, and batch-migrate files.
* Make agent workflows measurable by default: every substantial operation should be able to produce a render, structured diff, lint result, or comparison artifact.
* Keep MCP, CLI, and SDK operations aligned so agent skills can run the same workflow in desktop, browser, CI, or headless file mode.
### Tooling and API parity
* Maintain a public tool/API reference that maps editor operations to CLI commands, MCP tools, SDK APIs, and Figma Plugin API-compatible eval usage.
* Add coverage tests that detect when a core editor capability exists in the UI but is missing from CLI/MCP/SDK, or vice versa.
* Keep tool outputs structured enough for agents to chain safely: node IDs, bounds, diffs, render artifacts, diagnostics, and machine-readable error details.
* Improve deterministic CLI/MCP export and comparison tools for CI.
* Add more design linting and migration helpers for batch `.fig` and `.pen` workflows.
* Package desktop-side MCP integration so local agent workflows do not require global installs.
### Performance and scale
* Incremental layout and render invalidation for large documents.
* Better renderer profiling surfaces for slow nodes, effects, masks, and imported files.
* Smarter raster/retained caching that preserves fidelity during zoom and pan.
### Interactive shader layers
* Add Unicorn Studio-style shader scenes as first-class design layers: animated gradients, particles, noise fields, metaballs, lighting, displacement, and pointer-reactive backgrounds.
* Provide a preset-first editor for common generative visuals before exposing raw shader code.
* Support timeline and interaction inputs such as time, pointer position, scroll, layer bounds, colors, variables, and imported image textures.
* Render shader layers through CanvasKit/WebGL while keeping deterministic raster export for PNG/JPG/WEBP and thumbnails.
* Store shader layer configuration in OpenPencil documents and export graceful fallbacks when a target format cannot preserve the live effect.
## Later
### SDK and embedded editor
* Document the Vue SDK and core subpath exports as a platform for custom editor shells, embedded design surfaces, and automation-specific UIs.
* Provide examples for embedding OpenPencil in product tools: read-only previews, editable canvases, design review surfaces, and agent-controlled editors.
* Keep the renderer, editor core, and tool registry framework-agnostic enough for headless and embedded use.
### Product depth
* Prototyping: frame connections, triggers, overlays, transitions, and preview mode.
* Comments: pins, threads, resolution state, and collaboration-aware display.
* Shared libraries: publish, consume, and update components/styles across files.
* Platform polish: Windows code signing, PWA support, packaged updater improvements, and desktop-side MCP bundling.
## Non-goals
* Cloud-first storage or mandatory accounts.
* Read-only automation surfaces that cannot modify documents.
* Feature work that sacrifices `.fig` import/export fidelity for convenience.
This section tracks OpenPencil's current compatibility with Figma Design features. It is based on Figma's public Help Center feature areas and the current OpenPencil scene graph, Kiwi import/export, CanvasKit renderer, UI panels, CLI, and MCP tools.
Legend:
* **✅ Supported** — implemented for common files and expected to work directly.
* **◐ Partial** — implemented for important cases, but missing parity, UI, or edge-case behavior.
* **↩ Round-trip only** — imported/preserved/exported for `.fig` fidelity, but not rendered or editable as a first-class OpenPencil feature.
* **— Not supported** — not currently modeled or intentionally out of scope.
Support tiers used for prioritization:
1. **Visual fidelity** — fields that change pixels in normal design exports. These get real Figma oracle fixtures, renderer tests, and visual metrics first.
2. **Round-trip fidelity** — fields that should survive read → write → Figma import but do not need OpenPencil UI/rendering yet. These need raw-preservation and invalidation tests.
3. **Product/runtime systems** — prototypes, libraries, FigJam, Slides, Dev Mode, CMS/AI, and media timelines. These stay schema-only or raw-preserved until OpenPencil has matching product concepts.
4. **Unsafe/internal metadata** — fields that can corrupt Figma import or overwrite user edits when stale. These are filtered or preserved only with fixture evidence.
## Official Figma feature areas
Figma's design documentation groups features into these areas:
* Layers, frames, groups, sections, shape layers, text, vectors, and boolean operations.
* Fills, gradients, images, patterns, blend modes, strokes, effects, corner radius, and corner smoothing.
* Auto layout: vertical, horizontal, wrap, grid, padding, gap, hug/fill/fixed/min/max, and ignore auto layout.
* Components, instances, variants, component properties, slots, libraries, and library updates.
* Variables: color, number, string, boolean, collections, modes, aliases, scopes, and prototype variables.
* Prototyping: flows, hotspots, triggers, actions, overlays, smart animate, easing, conditionals, expressions, and variable actions.
* Dev Mode: inspect, measurements, annotations, Code Connect, dev resources, ready-for-dev states, and Figma MCP.
* Collaboration/file workflows: comments, version history, thumbnails, branches, library publishing, and multiplayer metadata.
## Figma compatibility matrix
| Area | Import | Render | UI edit | Export round-trip | CLI/MCP | Notes |
| ---------------------------------------------------- | -----: | -----: | ------: | ----------------: | ------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pages / canvases | ✅ | ✅ | ✅ | ✅ | ✅ | Multi-page documents and per-page viewport are supported. |
| Frames | ✅ | ✅ | ✅ | ✅ | ✅ | Includes clipping and auto-layout container behavior. |
| Groups | ✅ | ✅ | ✅ | ✅ | ✅ | Grouping preserves visual positions. |
| Sections | ✅ | ✅ | ✅ | ✅ | ✅ | Section rendering and title pills are OpenPencil-specific approximations. |
| Rectangles / rounded rectangles | ✅ | ✅ | ✅ | ✅ | ✅ | Per-corner radii and smoothed corners render for fills, strokes, clips, masks, and effects. |
| Ellipses / arcs | ✅ | ✅ | ◐ | ✅ | ✅ | `arcData` renders/exports; no full inspector controls. |
| Lines | ✅ | ✅ | ✅ | ✅ | ✅ | Stroke caps/joins render but are not fully exposed in UI. |
| Polygons / stars | ✅ | ✅ | ◐ | ✅ | ✅ | `pointCount` and `starInnerRadius` modeled. |
| Text | ✅ | ✅ | ◐ | ✅ | ✅ | Derived Figma glyphs improve fidelity; advanced typography is partial. |
| Vectors / vector networks | ✅ | ✅ | ◐ | ✅ | ✅ | Vector edit support exists; Figma Draw tools are not fully replicated. |
| Boolean operations | ✅ | ✅ | ◐ | ✅ | ✅ | Figma `BOOLEAN_OPERATION` nodes import/export as boolean operations; inspector editing remains limited. |
| Components | ✅ | ✅ | ◐ | ✅ | ✅ | Component metadata, descriptions, links, and publish fields mostly round-trip. |
| Component sets / variants | ✅ | ✅ | ◐ | ✅ | ✅ | Variant values are usable; full component property authoring is incomplete. |
| Instances / overrides | ✅ | ✅ | ◐ | ✅ | ✅ | Raw symbol overrides and derived symbol data are preserved for fidelity. |
| Slots | ↩ | ◐ | — | ↩ | — | Some component property payloads may survive round-trip, but Figma slots are not a first-class workflow. |
| Connectors | ◐ | ◐ | — | ◐ | ◐ | Type exists, but Figma connector semantics are weak. |
| Shape-with-text / FigJam shapes | ◐ | ◐ | — | ◐ | ◐ | Type exists, but not a full FigJam feature implementation. |
| Slices | ◐ | — | ◐ | ◐ | ✅ | Slice-like export regions exist via tooling, not as true Figma slice nodes. |
| FigJam / Slides / Code / CMS / Buzz node families | ↩ | — | — | ↩ | — | Current Kiwi schema recognizes many newer Figma node families (`TABLE`, `SLIDE`, `CODE_COMPONENT`, `CMS_RICH_TEXT`, `REPEATER`, `WEBPAGE`, etc.), but OpenPencil only preserves/round-trips them where safe; they are not first-class scene nodes. |
| Solid fills | ✅ | ✅ | ✅ | ✅ | ✅ | Color variables supported for common fill cases. |
| Gradients | ✅ | ✅ | ✅ | ✅ | ✅ | Linear/radial/angular/diamond support; Figma edge cases may differ. |
| Image fills | ✅ | ✅ | ◐ | ✅ | ✅ | Fill/fit/crop/tile support exists; imported crop/tile affine transforms are applied, but exact Figma parity is still partial. |
| Pattern / noise / custom fills | ✅ | ◐ | — | ✅ | — | Schema metadata imports/exports; Figma pattern fills with a referenced source node render as repeated source tiles with scale, spacing, alignment, and basic hex offsets. Noise/custom paints still render with a solid fallback pending real paint payload samples; Figma-authored noise/texture/glass effect payloads are captured separately. |
| Video/GIF/media fills | ↩ | — | — | ↩ | — | Kiwi schema includes media paint/export enums, but OpenPencil has no video/GIF playback or media layer support. |
| Layer/fill/effect blend modes | ✅ | ◐ | — | ✅ | ✅ | Canvas applies node, fill, and common shadow effect blend modes; Figma isolation edge cases remain partial. |
| Opacity | ✅ | ✅ | ✅ | ✅ | ✅ | Node opacity uses save layers in the renderer. |
| Strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Weight, alignment, dashes, and side weights are supported. |
| Stroke caps / joins / miter limit | ✅ | ✅ | ◐ | ✅ | ✅ | Renderer/export support exists; inspector controls are limited. |
| Effects: shadows and blurs | ✅ | ✅ | ✅ | ✅ | ✅ | `showShadowBehindNode` is rendered but not exposed in UI. |
| Effect styles | ↩ | — | — | ↩ | — | Style IDs round-trip; no style manager. |
| Corner radius | ✅ | ✅ | ✅ | ✅ | ✅ | Uniform and independent radii supported. |
| Corner smoothing | ✅ | ✅ | — | ✅ | ✅ | Figma-style smoothed corners render for common uniform and independent-radius rectangles; exact parity still needs broader fixture tuning. |
| Masks | ✅ | ◐ | — | ✅ | ✅ | Figma schema `mask`, `maskType`, and `maskIsOutline` fields import and export; common sibling alpha/vector/luminance mask stacks render, including consecutive mask layers. UI controls and deeper Figma edge cases remain incomplete. |
| Auto layout: vertical/horizontal | ✅ | ✅ | ✅ | ✅ | ✅ | Yoga-backed layout. |
| Auto layout: wrap | ✅ | ✅ | ✅ | ✅ | ✅ | UI toggle exists. |
| Auto layout: grid | ✅ | ◐ | ◐ | ✅ | ✅ | CSS-grid-like support is partial; newer schema fields for grid child alignment and auto tracks are not fully exposed. |
| Padding / gaps / alignment | ✅ | ✅ | ✅ | ✅ | ✅ | Common flex controls are exposed. |
| Hug / fill / fixed sizing | ✅ | ✅ | ✅ | ✅ | ✅ | Min/max support is partial in UI. |
| Ignore auto layout / absolute positioning | ✅ | ✅ | ◐ | ✅ | ✅ | Mode is modeled; UI coverage is partial. |
| Strokes included in layout | ✅ | ◐ | — | ✅ | ✅ | Stored/exported and used in layout paths, but no obvious panel control. |
| Reverse z-index / align-content | ✅ | ◐ | — | ✅ | ✅ | Modeled and exported; UI is limited. |
| Constraints | ✅ | ◐ | — | ✅ | ✅ | Tools/API expose constraints; main UI is limited. |
| Layout grids / guides | ↩ | ◐ | — | ↩ | — | Imported layout grids and page guides render from preserved Figma metadata; style IDs round-trip, but editing UI is not exposed. |
| Text styles | ↩ | ◐ | — | ↩ | — | Style IDs round-trip; no style management UI. Rich schema metadata such as derived text data, leading trim, decoration style/thickness/fill, and semantic font style/weight is preserved for round-trip. |
| Rich style runs | ✅ | ✅ | ◐ | ✅ | ✅ | Import/render/export support; editing mixed runs is partial. |
| Text auto resize | ✅ | ✅ | ◐ | ✅ | ✅ | Used by renderer/layout; UI does not expose every mode. |
| Text truncation / max lines | ✅ | ✅ | — | ✅ | ✅ | Renderer supports ending truncation; no inspector control. |
| Text case | ✅ | ◐ | — | ✅ | ✅ | Model/export/JSX support; UI missing. |
| Vertical text alignment | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI/render parity needs more coverage. |
| Justified text | ✅ | ◐ | — | ✅ | ✅ | Modeled; UI does not expose it. |
| Font variations / OpenType features | ✅ | ✅ | — | ✅ | — | Imported `fontVariations`, common ligature/caps/numeric OpenType fields, and raw `toggledOnOTFeatures` / `toggledOffOTFeatures` are applied to CanvasKit text styles and exported; UI controls are not exposed. |
| Variables: collections/modes/aliases | ✅ | ◐ | ◐ | ✅ | ✅ | Color/number/string/boolean model exists; inspector coverage is still incomplete. |
| Variables bound to fills/strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Common color bindings render and edit. |
| Variables bound to text/layout/visibility/effects | ◐ | ◐ | ◐ | ◐ | ✅ | Some bindings exist; not full Figma property coverage. |
| Variables in prototypes / expressions / conditionals | — | — | — | — | — | Depends on prototype system, which is not implemented. |
| Libraries / publish / update review | ↩ | — | ◐ | ↩ | — | Metadata can survive round-trip; no full library workflow. |
| Prototype flows / starting points | — | — | — | — | — | Not modeled. |
| Prototype hotspots / triggers / actions | — | — | — | — | — | Not modeled. |
| Prototype overlays / scroll-to | — | — | — | — | — | Not modeled. |
| Smart animate / easing / spring / duration | — | — | — | — | — | Not modeled. |
| Interactive components | — | — | — | — | — | Component-level prototype connections are not supported. |
| Dev Mode inspect / measurements / annotations | — | — | — | — | ◐ | OpenPencil has CLI/MCP inspection, but not Figma Dev Mode UI. |
| Code Connect / dev resources / ready-for-dev | — | — | — | — | — | Not modeled. |
| Comments | — | — | — | — | — | Not modeled. |
| Version history / branches | — | — | — | — | — | Not modeled. |
| Real-time collaboration | — | ✅ | ✅ | — | — | OpenPencil has its own P2P collaboration, not Figma-compatible metadata. |
## Raw Kiwi metadata coverage
OpenPencil deliberately preserves many Figma/Kiwi fields even when they are not rendered or editable. These live under `SceneNode.source.fig` and are applied late during `.fig` export. A schema coverage test compares the current `fig.kiwi` `NodeChange` fields against modeled codec fields, raw-preserved fields, and intentionally schema-only metadata buckets so drift stays visible.
| Field group | Import/export | Render | UI | Fidelity impact |
| ----------------------------------------------------------------------- | ------------: | -------: | --: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source.fig.rawSize` | ✅ | Indirect | — | Preserves original Figma size for round-trip. Cleared when size is edited. |
| `source.fig.rawTransform` | ✅ | Indirect | — | Preserves exact Figma transform. Cleared when transform is edited. |
| `source.fig.rawNodeFields` | ✅ | Mixed | — | Late-applied to exported NodeChange for round-trip fidelity; raw-field and schema coverage tests guard preservation drift. |
| `source.fig.layout` | ✅ | ✅ | ◐ | Preserves original Figma stack metadata while using normalized layout fields. |
| `source.fig.symbolOverrides` | ✅ | Indirect | — | Important for instance override fidelity. |
| `source.fig.componentPropAssignments` | ✅ | Indirect | ◐ | Used for component property fidelity; not raw-editable. |
| `source.fig.derivedSymbolData` | ✅ | Indirect | — | Critical for instance-derived geometry/layout/text. |
| `source.fig.derivedSymbolDataLayoutVersion` | ✅ | — | — | Figma bookkeeping. |
| `source.fig.uniformScaleFactor` | ✅ | Indirect | — | Important for scaled instances. |
| Style IDs: fill/stroke/text/effect/grid | ↩ | — | — | Preserves style linkage for Figma, but OpenPencil has no style manager yet. |
| Component property refs/defs/specs | ✅ | Indirect | ◐ | Full Figma component-property authoring is incomplete. |
| State-group metadata | ↩ | — | — | Preserved only. |
| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. |
| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. |
| Page fields: background, page type, guides | ↩ | ◐ | — | Background color, background paints, page type, and guides round-trip for imported pages. Guides render as editor overlays but are not editable. |
| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness/skip-ink, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe; decoration style/thickness/color and leading trim now render through CanvasKit, and raster export bounds account for decoration overflow. |
| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. |
| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. |
## Highest-priority visual gaps
These are parsed or visible in Figma docs and most likely to cause visible differences in real design files:
1. **Masks** — tune remaining exact Figma stack semantics beyond common alpha/vector/luminance and consecutive-mask paths. `tests/fixtures/figma-oracles/masks.json` records live Figma API values for alpha, vector, and luminance masks.
2. **Corner smoothing** — expand Figma fixture comparisons and tune remaining stroke/effect edge cases.
3. **Pattern/noise/custom fills** — tune first-class pattern rendering for nested/effectful pattern sources and exact Figma hex spacing. `tests/fixtures/figma-oracles/pattern-noise-custom-paints.json` captures a real async Figma `PATTERN` payload and Figma-authored noise/texture/glass effect payloads; real `NOISE` / `CUSTOM` paint payloads remain blocked on Figma-authored samples.
4. **Variable-font and rich text fixtures** — broaden real-file coverage for variable axes, derived text data, leading trim, decoration style, underline offset/skip-ink, semantic font metadata, and raw OpenType feature metadata; `tests/fixtures/figma-oracles/rich-text-decoration.json` captures the first live Figma rich-text oracle.
5. **Boolean operation editing** — improve inspector/tooling workflows for imported boolean-operation nodes.
6. **Layout grids and guides** — add editing and fuller alignment/style parity for rendered imported page guides and Figma layout grids.
7. **Full component property and slot workflows** — support authoring, not just preserving imported payloads.
8. **Prototype/media/interaction metadata** — schema now includes more interaction, media runtime, animation, and slide fields; start by preserving flows/connections/runtime metadata before building playback.
## Code map
| Concern | Files |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Scene graph fields | `packages/core/src/scene-graph/types.ts` |
| Source metadata invalidation | `packages/core/src/scene-graph/source-metadata.ts` |
| Kiwi import mapping | `packages/core/src/kiwi/fig/node-change/convert.ts` |
| Kiwi export mapping | `packages/core/src/kiwi/fig/node-change/export-node.ts`, `packages/core/src/kiwi/fig/node-change/serialize.ts` |
| Kiwi schema | `packages/core/src/kiwi/fig/codec/schema/fig.kiwi`, `tests/engine/io/fig/import/schema-coverage.test.ts` |
| Renderer dispatch | `packages/core/src/canvas/scene.ts` |
| Fills / images / gradients | `packages/core/src/canvas/fills.ts` |
| Strokes | `packages/core/src/canvas/strokes.ts` |
| Effects / shadows | `packages/core/src/canvas/shadows.ts` |
| Text rendering | `packages/core/src/canvas/text.ts`, `packages/core/src/canvas/text-derived.ts` |
| Layout engine | `packages/core/src/layout.ts`, `packages/core/src/layout/**` |
| Property panels | `src/components/properties/**`, `packages/vue/src/controls/**` |
| CLI | `packages/cli/src/index.ts`, `packages/cli/src/commands/**` |
| MCP/tools | `packages/core/src/tools/**`, `packages/mcp/src/tool/registration.ts` |
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/locale-apis.md'
description: Lower-level locale stores and metadata exported by @open-pencil/vue.
---
# Locale APIs
In addition to `useI18n()`, the Vue SDK exports lower-level locale primitives for advanced integrations:
* `locale`
* `localeSetting`
* `setLocale()`
* `AVAILABLE_LOCALES`
* `LOCALE_LABELS`
Use these when you want direct store access, need to integrate locale state with a larger app shell, or want locale metadata without subscribing to the full `useI18n()` return object.
## Usage
```ts
import {
locale,
localeSetting,
setLocale,
AVAILABLE_LOCALES,
LOCALE_LABELS,
} from '@open-pencil/vue'
```
## Notes
* `locale` is the resolved active locale store
* `localeSetting` is the persisted user preference store
* `setLocale()` updates the preference and active locale together
* `AVAILABLE_LOCALES` and `LOCALE_LABELS` are useful for custom pickers
## Related APIs
* [useI18n](../composables/use-i18n)
---
---
url: 'https://openpencil.dev/development/renderer-profiler.md'
description: >-
Use the CanvasKit renderer profiler HUD and frame capture tools to investigate
rendering performance.
---
# Renderer Profiler
OpenPencil includes a CanvasKit renderer profiler for debugging frame time, GPU timing, draw calls, cache behavior, and expensive render phases.
## Enable the HUD
In the browser app, open the menu and choose:
```txt
View → Profiler
```
The app toggles `store.toggleProfiler()`, which maps to `editor.renderer.profiler.toggle()`.
The HUD is drawn directly on the Skia canvas so it measures the same rendering path as the document. It is not a DOM overlay.
## HUD metrics
The profiler HUD shows:
* **FPS / frame time** — smoothed frame cadence.
* **CPU** — JavaScript/WASM render time for the frame.
* **GPU** — latest available `EXT_disjoint_timer_query_webgl2` result when the browser exposes it.
* **Nodes / culled nodes** — total visible scene work and viewport culling count.
* **Draws** — WebGL draw calls counted through the instrumented context.
* **Cache** — whether the scene picture cache was reused.
* **Phases** — timings for renderer phases such as scene draw, picture replay/record, volatile overlays, section labels, selection, rulers, and flush.
* **Frame graph** — rolling frame history with 60 fps / 30 fps / slow thresholds and GPU bars when available.
GPU timing is asynchronous. The value shown is the latest completed GPU query, not necessarily the current frame.
## Implementation locations
Core profiler code lives in:
```txt
packages/core/src/profiler/
```
Main entry points:
* `render-profiler.ts` — `RenderProfiler` facade used by `SkiaRenderer`.
* `frame/stats.ts` — rolling frame statistics.
* `gpu-timer.ts` — WebGL timer query wrapper.
* `draw-call-counter.ts` — WebGL draw-call instrumentation.
* `phase-timer.ts` — phase timing and User Timing integration.
* `hud-renderer.ts` — canvas HUD rendering.
* `frame/capture.ts` and `speedscope-export.ts` — detailed capture and Speedscope export.
Renderer integration lives under:
```txt
packages/core/src/canvas/renderer*.ts
packages/core/src/canvas/renderer/
```
App wiring lives in:
```txt
src/app/editor/profiler/index.ts
src/app/shell/menu/schema.ts
src/app/shell/menu/app-menu.ts
```
## Programmatic use
From app/editor code:
```ts
store.toggleProfiler()
```
From a renderer instance:
```ts
renderer.profiler.toggle()
renderer.profiler.beginCapture()
// render one or more frames
const capture = renderer.profiler.endCapture()
const speedscopeJson = renderer.profiler.exportSpeedscope()
renderer.profiler.downloadSpeedscope()
```
Detailed captures are for targeted debugging. Keep the normal HUD path lightweight and avoid enabling expensive capture work unless a user or developer explicitly asks for it.
## Notes
* The profiler is designed to be safe when disabled: no timing calls or allocations should be added to hot paths unless `profiler.enabled` / `profiler.capturing` is active.
* GPU timing depends on browser and hardware support for `EXT_disjoint_timer_query_webgl2`.
* If GPU timing is unavailable, the HUD still reports CPU time, draw calls, phases, node counts, and cache status.
---
---
url: 'https://openpencil.dev/programmable/sdk/api/composables/use-i18n.md'
description: Read localized OpenPencil UI messages and switch the active SDK locale.
---
# useI18n
`useI18n()` returns reactive translation groups plus locale controls for OpenPencil-powered editor shells.
Use it when you want SDK-backed labels for menus, commands, panels, pages, and dialogs, or when you need to let users switch locales.
## Usage
```ts
import { useI18n } from '@open-pencil/vue'
const { menu, commands, panels, locale, availableLocales, localeLabels, setLocale } = useI18n()
```
## Returns
* `menu`
* `commands`
* `tools`
* `panels`
* `pages`
* `dialogs`
* `locale`
* `availableLocales`
* `localeLabels`
* `setLocale`
## Basic example
```vue
{{ menu.view }}
{{ localeLabels[code] }}
```
## Notes
* locale changes are reactive across all SDK message groups
* the SDK also exports lower-level locale primitives when you need direct store access
## Related APIs
* [useMenuModel](./use-menu-model)
* [SDK Locale APIs](../advanced/locale-apis)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-okhcl.md'
description: Work with RGBA and OkHCL color models for fills and strokes.
---
# useOkHCL
`useOkHCL()` exposes helpers for reading, enabling, disabling, and updating OkHCL color values on node fills and strokes.
Use it when you are building advanced color tooling that needs to switch between standard RGBA editing and perceptual OkHCL editing.
## Usage
```ts
import { useOkHCL } from '@open-pencil/vue'
const okhcl = useOkHCL()
```
## Returns
* `getFillColorModel()`
* `getStrokeColorModel()`
* `getFillOkHCLColor()`
* `getStrokeOkHCLColor()`
* `enableFillOkHCL()`
* `disableFillOkHCL()`
* `enableStrokeOkHCL()`
* `disableStrokeOkHCL()`
* `updateFillOkHCL()`
* `updateStrokeOkHCL()`
* `modelOptions`
## Related APIs
* [useFillControls](../composables/use-fill-controls)
* [useStrokeControls](../composables/use-stroke-controls)
* [ColorPickerRoot](../components/color-picker-root)
---
---
url: 'https://openpencil.dev/user-guide.md'
description: >-
Learn how to use OpenPencil — canvas navigation, drawing, text, components,
auto-layout, and more.
---
# User Guide
OpenPencil is an open-source, Figma-compatible design editor — fully local, AI-native, and programmable. This guide covers everything you need to know to use the editor effectively.
::: tip Cross-platform shortcuts
Throughout this guide, keyboard shortcuts use Mac notation: ⌘ = Command (Ctrl on Windows/Linux), ⌥ = Option (Alt), ⇧ = Shift.
:::
## Getting Around
* [Canvas Navigation](./canvas-navigation) — panning, zooming, and the hand tool
* [Selection & Manipulation](./selection-and-manipulation) — selecting, moving, resizing, rotating, and organizing nodes
## Creating Content
* [Drawing Shapes](./drawing-shapes) — rectangles, ellipses, lines, frames, sections, polygons, and stars
* [Text Editing](./text-editing) — creating and editing text with rich formatting
* [Pen Tool](./pen-tool) — drawing vector paths with bezier curves
## Organizing & Managing
* [Layers & Pages](./layers-and-pages) — the layers panel, pages, and properties panel
* [Context Menu](./context-menu) — right-click actions for clipboard, grouping, components, and more
* [Exporting](./exporting) — image export and .fig file operations
## Advanced Features
* [Auto Layout](./auto-layout) — flexbox-based automatic positioning
* [Components](./components) — reusable components, instances, and overrides
* [Variables](./variables) — design variables, collections, modes, and fill bindings
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-variables.md'
description: 'Read and mutate variable collections, variables, and variable values.'
---
# useVariables
`useVariables()` is the lower-level variables composable behind the higher-level variables editor helpers.
Use it when you want direct control over collections, active modes, filtering, and CRUD operations without taking the full table/dialog abstraction.
## Usage
```ts
import { useVariables } from '@open-pencil/vue'
const variables = useVariables()
```
## Returns
* `collections`
* `activeCollectionId`
* `activeCollection`
* `activeModes`
* `variables`
* `searchTerm`
* `setSearchTerm()`
* `setActiveCollection()`
* `addCollection()`
* `renameCollection()`
* `addVariable()`
* `removeVariable()`
* `renameVariable()`
* `updateVariableValue()`
* `formatModeValue()`
* `parseVariableValue()`
* `shortName()`
## Related APIs
* [useVariablesEditor](../composables/use-variables-editor)
* [useVariablesDialogState](./use-variables-dialog-state)
* [useVariablesTable](./use-variables-table)
---
---
url: >-
https://openpencil.dev/programmable/sdk/api/advanced/use-variables-dialog-state.md
description: Manage variables dialog editing state on top of useVariables().
---
# useVariablesDialogState
`useVariablesDialogState()` builds on `useVariables()` and adds dialog-specific editing state for collection renaming and focus management.
Use it when you are building a custom variables dialog rather than only consuming the combined `useVariablesEditor()` helper.
## Usage
```ts
import { useVariablesDialogState } from '@open-pencil/vue'
const variablesDialog = useVariablesDialogState()
```
## Adds to useVariables()
* `editingCollectionId`
* `setCollectionInputRef()`
* `startRenameCollection()`
* `commitRenameCollection()`
## Related APIs
* [useVariables](./use-variables)
* [useVariablesEditor](../composables/use-variables-editor)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-variables-table.md'
description: Build TanStack Table column definitions for OpenPencil variables UIs.
---
# useVariablesTable
`useVariablesTable(options)` returns reactive TanStack Table column definitions for variables editors.
Use it when you want the SDK's variable-table behavior but need to supply your own table instance, custom icons, or app-specific shell components.
## Usage
```ts
import { useVariablesTable } from '@open-pencil/vue'
const { columns } = useVariablesTable(options)
```
## Notes
* this is a specialized integration helper for table-driven variables UIs
* most consumers should start with `useVariablesEditor()` unless they need finer control
## Related APIs
* [useVariablesEditor](../composables/use-variables-editor)
* [useVariables](./use-variables)
* [useVariablesDialogState](./use-variables-dialog-state)
---
---
url: 'https://openpencil.dev/programmable/sdk/api/advanced/use-viewport-kind.md'
description: Read coarse mobile and desktop viewport flags for responsive editor shells.
---
# useViewportKind
`useViewportKind()` returns simple responsive flags used by OpenPencil editor UI.
Use it when your shell needs a light abstraction over breakpoints instead of wiring `useBreakpoints()` directly.
## Usage
```ts
import { useViewportKind } from '@open-pencil/vue'
const { isMobile, isDesktop } = useViewportKind()
```
## Returns
* `isMobile`
* `isDesktop`
## Related APIs
* [useCanvas](../composables/use-canvas)
---
---
url: 'https://openpencil.dev/development/vector-conversion.md'
description: >-
Development notes for OpenPencil boolean operations, flattening, text
outlines, stroke outlines, and vector geometry conversion.
---
# Vector conversion
OpenPencil has a shared path-conversion pipeline for commands that turn scene nodes into vector geometry.
## Commands
* **Boolean operations** keep a live `BOOLEAN_OPERATION` container and render it with CanvasKit path operations.
* **Flatten** replaces selected supported nodes with one persistent `VECTOR` node.
* **Outline text** replaces selected supported text nodes with vector outlines.
* **Outline stroke** replaces selected supported stroked nodes with vector stroke outlines.
The editor, renderer-backed Figma API, and menu command enablement all use the same source-path checks so unsupported nodes fail safely instead of being silently dropped.
## Supported sources
Supported sources include basic shapes, vectors, lines, nested boolean operations, and visual descendants inside groups, frames, components, and instances. Containers contribute their visible descendants, and their own fill/stroke if present.
Text can be converted when all required font data is loaded. The outline engine supports multiline text, horizontal and vertical alignment, letter spacing, style runs, and loaded fallback glyphs for mixed-font text such as Latin plus CJK.
## Unsupported sources
The conversion pipeline rejects these cases:
* visible image fills
* sections and component sets
* text with missing font data or missing fallback glyphs
* complex scripts that require shaping, such as Arabic, Hebrew, and Indic scripts
Complex-script text stays unsupported until we can extract exact shaped glyph runs and positions from the rendering stack.
## Figma API flatten
`FigmaAPI.flatten()` produces real vector geometry when a `SkiaRenderer` is attached with `api.setRenderer(renderer)`. In headless compatibility mode without a renderer, it keeps the historical placeholder behavior: the source nodes are replaced by a vector-sized placeholder without `vectorNetwork` geometry.
---
---
url: 'https://openpencil.dev/user-guide/vector-edit.md'
description: >-
How to edit vector path geometry: anchors, bezier handles, modifiers, and Pen
tool actions in edit mode.
---
# Vector Object Editing
Vector Object Editing mode lets you change a curve's **geometry**: anchor positions, segment shape, and bezier handles.\
In this mode, you edit the path itself, not standard object transforms.
## Entering the Mode
* Select a vector object with the Select tool.
* **Double-click the curve**.
This activates geometry editing for the selected vector.
## Exiting the Mode
* Press Escape.
* Or switch to another editing context.
## What Changes in This Mode
* The normal transform bounding box is disabled for the object.
* Anchor, segment, and handle editing becomes available.
* Cursor behavior does not switch to resize/rotate at bbox corners.
## Basic Actions
### Move an Anchor
* Drag an anchor point.
* Connected segments and path shape update live in preview.
### Edit a Bezier Handle
* Drag a handle on the anchor.
* By default, behavior follows the anchor's current handle composition.
## Handle Drag Modifiers
| Action | Mac | Windows / Linux |
|----------|-----|-----------------|
| Continuous (Smooth / Continuous) | Cmd + drag | Ctrl + drag |
| Corner (Independent handles) | Option + drag | Alt + drag |
| Direction lock (length only) | Shift + drag | Shift + drag |
### Continuous: Cmd/Ctrl + drag
* The active handle is constrained to the same line as the sister handle.
* Only the active handle length changes.
* Use this for smooth transitions without a corner break.
### Corner: Option/Alt + drag
* The active handle is edited independently.
* The sister handle stays in place.
* Use this to create a sharp corner transition.
### Direction Lock: Shift + drag
For anchors with **Continuous** or **Symmetric** composition:
* handle direction is locked to the value from **before the current drag started**;
* dragging changes only handle length (or lengths, depending on composition).
## Bend Override by Dragging an Anchor
When you drag an anchor while holding Cmd/Ctrl, the editor selects the target handle by **segment attachment direction** at that anchor (not by nearest neighbor-point distance).\
This also works on multi-branch vector-web anchors: once resolved, the target handle stays locked for the current drag.
## Using the Pen Tool in Edit Mode
With the Pen tool active:
* **Click a segment** to insert a new anchor (split segment).
* **Click an open-path endpoint** to resume drawing from that point.
* **Option/Alt + click an anchor** to delete it (when topology allows).
For path creation and closing behavior, see [Pen Tool](./pen-tool.md).
## Practical Workflow
1. Draw a shape with the Pen tool.
2. Double-click the curve to enter Vector Object Editing mode.
3. Move anchors to refine the silhouette.
4. Drag handles:
* with Cmd/Ctrl for smooth continuous transitions,
* with Option/Alt for independent edits,
* with Shift for length-only edits.
5. Press Escape to exit.