Find a file
Danila Poyarkov 7992516d81
fix: match Figma's transforms, strokes, booleans, sections, and names (#931)
* fix(core): start new layers with Figma's defaults in the editor and plugin API

The plugin API created bare nodes: frames, components, and shapes without fills, and lines and vectors without strokes, so scripts written for Figma drew nothing. Drawn lines also had a black fill instead of a stroke and were invisible. Both paths now share newLayerDefaults, recorded from Figma desktop 126: frames and components white with frames clipping their content, shapes #D9D9D9, lines and vectors a black 1 px stroke, text black. A stroke a script adds gets the 1 px default weight, and an empty vector has no render bounds.

* fix(core): combine variants as Figma does from the canvas and from scripts

The plugin API and the editor command each built component sets their own way, both with 40 px of padding and a grey fill. Figma's command pads the variants by 20 and outlines the set with a 1 px dashed #8A38F5 stroke; its plugin API wraps them exactly with no fill or stroke. One variantSetProps now places and styles the set for both, with a canvas or script style, and applyVariantProperties derives variant properties for both.

* fix(core): report group children in their container's space in the plugin API

Figma's plugin API places children of groups and booleans relative to the nearest real container and refits a group whenever a script changes one of its children. Ours reported group-relative positions and never refit, so scripts placing layers inside groups landed them in the wrong place. x, y, and relativeTransform now map through the groups around a node, and geometry changes, appendChild, insertChild, and remove refit the surrounding groups. The refit moves to Scene Graph as fitEnclosingGroups, shared by the canvas (with undo) and the plugin API.

* test(core): pass script-style strokes and typed components in parity tests

* test(e2e): expect Figma's default shape grey in the scene freshness spec

* fix(vue): draw lines by length and angle as Figma does

The Line tool sized a line as the box spanned by the drag. With the stroke a new line now gets, that box drew as a rectangle outline. A line now starts at the press point with the drag length as its width, no height, and the drag angle as its rotation, as Figma's Line tool makes it; Shift snaps the angle to 45° steps, as the docs already described, and a click makes a 100 px horizontal line.

* fix(core): give each new layer its own copy of the default paints

The defaults spread each paint shallowly, so every layer shared the colour object of the module-level default and editing one layer's colour in place changed the next new layer. Copy the paints with the Scene Graph copy helpers.

* fix(core): group, ungroup, and combine layers through shared code in the plugin API

The plugin API wrapped layers, ungrouped, made booleans, and made components from layers with its own code. Ungroup moved the children to the top of the stack, booleans were named "Boolean union", and a component made from a frame cloned its children under new ids. These now run through the editor's shared wrap, ungroup, and boolean functions, with the placement and defaults recorded in Figma desktop 126: a group or boolean without an index goes on top, ungrouped children take the group's place, booleans are named after the operation and filled with the default grey, a frame becomes a component in its place with its children, and any other layer is wrapped in a white component named after it. Undoing a wrap in the editor now returns each layer to its own place in the stack.

* fix(core): group, frame, combine, and make components from the canvas as Figma does

Recorded in Figma desktop 126: a container made from the canvas takes the topmost selected layer's place, Frame selection adds no fill and does not clip, a component wrapped around layers is white and takes a single layer's name, and a boolean is filled like its topmost operand, or its base for Subtract, without strokes. The canvas commands and the plugin API now share the wrap parent check, stack ordering, component rules, and boolean paints, and the plugin API's createComponentFromNode converts groups in place as Figma does. Undoing a boolean returns each operand to its own place in the stack.

* refactor(core): reuse translate when centering pasted layers

* fix: match Figma's transforms, strokes, booleans, sections, and names

Each behaviour was recorded in Figma desktop 126 with the same script, or
from its canvas, and both the editor and the plugin API now share it.

- Scripts turn a layer counterclockwise about its top-left corner, read x
  and y as that corner, keep it in place on resize, can set
  relativeTransform, and get absoluteBoundingBox around the turned layer.
  appendChild and insertChild keep x, y, and rotation in the new parent
  instead of the canvas position, which the create and reparent tools
  inherit.
- A layer keeps its stroke weight and alignment without strokes, in the
  model and through .fig export and import. strokeWeight and strokeAlign
  apply to every stroke, and a stroke added from the panel or a script
  takes the layer's.
- Booleans size to their result when a renderer can measure it, from the
  canvas, from scripts, and when an operand moves.
- New sections take Figma's fill for the interface theme, a faint white
  stroke, and 2 px corners; sections and component sets draw with their
  own radius.
- Layers drawn on the canvas and the containers it wraps layers in are
  numbered past the highest number on the page; scripts keep plain names.

* test(core): dispose the editor in the canvas boolean bounds test

Attaching CanvasKit installs a global text measurer, and the test left it installed, so later text measurement tests in the same process found it.

* fix(core): undo a boolean's group refit and clamp dashed set corners

Sizing a boolean to its result also refits the groups around it, and undo left them refitted, so the operands came back displaced. createBooleanOperation returns the fit, and undo reverses it first. A component set's dashed outline now clamps its radius to its bounds as the fill does, and a script's resize refits the enclosing groups once, after the corner is restored.
2026-10-07 08:36:50 +00:00
.claude ci: keep AI disclosure out of co-author credits 2026-09-15 23:12:44 +03:00
.devcontainer refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
.github test(fig): pin override and variable precedence to what Figma renders (#912) 2026-10-06 09:08:51 +00:00
.storybook fix(vue): start CanvasKit once CanvasSurface provides the canvas (#823) 2026-10-02 00:23:49 +04:00
.vscode
assets/brand build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
desktop feat(desktop): show updates in a Software Update window (#936) 2026-10-06 14:57:31 +00:00
lint build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
packages fix: match Figma's transforms, strokes, booleans, sections, and names (#931) 2026-10-07 08:36:50 +00:00
public feat: refresh branding with generated platform icons (#707) 2026-09-16 11:54:07 +03:00
skills/open-pencil feat: behaviours and preview mode (#893) 2026-10-06 13:23:05 +00:00
src fix: match Figma's transforms, strokes, booleans, sections, and names (#931) 2026-10-07 08:36:50 +00:00
tests fix: match Figma's transforms, strokes, booleans, sections, and names (#931) 2026-10-07 08:36:50 +00:00
tools feat(desktop): show updates in a Software Update window (#936) 2026-10-06 14:57:31 +00:00
vite feat(lint): suggest group-to-frame and hidden-layer fixes; keep canvas edits in code previews (#870) 2026-10-04 12:46:06 +00:00
.coderabbit.yaml chore: hide review status chatter and test generation prompts 2026-09-15 21:43:04 +03:00
.gitattributes build: merge CHANGELOG.md entries instead of conflicting (#905) 2026-10-05 12:56:22 +00:00
.gitignore build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
.gitleaks.toml chore(tools): add secret scanning gate 2026-07-01 15:24:06 +03:00
.lfsconfig ci: move Git LFS to provider-neutral gateway 2026-08-01 17:52:08 +03:00
.oxfmtrc.json style: tighten import grouping 2026-05-06 02:22:08 +03:00
AGENTS.md fix: lay out fill text and repeat() grid tracks (#866) 2026-10-05 14:06:07 +00:00
bun.lock feat: behaviours and preview mode (#893) 2026-10-06 13:23:05 +00:00
bunfig.toml refactor(mcp): align transport domain structure 2026-07-25 22:59:59 +03:00
CHANGELOG.md fix: match Figma's transforms, strokes, booleans, sections, and names (#931) 2026-10-07 08:36:50 +00:00
commitlint.config.ts build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
CONTRIBUTING.md docs: describe rebasing and merging stacked pull requests (#869) 2026-10-04 10:17:30 +00:00
index.html fix: explain unsupported browsers instead of a blank window (#745) 2026-09-22 14:40:59 +04:00
knip.json fix(text): finalize font readiness and label shaping (#593) 2026-08-30 14:33:32 +03:00
LICENSE docs: acknowledge OpenPencil contributors in license 2026-08-31 09:11:10 +03:00
oxlint.json chore: prefer es-toolkit helpers and lint the mechanical cases (#898) 2026-10-05 09:34:00 +00:00
package.json refactor(vue)!: remove the variables table composables (#908) 2026-10-06 12:35:41 +00:00
playwright.config.ts chore: merge master into live-editor-regressions 2026-09-16 00:14:31 +03:00
portless.json chore: add Portless development URLs 2026-08-20 08:15:54 +03:00
README.md feat: behaviours and preview mode (#893) 2026-10-06 13:23:05 +00:00
SECURITY.md docs: document private security reporting 2026-05-17 12:52:10 +03:00
steiger.config.ts build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
tsconfig.json test: typecheck the test suites and fix what that found (#896) 2026-10-05 12:42:38 +00:00
tsconfig.node.json ci: validate PR commits and streamline package verification 2026-09-15 20:51:33 +03:00
tsconfig.tests.json test: typecheck the test suites and fix what that found (#896) 2026-10-05 12:42:38 +00:00
updater.html feat(desktop): show updates in a Software Update window (#936) 2026-10-06 14:57:31 +00:00
vite.config.ts feat(desktop): show updates in a Software Update window (#936) 2026-10-06 14:57:31 +00:00
wdio.conf.ts test(native): cover the desktop MCP server lifecycle (#723) 2026-09-18 11:42:55 +03:00

OpenPencil

MIT license npm Discord GitHub Discussions

Open-source design editor. Opens .fig and .pen design files, includes built-in AI, and ships as a programmable toolkit with a headless Vue SDK for building custom editors.

Status: Active development. Usable today, with some rough edges as features evolve.

Try it online → · Download · Documentation · Roadmap · llms.txt

OpenPencil

Installation

macOS (Homebrew):

brew install --cask openpencil

Or download from the releases page, or use the web app — no install needed.

Requires macOS 13 or later with current Safari updates, Windows 10 or later, or Linux with WebKitGTK 2.40+; the web app needs Chrome 111, Edge 111, Firefox 128, or Safari 16.4 or later. See system requirements.

What it does

  • Opens .fig and .pen files — read and write native Figma files, open supported Pencil documents from the app or OS file browser, copy & paste nodes between apps
  • AI builds designs — describe what you want in chat, 100+ tools create and modify nodes. Connect OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints
  • Fully programmable — headless CLI, XPath queries, Figma Plugin API plus an openpencil API via eval, MCP server for AI agents, and desktop agent integrations for Claude Code, Codex, and Gemini CLI
  • Lint, convert, and extract tokens — inspect documents, lint naming/layout/accessibility, convert between supported formats, analyze colors/typography/spacing/clusters, and extract design tokens
  • Components and variants — create reusable components, group variants into component sets, insert local assets as instances, and switch variants from the inspector
  • Working controls — give a component a Reka UI behaviour (switch, slider, tabs, text field, …) and preview it live over the canvas, with real inputs, focus, and keyboard
  • Image vectorization — convert image layers into editable vector layers with Recraft or fal.ai
  • Design-to-code export — export selections as JSX/Tailwind, generate token outputs, and map designs into component-oriented code workflows
  • Vue SDK for custom editors — headless components and composables for embedding OpenPencil into other apps or building workflow-specific editing surfaces. Read the SDK docs →
  • Real-time collaboration — P2P via WebRTC, no server, no account. Cursors, presence, follow mode
  • Auto layout & CSS Grid — flex and grid layout via Yoga WASM, with gap, padding, alignment, track sizing
  • ~15 MB desktop app — Tauri v2 for macOS, Windows, Linux. Also runs in the browser as a PWA

CLI

npm install -g @open-pencil/cli
# or: bun add -g @open-pencil/cli

Inspect design files

Browse node trees, search by name or type, dig into properties — all without opening the editor:

openpencil tree design.fig
openpencil find design.pen --type TEXT
openpencil node design.fig --id 1:23
openpencil info 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)

Query with XPath

Find nodes by type, attributes, and structure:

openpencil query design.fig "//FRAME[@width < 300]"  # Frames under 300px
openpencil query design.fig "//SECTION//TEXT"        # Text inside sections

Export, convert, and import

Render to PNG, JPG, WEBP, SVG, PDF, PPTX, HTML, JSX, Storybook stories, or .fig, convert between document formats, and turn HTML/CSS into editable layers:

openpencil export design.fig                  # PNG
openpencil export design.fig -f jpg -s 2      # JPG at 2x
openpencil export design.fig -f tailwind-jsx  # Tailwind JSX
openpencil export design.fig -f storybook     # Storybook stories per component
openpencil convert design.pen design.fig      # Between document formats
openpencil import card.html --css card.css    # HTML/CSS → editable .fig

Lint and analyze

Catch naming, layout, and accessibility issues, and audit a design system's real palette, type scale, spacing, and repeated components:

openpencil lint design.fig --preset strict  # Naming, layout, accessibility
openpencil analyze colors design.fig        # Also typography, spacing, clusters
openpencil variables design.fig             # Variables and collections
#1d1b20  ██████████████████████████████ 17155×
#49454f  ██████████████████████████████ 9814×
#ffffff  ██████████████████████████████ 8620×
#6750a4  ██████████████████████████████ 3967×

Script with the Figma Plugin API

eval runs JavaScript against the document with Figma's Plugin API; -w writes the result back:

openpencil eval design.fig -c "figma.currentPage.selection.forEach(n => n.opacity = 0.5)" -w

Control the running app

Omit the file argument and the CLI works on the document open in the editor:

openpencil tree                                 # Inspect the live document
openpencil documents list                       # Also open, new, save, close, activate
openpencil tool call get_selection              # Run any MCP tool
openpencil undo                                 # Undo the newest automation change
openpencil settings set appearance.theme light  # Change editor settings

Every command supports --json. See the CLI reference for all commands and options.

AI & MCP

Built-in chat

Press ⌘J (CtrlJ on Windows and Linux) to open the AI assistant. It has 100+ tools that can create shapes, set fills and strokes, manage auto-layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Bring your own API key for OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints. No backend, no account.

Not every provider works in the browser, and not every model streams tool calls correctly. See BYOK provider & model compatibility for measured results — contributions welcome.

Coding agents (desktop)

Use Claude Code, Codex, or Gemini CLI directly in the chat panel. The agent connects to the editor's MCP server and uses all 100+ design tools. Requires the desktop app and the agent CLI installed locally.

Pi works too: install @open-pencil/harness globally, then add a Pi model profile in Settings → AI & agents.

Setup (Claude Code):

  1. Install the ACP adapter: npm install -g @agentclientprotocol/claude-agent-acp
  2. Add MCP permission to ~/.claude/settings.json:
    {
      "permissions": {
        "allow": ["mcp__open-pencil__*"]
      }
    }
    
  3. Open the desktop app → ⌘J → select Claude Code from the provider dropdown

MCP server

Connect Claude Code, Cursor, Windsurf, or any MCP client to inspect, modify, and export design documents headlessly. 100+ tools. Full docs →

Stdio (Claude Code, Cursor, Windsurf):

npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp

For other MCP clients:

{
  "mcpServers": {
    "open-pencil": {
      "command": "openpencil-mcp"
    }
  }
}

HTTP (scripts, CI):

openpencil-mcp-http   # Unix socket on macOS/Linux + http://127.0.0.1:7600/mcp

Set OPENPENCIL_MCP_ROOT to limit file access to one directory; it defaults to the current working directory.

AI agent skill

Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:

npx skills add open-pencil/open-pencil

Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports skills.

For documentation-aware agents, the docs site publishes llms.txt, llms-full.txt, and per-page Markdown files generated from the VitePress docs.

Collaboration

Share a link to co-edit in real time. No server, no account — peers connect directly via WebRTC.

  1. Click the share button in the top-right panel
  2. Share the generated link (app.openpencil.dev/share/<room-id>)
  3. Collaborators see your cursor, selection, and edits in real time
  4. Click a peer's avatar to follow their viewport

Why

Figma is a closed platform that actively fights programmatic access. Their MCP server is read-only. figma-use added full read/write automation via CDP — then Figma 126 killed CDP. Your design files are in a proprietary binary format that only their software can fully read. Your workflows break when they decide to ship a point release.

OpenPencil is the alternative: open source (MIT), reads .fig files natively, every operation is scriptable, and your data never leaves your machine.

See the roadmap for product direction and current Figma compatibility gaps.

Community

Contributing

bun install
bun run dev:portless  # Web editor at https://open-pencil.localhost
bun run tauri dev     # Desktop app (requires Rust)

CONTRIBUTING.md covers setup, quality gates, pull requests, and commits. AGENTS.md maps the repository and links the guide inside each package. Desktop builds need Rust and the Tauri v2 prerequisites; run bun run tauri build.

Tech stack

Layer Tech
Rendering Skia (CanvasKit WASM)
Layout Yoga WASM (flex + grid via fork)
UI Vue 3, Reka UI, Tailwind CSS 4
File format Kiwi binary + Zstd + ZIP
Collaboration Trystero (WebRTC P2P) + Yjs (CRDT)
Desktop Tauri v2
AI/MCP Vercel AI SDK (multi-provider BYOK), MCP SDK, Hono

Acknowledgments

Thanks to @sld0Ant (Anton Soldatov) for creating and maintaining the documentation site.

License

OpenPencil is licensed under the MIT License.

Copyright (c) 2026 Danila Poyarkov and OpenPencil contributors.