Fix code formatting across all docs

- Wrap all enum values in backticks (FRAME, SOLID, ROUND, etc.)
- Fix single-backtick code blocks → triple backticks in node-types, scene-graph
- Replace ASCII diagrams with mermaid in file-format
- Delete stale mcp-tools.md and eval-command.md from all locales
- Update dead links: /reference/mcp-tools → /programmable/mcp-server,
  /eval-command → /programmable/cli/scripting
This commit is contained in:
Danila Poyarkov 2026-03-08 13:53:33 +03:00
parent 96b9b7733b
commit d91e6a36d9
70 changed files with 900 additions and 3313 deletions

View file

@ -4,7 +4,7 @@
### Phase 1: Core-Engine ✅
SceneGraph, Skia-Rendering, Grundformen, Auswahl, Zoom/Pan, Rückgängig/Wiederherstellen, Fanglinien.
`SceneGraph`, Skia-Rendering, Grundformen, Auswahl, Zoom/Pan, Rückgängig/Wiederherstellen, Fanglinien.
### Phase 2: Editor-UI + Layout ✅
@ -16,7 +16,7 @@ Vue 3 + Reka UI Panels, Eigenschaften, Ebenen, Werkzeugleiste, Yoga Auto-Layout,
### Phase 4: Komponenten + Variablen ✅
Komponenten, Instanzen, Overrides, Komponenten-Sets, Variablen (COLOR/FLOAT/STRING/BOOLEAN), Sammlungen, Modi, Bildexport, Kontextmenü, Rich-Text-Formatierung.
Komponenten, Instanzen, Overrides, Komponenten-Sets, Variablen (`COLOR`/FLOAT/STRING/BOOLEAN), Sammlungen, Modi, Bildexport, Kontextmenü, Rich-Text-Formatierung.
### Phase 5: KI-Integration & Werkzeuge ✅
@ -55,7 +55,7 @@ Komponenten, Instanzen, Overrides, Komponenten-Sets, Variablen (COLOR/FLOAT/STRI
- Prototyping (Frame-Verbindungen, Übergänge, Animationen)
- Kommentare (Pin, Threads, Auflösen)
- PWA-Unterstützung
- Varianten-Switching, FLOAT/STRING/BOOLEAN Variable UI, variablengesteuertes Theming
- Varianten-Switching, `FLOAT`/STRING/BOOLEAN Variable UI, variablengesteuertes Theming
## Zeitplan

View file

@ -1,140 +0,0 @@
# `open-pencil eval` — Figma-ähnliche Plugin-API für Headless-Skripting
## Übersicht
`bun open-pencil eval <file> --code '<js>'` führt JavaScript gegen eine `.fig`-Datei mit einem Figma-kompatiblen `figma`-Globalobjekt aus. Dies ermöglicht Headless-Skripting, Batch-Operationen, KI-Werkzeug-Ausführung und Tests — alles ohne die GUI.
Das `figma`-Objekt spiegelt Figmas Plugin-API-Oberfläche so nah wie möglich, sodass vorhandenes Figma-Plugin-Wissen und Code-Snippets direkt übertragbar sind.
```bash
# Frame erstellen, Auto-Layout setzen, Kinder hinzufügen
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.paddingTop = frame.paddingBottom = 16
frame.paddingLeft = frame.paddingRight = 16
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
const title = figma.createText()
title.characters = "Hello World"
title.fontSize = 24
frame.appendChild(title)
return { id: frame.id, name: frame.name }
'
# Knoten abfragen
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.type === "FRAME" && n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name, w: b.width, h: b.height }))
'
# Von stdin lesen (für mehrzeilige Skripte / Piping)
cat transform.js | bun open-pencil eval design.fig --stdin
# Änderungen zurückschreiben
bun open-pencil eval design.fig --code '...' --write
bun open-pencil eval design.fig --code '...' -o modified.fig
```
## Architektur
```
┌──────────────────────────────────────────────────────┐
│ CLI: `open-pencil eval <file> --code '...'` │
│ ↓ │
│ loadDocument(file) → SceneGraph │
│ ↓ │
│ FigmaAPI(sceneGraph) → `figma` Proxy-Objekt │
│ ↓ │
│ AsyncFunction('figma', wrappedCode)(figmaProxy) │
│ ↓ │
│ Ergebnis als JSON / agentfmt ausgeben │
│ optional: saveDocument(file) bei --write │
└──────────────────────────────────────────────────────┘
```
### Schlüsselklassen
| Klasse | Ort | Rolle |
|--------|-----|-------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Proxy-Objekt, das `figma.*`-Methoden gegen `SceneGraph` implementiert |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy, der `SceneNode` mit Figma-ähnlichem Eigenschaftszugriff umhüllt |
| `eval`-Befehl | `packages/cli/src/commands/eval.ts` | CLI-Befehl: Dokument laden, API erstellen, Code ausführen |
### Warum in `@open-pencil/core`?
Die `FigmaAPI`-Klasse lebt in core (nicht CLI), weil:
- **KI-Werkzeuge nutzen sie wieder** — das Chat-Panels `render`-Werkzeug kann JSX über dieselbe API ausführen
- **Testskripte** — Unit-Tests können die API für Fixture-Setup verwenden
- **Keine DOM-Abhängigkeiten** — läuft headless in Bun
## CLI-Befehl
```
bun open-pencil eval <file> [optionen]
Argumente:
file .fig-Datei zum Bearbeiten
Optionen:
--code, -c Auszuführender JavaScript-Code (hat Zugriff auf `figma`-Global)
--stdin Code von stdin statt --code lesen
--write, -w Änderungen in die Eingabedatei zurückschreiben
-o, --output In eine andere Datei schreiben
--json Ergebnis als JSON ausgeben (Standard für Nicht-TTY)
--quiet, -q Ausgabe unterdrücken, nur Datei schreiben
```
### Ausführungsmodell
1. `.fig` laden → `SceneGraph`
2. `FigmaAPI(graph)` erstellen → `figma`-Proxy
3. Benutzercode in async-Funktion verpacken: `return (async () => { <code> })()`
4. Mit `figma` als einzigem Argument ausführen
5. Rückgabewert ausgeben (JSON oder agentfmt)
6. Bei `--write` oder `-o`: `SceneGraph` zurück in `.fig` serialisieren
### Rückgabewert-Formatierung
- `undefined` / `void` → keine Ausgabe
- Primitive → direkt ausgeben
- Objekte/Arrays → `JSON.stringify(result, null, 2)` oder agentfmt-Tabellen
- `FigmaNode` → serialisiert als `{ id, type, name, x, y, width, height, fills, ... }`
- Arrays von `FigmaNode` → als Liste serialisiert
## `FigmaAPI` — Phasenweise Implementierung
Die vollständige API-Referenz mit Phasen (Core, Komponenten, Variablen, Stile) und Eigenschafts-Mapping finden Sie in der [englischen Version](/eval-command).
### Phase 1: Core
Deckt ~80% realer Plugin-Skripte ab. Enthält: Dokument & Seite, Knotenerstellung, Knoteneigenschaften, Baumoperationen, Auto-Layout, Text, Kontur und Export.
### Phase 2: Komponenten & Instanzen
`figma.createComponent()`, `combineAsVariants()`, `createInstance()`, `detachInstance()`, `mainComponent`.
### Phase 3: Variablen
`figma.variables.getLocalVariables()`, `createVariable()`, `createVariableCollection()`, `setBoundVariable()`.
### Phase 4: Stile & Erweitert
Paint/Text/Effekt-Stile, Boolesche Operationen, JSX-Renderer.
## Gemeinsam mit KI-Werkzeugen
Die `FigmaAPI`-Klasse ist **dieselbe API-Oberfläche**, die KI-Werkzeuge verwenden. Dies stellt sicher, dass CLI-Skripte und KI-Werkzeuge identisch funktionieren.
## Dateilayout
```
packages/core/src/
figma-api.ts # FigmaAPI-Klasse + FigmaNode-Proxy
packages/cli/src/commands/
eval.ts # CLI-Befehl
```

View file

@ -2,7 +2,7 @@
## Systemübersicht
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Editor-Layout
@ -62,7 +62,7 @@ Metas Yoga bietet CSS-Flexbox-Layout-Berechnung. Ein dünner Adapter mappt Figma
### Dateiformat (Kiwi-Binär)
Verwendet Figmas Kiwi-Binär-Codec mit 194 Message-/Enum-/Struct-Definitionen. Import: Header parsen → Zstd-Dekompression → Kiwi-Dekodierung → NodeChange[] → Szenengraph. Export kehrt den Prozess um, inklusive Thumbnail-Generierung.
Verwendet Figmas Kiwi-Binär-Codec mit 194 Message-/Enum-/Struct-Definitionen. Import: Header parsen → Zstd-Dekompression → Kiwi-Dekodierung → `NodeChange`[] → Szenengraph. Export kehrt den Prozess um, inklusive Thumbnail-Generierung.
Siehe [Dateiformat-Referenz](/reference/file-format) für Details.

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
Siehe [MCP-Tools-Referenz](/reference/mcp-tools) für die vollständige Werkzeugliste.
Siehe [MCP-Tools-Referenz](/programmable/mcp-server) für die vollständige Werkzeugliste.
## CLI

View file

@ -126,7 +126,7 @@ Feature-für-Feature-Vergleich der Figma-Design-Funktionen mit dem aktuellen Imp
| Hintergrund-Unschärfe | ✅ | Inhalt hinter der Ebene unscharf |
| Vordergrund-Unschärfe | ✅ | Unschärfe im Vordergrund |
| Konturstärke | ✅ | Konfigurierbar im Eigenschafts-Panel |
| Kontur-Endung (Rund, Quadrat, Pfeil) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Kontur-Endung (Rund, Quadrat, Pfeil) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Kontur-Verbindung (Gehrung, Abschrägung, Rund) | ✅ | Alle drei Verbindungstypen |
| Strichmuster | ✅ | Strich-An/Strich-Aus-Muster |
| Eckenradius | ✅ | Einheitlicher und pro-Ecke-Radius mit unabhängigem Toggle |
@ -160,7 +160,7 @@ Feature-für-Feature-Vergleich der Figma-Design-Funktionen mit dem aktuellen Imp
| Varianten | 🔲 | Variantenwechsel und eigenschaftsbasierte Auswahl |
| Komponenteneigenschaften | 🔲 | Boolesche, Text-, Instanztausch-Eigenschaften |
| Override-Propagation | ✅ | Änderungen an Hauptkomponente werden propagiert; Overrides erhalten |
| Variablen (Farbe, Zahl, String, Boolean) | 🟡 | COLOR mit vollem UI; FLOAT/STRING/BOOLEAN definiert ohne Bearbeitungs-UI |
| Variablen (Farbe, Zahl, String, Boolean) | 🟡 | `COLOR` mit vollem UI; `FLOAT`/STRING/BOOLEAN definiert ohne Bearbeitungs-UI |
| Variablensammlungen & Modi | 🟡 | Sammlungen, Modi, activeMode-Wechsel funktionieren; kein Variablen-Theming-UI |
| Stile (Farbe, Text, Effekt, Layout) | 🔲 | Wiederverwendbare benannte Stil-Presets |
| Bibliotheken (veröffentlichen, teilen, aktualisieren) | 🔲 | Geteilte Komponenten-/Stil-Bibliotheken |
@ -187,7 +187,7 @@ Feature-für-Feature-Vergleich der Figma-Design-Funktionen mit dem aktuellen Imp
| Funktion | Status | Anmerkungen |
|----------|--------|-------------|
| .fig-Datei-Import | ✅ | Vollständiger Kiwi-Codec: 194 Definitionen, ~390 Felder pro NodeChange |
| .fig-Datei-Import | ✅ | Vollständiger Kiwi-Codec: 194 Definitionen, ~390 Felder pro `NodeChange` |
| .fig-Datei-Export | ✅ | Kiwi-Encoding + Zstd-Kompression + Miniatur-Generierung |
| Speichern / Speichern unter | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; native Dialoge (Tauri), File System Access API (Chrome/Edge), Download-Fallback (Safari) |
| Figma-Zwischenablage (Einfügen) | ✅ | Kiwi-Binär aus Figma-Zwischenablage dekodieren |

View file

@ -1,71 +1,94 @@
# Dateiformat
# File Format
## .fig-Dateistruktur
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Import-Pipeline
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Export-Pipeline
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,150 +0,0 @@
# MCP-Server
OpenPencil enthält einen MCP-Server (Model Context Protocol), der KI-Coding-Tools — Claude Code, Cursor, Windsurf usw. — ermöglicht, .fig-Dateien headless zu lesen und zu bearbeiten.
Zwei Transporte
## **stdio** für MCP-Clients, **HTTP** für alles andere.
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
Oder aus dem Quellcode ausführen:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Starts on port 3100 (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
Install the OpenPencil skill for your AI coding agent:
```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).
## Tools (75)
### 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_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
### 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 |
### 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 |
### 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 |

View file

@ -1,4 +1,4 @@
# Knotentypen
# 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 17 of these types.
@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -1,8 +1,8 @@
# Szenengraph
# Scene Graph
## In-Memory-Darstellung
## In-Memory Representation
Knoten leben in einer flachen `Map<string, Node>` keyed by GUID string. Baumstruktur wird über `parentIndex` references. This gives O(1)-Lookup by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -4,7 +4,7 @@
### Phase 1: Core Engine ✅
SceneGraph, Skia rendering, basic shapes, selection, zoom/pan, undo/redo.
`SceneGraph`, Skia rendering, basic shapes, selection, zoom/pan, undo/redo.
**Delivered:**
- Scene graph with flat Map storage and parent-child tree
@ -77,7 +77,7 @@ Components, instances, overrides, variables, collections, modes, image export.
- Resizable left/right panels via reka-ui Splitter (persistent layout)
- @/ import alias, shared types module (src/types.ts, src/global.d.ts)
- Codebase lint-clean: 0 oxlint warnings, 0 tsgo type errors
- Variables: COLOR type with collections, modes, bindings, FillSection variable picker, .fig import
- Variables: `COLOR` type with collections, modes, bindings, FillSection variable picker, .fig import
- Variables dialog: TanStack Table with resizable columns, mode columns, collection tabs with rename, search, demo collections (Primitives/Semantic/Spacing), undo/redo for all variable operations
- Image export: PNG/JPG/WEBP with ExportSection (scale, format, live preview), <kbd>⇧</kbd><kbd>⌘</kbd><kbd>E</kbd> shortcut, context menu
- Canvas-native text editing: TextEditor class in core, phantom textarea, cursor/selection/word boundaries on canvas, caret blinking, selection highlights
@ -92,7 +92,7 @@ Components, instances, overrides, variables, collections, modes, image export.
**Remaining (deferred to Phase 6):**
- Variant switching
- Variable types: FLOAT, STRING, BOOLEAN editing UI
- Variable types: `FLOAT`, `STRING`, `BOOLEAN` editing UI
- Variable-driven theming
### Phase 5: AI Integration & Tooling ✅
@ -146,7 +146,7 @@ Real-time collaboration, prototyping, comments, desktop distribution.
- Prototyping (frame connections, transitions, animations)
- Comments (pin, threads, resolve)
- PWA support
- Variant switching, FLOAT/STRING/BOOLEAN variable UI, variable-driven theming
- Variant switching, `FLOAT`/STRING/BOOLEAN variable UI, variable-driven theming
- Full Figma compatibility test suite
## Timeline

View file

@ -4,7 +4,7 @@
### Fase 1: Motor Core ✅
SceneGraph, renderizado Skia, formas básicas, selección, zoom/pan, deshacer/rehacer, guías de ajuste.
`SceneGraph`, renderizado Skia, formas básicas, selección, zoom/pan, deshacer/rehacer, guías de ajuste.
### Fase 2: UI del Editor + Layout ✅
@ -16,7 +16,7 @@ Importación/exportación .fig, codec Kiwi, portapapeles, herramienta de pluma,
### Fase 4: Componentes + Variables ✅
Componentes, instancias, overrides, conjuntos de componentes, variables (COLOR/FLOAT/STRING/BOOLEAN), colecciones, modos, exportación de imágenes, menú contextual, formateo de texto enriquecido.
Componentes, instancias, overrides, conjuntos de componentes, variables (`COLOR`/FLOAT/STRING/BOOLEAN), colecciones, modos, exportación de imágenes, menú contextual, formateo de texto enriquecido.
### Fase 5: Integración IA & Herramientas ✅
@ -53,7 +53,7 @@ Componentes, instancias, overrides, conjuntos de componentes, variables (COLOR/F
- Prototipado (conexiones de frames, transiciones, animaciones)
- Comentarios (pin, hilos, resolver)
- Soporte PWA
- Cambio de variantes, UI de variables FLOAT/STRING/BOOLEAN, theming por variables
- Cambio de variantes, UI de variables `FLOAT`/STRING/BOOLEAN, theming por variables
## Cronograma

View file

@ -1,77 +0,0 @@
# `open-pencil eval` — API de Plugin compatible con Figma para scripting headless
## Visión general
`bun open-pencil eval <file> --code '<js>'` ejecuta JavaScript contra un archivo `.fig` con un objeto global `figma` compatible con Figma. Esto permite scripting headless, operaciones por lotes, ejecución de herramientas IA y pruebas — todo sin interfaz gráfica.
El objeto `figma` refleja la superficie de la API de Plugin de Figma lo más fielmente posible, por lo que el conocimiento existente sobre plugins de Figma y los fragmentos de código son directamente transferibles.
```bash
# Crear un marco, configurar auto-layout, añadir hijos
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
return { id: frame.id, name: frame.name }
'
# Consultar nodos
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name }))
'
# Escribir los cambios
bun open-pencil eval design.fig --code '...' --write
```
## Arquitectura
```
CLI: open-pencil eval <file> --code '...'
→ loadDocument(file) → SceneGraph
→ FigmaAPI(sceneGraph) → proxy `figma`
→ AsyncFunction('figma', code)(figmaProxy)
→ imprimir resultado / guardar con --write
```
### Clases principales
| Clase | Ubicación | Rol |
|-------|-----------|-----|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Objeto proxy que implementa métodos `figma.*` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy que envuelve `SceneNode` con acceso a propiedades estilo Figma |
| Comando `eval` | `packages/cli/src/commands/eval.ts` | Carga documento, crea API, ejecuta código |
### ¿Por qué en `@open-pencil/core`?
La clase `FigmaAPI` vive en core porque: las herramientas IA la reutilizan, los tests la usan y no tiene dependencias DOM.
## Comando CLI
```
bun open-pencil eval <file> [opciones]
Argumentos:
file Archivo .fig sobre el que operar
Opciones:
--code, -c Código JavaScript a ejecutar
--stdin Leer código desde stdin
--write, -w Escribir cambios en el archivo de entrada
-o, --output Escribir en un archivo diferente
--json Salida como JSON
--quiet, -q Suprimir la salida
```
## Implementación por fases
- **Fase 1: Core** — creación de nodos, propiedades, operaciones de árbol, auto-layout, texto (~80% de scripts reales)
- **Fase 2: Componentes e Instancias** — createComponent, createInstance, detachInstance
- **Fase 3: Variables** — getLocalVariables, createVariable, setBoundVariable
- **Fase 4: Estilos y Avanzado** — estilos paint/texto/efectos, operaciones booleanas
[Referencia API completa en inglés](/eval-command)

View file

@ -2,7 +2,7 @@
## Vista general del sistema
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Diseño del editor
@ -62,7 +62,7 @@ Yoga de Meta proporciona cálculo de layout CSS flexbox. Un adaptador delgado ma
### Formato de archivo (Kiwi binario)
Reutiliza el códec binario Kiwi de Figma con 194 definiciones de mensaje/enum/struct. Importación: parsear cabecera → descomprimir Zstd → decodificar Kiwi → NodeChange[] → grafo de escena. La exportación invierte el proceso con generación de miniatura.
Reutiliza el códec binario Kiwi de Figma con 194 definiciones de mensaje/enum/struct. Importación: parsear cabecera → descomprimir Zstd → decodificar Kiwi → `NodeChange`[] → grafo de escena. La exportación invierte el proceso con generación de miniatura.
Véase [Referencia del formato de archivo](/reference/file-format) para más detalles.

View file

@ -242,7 +242,7 @@ El enfoque de Open Pencil es más simple y con menos overhead.
## 11. Scripting y extensibilidad
OpenPencil incluye un [comando `eval`](/eval-command) que proporciona una API de Plugin compatible con Figma para scripting headless. Además, 90 herramientas AI disponibles vía chat integrado, servidor MCP (stdio + HTTP) y CLI. Penpot tiene sistema de plugins con ejecución sandboxed pero sin API de scripting headless ni integración MCP.
OpenPencil incluye un [comando `eval`](/programmable/cli/scripting) que proporciona una API de Plugin compatible con Figma para scripting headless. Además, 90 herramientas AI disponibles vía chat integrado, servidor MCP (stdio + HTTP) y CLI. Penpot tiene sistema de plugins con ejecución sandboxed pero sin API de scripting headless ni integración MCP.
## Resumen

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
Consulta la [referencia de herramientas MCP](/reference/mcp-tools) para la lista completa.
Consulta la [referencia de herramientas MCP](/programmable/mcp-server) para la lista completa.
## CLI

View file

@ -126,7 +126,7 @@ Comparación característica por característica de las capacidades de Figma Des
| Desenfoque de fondo | ✅ | Desenfocar contenido detrás de la capa |
| Desenfoque de primer plano | ✅ | Desenfoque en primer plano |
| Grosor de trazo | ✅ | Configurable en panel de propiedades |
| Cap de trazo (round, square, arrow) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Cap de trazo (round, square, arrow) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Join de trazo (miter, bevel, round) | ✅ | Los tres tipos de join |
| Patrones de guiones | ✅ | Patrón de trazo dash-on/dash-off |
| Radio de esquina | ✅ | Radio uniforme y por esquina con toggle independiente |
@ -160,7 +160,7 @@ Comparación característica por característica de las capacidades de Figma Des
| Variantes | 🔲 | Cambio de variante y selección por propiedades |
| Propiedades de componente | 🔲 | Propiedades booleanas, texto, intercambio de instancia |
| Propagación de overrides | ✅ | Cambios en componente principal se propagan; overrides preservados |
| Variables (color, número, string, booleano) | 🟡 | COLOR con UI completa; FLOAT/STRING/BOOLEAN definidos sin UI de edición |
| Variables (color, número, string, booleano) | 🟡 | `COLOR` con UI completa; `FLOAT`/STRING/BOOLEAN definidos sin UI de edición |
| Colecciones y modos de variables | 🟡 | Colecciones, modos, cambio activeMode funcionan; sin UI de theming por variable |
| Estilos (color, texto, efecto, layout) | 🔲 | Presets de estilo reutilizables con nombre |
| Bibliotecas (publicar, compartir, actualizar) | 🔲 | Bibliotecas compartidas de componentes/estilos |
@ -187,7 +187,7 @@ Comparación característica por característica de las capacidades de Figma Des
| Característica | Estado | Notas |
|---------------|--------|-------|
| Import de archivo .fig | ✅ | Codec Kiwi completo: 194 definiciones, ~390 campos por NodeChange |
| Import de archivo .fig | ✅ | Codec Kiwi completo: 194 definiciones, ~390 campos por `NodeChange` |
| Export de archivo .fig | ✅ | Codificación Kiwi + compresión Zstd + generación de miniatura |
| Guardar / Guardar como | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; diálogos nativos (Tauri), File System Access API (Chrome/Edge), fallback de descarga (Safari) |
| Portapapeles de Figma (pegar) | ✅ | Decodificar binario Kiwi del portapapeles de Figma |

View file

@ -1,71 +1,94 @@
# Formato de archivo
# File Format
## Estructura de archivos .fig
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Pipeline de importación
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Pipeline de exportación
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,150 +0,0 @@
# Servidor MCP
OpenPencil incluye un servidor MCP (Model Context Protocol) que permite a las herramientas de coding IA — Claude Code, Cursor, Windsurf etc. — leer y modificar archivos .fig headless.
Dos transportes
## **stdio** para clientes MCP, **HTTP** para todo lo demás.
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
O ejecutar desde el código fuente:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Starts on port 3100 (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
Install the OpenPencil skill for your AI coding agent:
```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).
## Tools (75)
### 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_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
### 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 |
### 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 |
### 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 |

View file

@ -1,4 +1,4 @@
# Tipos de nodo
# 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 17 of these types.
@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -1,8 +1,8 @@
# Grafo de escena
# Scene Graph
## Representación en memoria
## In-Memory Representation
Los nodos viven en un Map plano `Map<string, Node>` keyed by GUID string. La estructura de árbol se mantiene via `parentIndex` references. This gives Búsqueda O(1) by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -1,437 +0,0 @@
# `open-pencil eval` — Figma-like Plugin API for Headless Scripting
## Overview
`bun open-pencil eval <file> --code '<js>'` executes JavaScript against a `.fig` file with a Figma-compatible `figma` global object. This enables headless scripting, batch operations, AI tool execution, and testing — all without the GUI.
The `figma` object mirrors Figma's Plugin API surface as closely as possible, so existing Figma plugin knowledge and code snippets transfer directly.
```bash
# Create a frame, set auto-layout, add children
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.paddingTop = frame.paddingBottom = 16
frame.paddingLeft = frame.paddingRight = 16
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
const title = figma.createText()
title.characters = "Hello World"
title.fontSize = 24
frame.appendChild(title)
return { id: frame.id, name: frame.name }
'
# Query nodes
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.type === "FRAME" && n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name, w: b.width, h: b.height }))
'
# Read from stdin (for multiline scripts / piping)
cat transform.js | bun open-pencil eval design.fig --stdin
# Write changes back
bun open-pencil eval design.fig --code '...' --write
bun open-pencil eval design.fig --code '...' -o modified.fig
```
## Architecture
```
┌──────────────────────────────────────────────────────┐
│ CLI: `open-pencil eval <file> --code '...'` │
│ ↓ │
│ loadDocument(file) → SceneGraph │
│ ↓ │
│ FigmaAPI(sceneGraph) → `figma` proxy object │
│ ↓ │
│ AsyncFunction('figma', wrappedCode)(figmaProxy) │
│ ↓ │
│ print result as JSON / agentfmt │
│ optionally: saveDocument(file) if --write │
└──────────────────────────────────────────────────────┘
```
### Key classes
| Class | Location | Role |
|-------|----------|------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Proxy object implementing `figma.*` methods against `SceneGraph` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy wrapping `SceneNode` with Figma-style property access (`.fills`, `.resize()`, `.appendChild()`, etc.) |
| `eval` command | `packages/cli/src/commands/eval.ts` | CLI command that loads doc, creates API, executes code |
### Why in `@open-pencil/core`?
The `FigmaAPI` class lives in core (not CLI) because:
- **AI tools reuse it** — the chat panel's `render` tool can execute JSX through the same API
- **Test scripts** — unit tests can use the API to set up fixtures
- **No DOM deps** — runs headless in Bun, no browser APIs needed
## `FigmaAPI` — Phased Implementation
### Phase 1: Core (MVP for eval command)
These cover ~80% of real plugin scripts:
#### Document & Page
| Figma API | Our implementation | Notes |
|-----------|--------------------|-------|
| `figma.root` | Getter → proxy for root node | `.children` returns page proxies |
| `figma.currentPage` | Getter/setter → first page by default | Settable to any page proxy |
| `figma.currentPage.selection` | Get/set → tracked selection array | |
| `figma.getNodeById(id)` | `graph.getNode(id)` wrapped in proxy | Sync, like Figma's deprecated version |
#### Node Creation
| Figma API | Maps to |
|-----------|---------|
| `figma.createFrame()` | `graph.createNode('FRAME', currentPageId)` |
| `figma.createRectangle()` | `graph.createNode('RECTANGLE', ...)` |
| `figma.createEllipse()` | `graph.createNode('ELLIPSE', ...)` |
| `figma.createText()` | `graph.createNode('TEXT', ...)` |
| `figma.createLine()` | `graph.createNode('LINE', ...)` |
| `figma.createPolygon()` | `graph.createNode('POLYGON', ...)` |
| `figma.createStar()` | `graph.createNode('STAR', ...)` |
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
| `figma.createPage()` | `graph.addPage(name)` |
| `figma.createSection()` | `graph.createNode('SECTION', ...)` |
#### Node Properties (via `FigmaNode` proxy)
Read/write on any node proxy. Property access maps to `SceneNode` fields:
```ts
// Geometry
node.x, node.y // direct
node.width, node.height // read-only, use node.resize(w, h)
node.rotation // direct
node.resize(w, h) // updates width + height
node.resizeWithoutConstraints(w, h) // same (no constraint engine yet)
// Visual
node.fills // get/set Fill[]
node.strokes // get/set Stroke[]
node.effects // get/set Effect[]
node.opacity // get/set number
node.visible // get/set boolean
node.locked // get/set boolean
node.blendMode // get/set BlendMode
node.clipsContent // get/set boolean
// Corner radius
node.cornerRadius // get/set (number or figma.mixed)
node.topLeftRadius // get/set
node.topRightRadius // get/set
node.bottomLeftRadius // get/set
node.bottomRightRadius // get/set
node.cornerSmoothing // get/set
// Identity
node.id // read-only
node.name // get/set
node.type // read-only
node.parent // read-only → FigmaNode | null
node.removed // read-only boolean
```
#### Tree Operations
```ts
node.children // read-only FigmaNode[]
node.appendChild(child) // reparent to end
node.insertChild(index, child) // reparent at index
node.remove() // graph.deleteNode(id)
// Traversal
node.findAll(callback?) // recursive find
node.findOne(callback) // first match
node.findChild(callback) // direct children only
node.findChildren(callback?) // direct children only
```
#### Auto-layout
```ts
node.layoutMode // 'NONE' | 'HORIZONTAL' | 'VERTICAL'
node.primaryAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
node.counterAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
node.itemSpacing // number
node.counterAxisSpacing // number | null
node.paddingTop / Right / Bottom / Left // number
node.layoutWrap // 'NO_WRAP' | 'WRAP'
// Child sizing
node.layoutPositioning // 'AUTO' | 'ABSOLUTE'
node.layoutGrow // 0 | 1
node.layoutSizingHorizontal // 'FIXED' | 'HUG' | 'FILL'
node.layoutSizingVertical // 'FIXED' | 'HUG' | 'FILL'
```
#### Text
```ts
node.characters // get/set (maps to node.text)
node.fontSize // get/set
node.fontName // get/set { family, style }
node.fontWeight // get/set
node.textAlignHorizontal // get/set
node.textAlignVertical // get/set
node.textAutoResize // get/set
node.letterSpacing // get/set
node.lineHeight // get/set
node.maxLines // get/set
node.textCase // get/set
node.textDecoration // get/set
```
#### Stroke details
```ts
node.strokeWeight // get/set (maps to strokes[0].weight)
node.strokeAlign // get/set (maps to strokes[0].align)
node.dashPattern // get/set
```
#### Misc
```ts
figma.mixed // Symbol sentinel for mixed values
figma.group(nodes, parent) // creates GROUP with given children
figma.ungroup(node) // ungroups, reparents children
figma.flatten(nodes) // NOT IMPLEMENTED YET — returns first node
```
#### Export
```ts
node.exportAsync(settings?) // only works if CanvasKit is loaded
// settings: { format: 'PNG'|'JPG'|'SVG', constraint? }
```
### Phase 2: Components & Instances
| API | Maps to |
|-----|---------|
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
| `figma.createComponentFromNode(node)` | Convert existing frame to component |
| `figma.combineAsVariants(components, parent)` | Create COMPONENT_SET |
| Node: `node.createInstance()` | `graph.createInstance(componentId, parentId)` |
| Node: `node.detachInstance()` | `graph.detachInstance(id)` |
| `figma.getNodeById(id).mainComponent` | `graph.getMainComponent(id)` |
### Phase 3: Variables
| API | Maps to |
|-----|---------|
| `figma.variables.getLocalVariables(type?)` | `graph.variables` filtered |
| `figma.variables.getLocalVariableCollections()` | `graph.variableCollections` |
| `figma.variables.createVariable(name, collection, type)` | `graph.addVariable(...)` |
| `figma.variables.createVariableCollection(name)` | `graph.addCollection(...)` |
| `figma.variables.getVariableById(id)` | `graph.variables.get(id)` |
| `node.setBoundVariable(field, variable)` | `graph.bindVariable(...)` |
| `node.boundVariables` | getter from SceneNode |
### Phase 4: Styles & Advanced
| API | Notes |
|-----|-------|
| `figma.createPaintStyle()` | Requires style storage in SceneGraph |
| `figma.createTextStyle()` | Requires style storage in SceneGraph |
| `figma.createEffectStyle()` | Requires style storage in SceneGraph |
| `figma.loadFontAsync(fontName)` | No-op (we don't have font loading constraints) |
| `figma.listAvailableFontsAsync()` | Return system fonts if available |
| Boolean operations (`union`, `subtract`, `intersect`, `exclude`) | Requires path boolean engine |
| `figma.createNodeFromJSXAsync(jsx)` | Port figma-use's JSX renderer |
## `FigmaNode` Proxy Design
The proxy wraps a `SceneNode` and translates Figma property names to our internal names. Key mappings:
```ts
const PROPERTY_MAP: Record<string, string> = {
// Figma name → SceneNode field name (only where they differ)
'characters': 'text',
'strokeWeight': → computed from strokes[0].weight,
'strokeAlign': → computed from strokes[0].align,
'fontName': → computed from { family: fontFamily, style: ... },
'primaryAxisAlignItems': 'primaryAxisAlign',
'counterAxisAlignItems': 'counterAxisAlign',
'primaryAxisSizingMode': 'primaryAxisSizing', // value mapping: 'AUTO' → 'HUG', 'FIXED' → 'FIXED'
'counterAxisSizingMode': 'counterAxisSizing',
'layoutSizingHorizontal': → computed from primaryAxisSizing / counterAxisSizing depending on layoutMode
'layoutSizingVertical': → computed
}
```
Methods on the proxy:
```ts
class FigmaNode {
// The proxy is created via: new Proxy(target, handler)
// where handler.get intercepts property reads and handler.set intercepts writes
resize(width: number, height: number): void
resizeWithoutConstraints(width: number, height: number): void
remove(): void
appendChild(child: FigmaNode): void
insertChild(index: number, child: FigmaNode): void
findAll(callback?: (node: FigmaNode) => boolean): FigmaNode[]
findOne(callback: (node: FigmaNode) => boolean): FigmaNode | null
findChild(callback: (node: FigmaNode) => boolean): FigmaNode | null
findChildren(callback?: (node: FigmaNode) => boolean): FigmaNode[]
exportAsync(settings?: ExportSettings): Promise<Uint8Array>
// Components (Phase 2)
createInstance(): FigmaNode
detachInstance(): void
get mainComponent(): FigmaNode | null
}
```
## CLI Command
```
bun open-pencil eval <file> [options]
Arguments:
file .fig file to operate on
Options:
--code, -c JavaScript code to execute (has access to `figma` global)
--stdin Read code from stdin instead of --code
--write, -w Write changes back to the input file
-o, --output Write to a different file
--json Output result as JSON (default for non-TTY)
--quiet, -q Suppress output, only write file
```
### Execution model
1. Load `.fig` → `SceneGraph`
2. Create `FigmaAPI(graph)` → `figma` proxy
3. Wrap user code in async function: `return (async () => { <code> })()`
4. Execute with `figma` as sole argument
5. Print return value (JSON or agentfmt)
6. If `--write` or `-o`: serialize `SceneGraph` back to `.fig`
### Return value formatting
- `undefined` / `void` → no output
- Primitives → printed directly
- Objects/arrays → `JSON.stringify(result, null, 2)` or agentfmt tables
- `FigmaNode` → serialized as `{ id, type, name, x, y, width, height, fills, ... }`
- Arrays of `FigmaNode` → serialized as list
## Shared with AI Tools
The `FigmaAPI` class is the **same API surface** that AI tools use. Currently `src/ai/tools.ts` calls `store.createShape()`, `store.updateNodeWithUndo()`, etc. — these should be refactored to go through `FigmaAPI`:
```ts
// Before (current AI tools)
execute: async ({ type, x, y, width, height }) => {
const id = store.createShape(type, x, y, width, height)
return { id }
}
// After (using FigmaAPI)
execute: async ({ type, x, y, width, height }) => {
const frame = figma.createFrame()
frame.resize(width, height)
frame.x = x
frame.y = y
return { id: frame.id }
}
```
This ensures CLI scripts and AI tools behave identically.
## File Layout
```
packages/core/src/
figma-api.ts # FigmaAPI class + FigmaNode proxy (Phase 1–4)
figma-api.test.ts # Unit tests against headless SceneGraph
packages/cli/src/commands/
eval.ts # CLI command
packages/cli/src/commands/eval.test.ts # Integration tests
```
## Test Plan
### Unit tests (`packages/core/src/figma-api.test.ts`)
1. **Node creation** — each `createX()` creates correct type, added to current page
2. **Property access** — `.fills`, `.x`, `.width`, `.name`, `.characters` read/write correctly
3. **Resize** — `.resize(w, h)` updates width/height
4. **Tree operations** — `.appendChild()`, `.insertChild()`, `.remove()`, `.parent`, `.children`
5. **Traversal** — `.findAll()`, `.findOne()`, `.findChild()`, `.findChildren()` with callbacks
6. **Auto-layout** — `.layoutMode`, `.itemSpacing`, `.paddingTop`, etc.
7. **Text** — `.characters` maps to `.text`, `.fontName` maps to `{ family, style }`
8. **Mixed values** — `.cornerRadius` returns `figma.mixed` when corners differ
9. **Selection** — `figma.currentPage.selection` get/set
10. **Page switching** — `figma.currentPage = page2` works
11. **Group/ungroup** — `figma.group()` creates group, `figma.ungroup()` dissolves it
12. **Clone** — node creation produces independent copies
### CLI integration tests (`packages/cli/src/commands/eval.test.ts`)
1. **Basic eval** — `eval test.fig --code 'return figma.currentPage.name'` → page name
2. **Create + read** — create a frame, return its properties
3. **Query nodes** — `findAll` returns correct nodes
4. **Write back** — `--write` saves changes, reloading shows them
5. **Stdin** — `echo 'return 42' | bun open-pencil eval test.fig --stdin` → `42`
6. **JSON output** — `--json` returns valid JSON
7. **Error handling** — syntax errors, runtime errors reported cleanly
## Implementation Order
1. **`FigmaNode` proxy** — property mapping, `.resize()`, `.remove()`, tree methods
2. **`FigmaAPI` class** — `createFrame/Rectangle/...`, `.root`, `.currentPage`, `.getNodeById()`, `.mixed`, `.group()`
3. **CLI `eval` command** — argument parsing, code wrapping, output formatting
4. **Unit tests** — all 12 test groups above
5. **CLI integration tests** — all 7 test groups above
6. **Wire to AI tools** — refactor `src/ai/tools.ts` to use `FigmaAPI` where possible
7. **Phase 2** — components & instances
8. **Phase 3** — variables
9. **Phase 4** — styles, boolean ops, JSX renderer
## Property Mapping Reference
| Figma Property | SceneNode Field | Type | Notes |
|---------------|-----------------|------|-------|
| `characters` | `text` | `string` | |
| `fontName` | `fontFamily` + `fontWeight` + `italic` | `{ family, style }` | Computed: `style` = "Bold Italic" etc. |
| `strokeWeight` | `strokes[0].weight` | `number` | Computed |
| `strokeAlign` | `strokes[0].align` | `string` | Computed |
| `primaryAxisAlignItems` | `primaryAxisAlign` | `string` | |
| `counterAxisAlignItems` | `counterAxisAlign` | `string` | |
| `layoutSizingHorizontal` | `primaryAxisSizing` or `counterAxisSizing` | `string` | Depends on `layoutMode` |
| `layoutSizingVertical` | (opposite of horizontal) | `string` | |
| `absoluteTransform` | computed from `x`, `y`, `rotation` | `Transform` | Read-only |
| `absoluteBoundingBox` | `getAbsoluteBounds(id)` | `Rect` | Read-only |
| All others | Same name | Same type | Direct passthrough |
## Open Questions
1. **Font loading**: `figma.loadFontAsync()` — should it be a no-op (we don't have font gating) or should we track loaded fonts?
→ **Decision: No-op that returns resolved Promise.** We don't gate text editing on font loading.
2. **Export in headless mode**: `node.exportAsync()` requires CanvasKit. Should eval load CanvasKit?
→ **Decision: Optional.** If CanvasKit is available (via `--with-canvaskit` flag or env), enable export. Otherwise, throw "Export requires CanvasKit" error.
3. **`figma.mixed` symbol**: Should we use the actual Figma symbol or our own?
→ **Decision: Our own `Symbol('mixed')`.** Exposed as `figma.mixed`.
4. **Undo**: `figma.commitUndo()` / `figma.triggerUndo()` — relevant in headless?
→ **Decision: No-op in CLI.** Undo only matters in the live editor. The AI tools can add undo support separately via EditorStore.
5. **Write format**: Should `--write` produce `.fig` (Kiwi binary) or also support `.json`?
→ **Decision: `.fig` only for now.** JSON export is a separate feature.

View file

@ -4,7 +4,7 @@
### Phase 1 : Moteur Core ✅
SceneGraph, rendu Skia, formes de base, sélection, zoom/pan, annuler/rétablir, guides d'alignement.
`SceneGraph`, rendu Skia, formes de base, sélection, zoom/pan, annuler/rétablir, guides d'alignement.
### Phase 2 : UI Éditeur + Layout ✅
@ -16,7 +16,7 @@ Import/export .fig, codec Kiwi, presse-papiers, outil plume, réseaux vectoriels
### Phase 4 : Composants + Variables ✅
Composants, instances, surcharges, jeux de composants, variables (COLOR/FLOAT/STRING/BOOLEAN), collections, modes, export d'images, menu contextuel, formatage de texte riche.
Composants, instances, surcharges, jeux de composants, variables (`COLOR`/FLOAT/STRING/BOOLEAN), collections, modes, export d'images, menu contextuel, formatage de texte riche.
### Phase 5 : Intégration IA & Outils ✅
@ -53,7 +53,7 @@ Composants, instances, surcharges, jeux de composants, variables (COLOR/FLOAT/ST
- Prototypage (connexions de frames, transitions, animations)
- Commentaires (pin, fils, résoudre)
- Support PWA
- Changement de variantes, UI variables FLOAT/STRING/BOOLEAN, theming par variables
- Changement de variantes, UI variables `FLOAT`/STRING/BOOLEAN, theming par variables
## Calendrier

View file

@ -1,77 +0,0 @@
# `open-pencil eval` — API Plugin compatible Figma pour le scripting headless
## Vue d'ensemble
`bun open-pencil eval <file> --code '<js>'` exécute du JavaScript sur un fichier `.fig` avec un objet global `figma` compatible Figma. Cela permet le scripting headless, les opérations par lots, l'exécution d'outils IA et les tests — le tout sans interface graphique.
L'objet `figma` reflète la surface de l'API Plugin de Figma aussi fidèlement que possible, de sorte que les connaissances existantes sur les plugins Figma et les extraits de code sont directement transférables.
```bash
# Créer un cadre, définir l'auto-layout, ajouter des enfants
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
return { id: frame.id, name: frame.name }
'
# Interroger les nœuds
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name }))
'
# Écrire les modifications
bun open-pencil eval design.fig --code '...' --write
```
## Architecture
```
CLI: open-pencil eval <file> --code '...'
→ loadDocument(file) → SceneGraph
→ FigmaAPI(sceneGraph) → proxy `figma`
→ AsyncFunction('figma', code)(figmaProxy)
→ afficher résultat / sauvegarder avec --write
```
### Classes principales
| Classe | Emplacement | Rôle |
|--------|-------------|------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Objet proxy implémentant les méthodes `figma.*` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy enveloppant `SceneNode` avec accès propriétés style Figma |
| Commande `eval` | `packages/cli/src/commands/eval.ts` | Charge le document, crée l'API, exécute le code |
### Pourquoi dans `@open-pencil/core` ?
La classe `FigmaAPI` vit dans core car : les outils IA la réutilisent, les tests l'utilisent et elle n'a pas de dépendances DOM.
## Commande CLI
```
bun open-pencil eval <file> [options]
Arguments :
file Fichier .fig sur lequel opérer
Options :
--code, -c Code JavaScript à exécuter
--stdin Lire le code depuis stdin
--write, -w Écrire les modifications dans le fichier d'entrée
-o, --output Écrire dans un fichier différent
--json Sortie en JSON
--quiet, -q Supprimer la sortie
```
## Implémentation par phases
- **Phase 1 : Core** — création de nœuds, propriétés, opérations d'arbre, auto-layout, texte (~80% des scripts réels)
- **Phase 2 : Composants & Instances** — createComponent, createInstance, detachInstance
- **Phase 3 : Variables** — getLocalVariables, createVariable, setBoundVariable
- **Phase 4 : Styles & Avancé** — styles paint/texte/effets, opérations booléennes
[Référence API complète en anglais](/eval-command)

View file

@ -2,7 +2,7 @@
## Vue d'ensemble du système
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Disposition de l'éditeur
@ -62,7 +62,7 @@ Yoga de Meta fournit le calcul de layout CSS flexbox. Un adaptateur fin mappe le
### Format de fichier (Kiwi binaire)
Réutilise le codec binaire Kiwi de Figma avec 194 définitions de message/enum/struct. Import : analyser l'en-tête → décompresser Zstd → décoder Kiwi → NodeChange[] → graphe de scène. L'export inverse le processus avec génération de miniature.
Réutilise le codec binaire Kiwi de Figma avec 194 définitions de message/enum/struct. Import : analyser l'en-tête → décompresser Zstd → décoder Kiwi → `NodeChange`[] → graphe de scène. L'export inverse le processus avec génération de miniature.
Voir la [Référence du format de fichier](/reference/file-format) pour plus de détails.

View file

@ -229,7 +229,7 @@ Gestion d'état via Potok (bibliothèque Redux-like pour atomes ClojureScript).
## 11. Scripting et extensibilité
OpenPencil inclut une [commande `eval`](/eval-command) offrant une API Plugin compatible Figma pour le scripting headless. De plus, 90 outils IA sont disponibles via le chat intégré, le serveur MCP (stdio + HTTP) et le CLI. Penpot a un système de plugins avec exécution sandboxée mais pas d'API de scripting headless ni d'intégration MCP.
OpenPencil inclut une [commande `eval`](/programmable/cli/scripting) offrant une API Plugin compatible Figma pour le scripting headless. De plus, 90 outils IA sont disponibles via le chat intégré, le serveur MCP (stdio + HTTP) et le CLI. Penpot a un système de plugins avec exécution sandboxée mais pas d'API de scripting headless ni d'intégration MCP.
## Résumé

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
Voir la [référence des outils MCP](/reference/mcp-tools) pour la liste complète.
Voir la [référence des outils MCP](/programmable/mcp-server) pour la liste complète.
## CLI

View file

@ -126,7 +126,7 @@ Comparaison fonctionnalité par fonctionnalité des capacités de Figma Design a
| Flou d'arrière-plan | ✅ | Flouter le contenu derrière le calque |
| Flou de premier plan | ✅ | Flou au premier plan |
| Épaisseur du contour | ✅ | Configurable dans le panneau de propriétés |
| Extrémité du contour (round, square, arrow) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Extrémité du contour (round, square, arrow) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Jointure du contour (miter, bevel, round) | ✅ | Les trois types de jointure |
| Motifs de tirets | ✅ | Motif de contour dash-on/dash-off |
| Rayon de coin | ✅ | Rayon uniforme et par coin avec toggle indépendant |
@ -160,7 +160,7 @@ Comparaison fonctionnalité par fonctionnalité des capacités de Figma Design a
| Variantes | 🔲 | Changement de variante et sélection par propriétés |
| Propriétés de composant | 🔲 | Propriétés booléennes, texte, échange d'instance |
| Propagation des surcharges | ✅ | Changements du composant principal propagés ; surcharges préservées |
| Variables (couleur, nombre, chaîne, booléen) | 🟡 | COLOR avec UI complète ; FLOAT/STRING/BOOLEAN définis sans UI d'édition |
| Variables (couleur, nombre, chaîne, booléen) | 🟡 | `COLOR` avec UI complète ; `FLOAT`/STRING/BOOLEAN définis sans UI d'édition |
| Collections et modes de variables | 🟡 | Collections, modes, changement activeMode fonctionnent ; pas d'UI de thématisation |
| Styles (couleur, texte, effet, layout) | 🔲 | Presets de style réutilisables nommés |
| Bibliothèques (publier, partager, mettre à jour) | 🔲 | Bibliothèques partagées de composants/styles |
@ -187,7 +187,7 @@ Comparaison fonctionnalité par fonctionnalité des capacités de Figma Design a
| Fonctionnalité | Statut | Notes |
|---------------|--------|-------|
| Import de fichier .fig | ✅ | Codec Kiwi complet : 194 définitions, ~390 champs par NodeChange |
| Import de fichier .fig | ✅ | Codec Kiwi complet : 194 définitions, ~390 champs par `NodeChange` |
| Export de fichier .fig | ✅ | Encodage Kiwi + compression Zstd + génération de miniature |
| Enregistrer / Enregistrer sous | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> ; dialogues natifs (Tauri), File System Access API (Chrome/Edge), téléchargement (Safari) |
| Presse-papiers Figma (coller) | ✅ | Décoder binaire Kiwi du presse-papiers Figma |

View file

@ -1,71 +1,94 @@
# Format de fichier
# File Format
## Structure des fichiers .fig
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Pipeline d'importation
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Pipeline d'exportation
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,150 +0,0 @@
# Serveur MCP
OpenPencil inclut un serveur MCP (Model Context Protocol) qui permet aux outils de coding IA — Claude Code, Cursor, Windsurf etc. — de lire et modifier des fichiers .fig en headless.
Deux transports
## **stdio** pour les clients MCP, **HTTP** pour tout le reste.
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
Ou exécuter depuis les sources :
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Starts on port 3100 (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
Install the OpenPencil skill for your AI coding agent:
```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).
## Tools (75)
### 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_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
### 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 |
### 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 |
### 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 |

View file

@ -1,4 +1,4 @@
# Types de nœuds
# 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 17 of these types.
@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -1,8 +1,8 @@
# Graphe de scène
# Scene Graph
## Représentation en mémoire
## In-Memory Representation
Les nœuds vivent dans une Map plate `Map<string, Node>` keyed by GUID string. La structure arborescente est maintenue via `parentIndex` references. This gives Recherche O(1) by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -2,7 +2,7 @@
## System Overview
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Editor Layout
@ -62,7 +62,7 @@ Meta's Yoga provides CSS flexbox layout computation. A thin adapter maps Figma p
### 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.
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.

View file

@ -272,7 +272,7 @@ Open Pencil's approach is simpler and lower overhead. Penpot's approach is more
## 11. Scripting & Extensibility
OpenPencil ships with an [`eval` command](/eval-command) 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.
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

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
See [MCP Tools reference](/reference/mcp-tools) for the full tool list.
See [MCP Tools reference](/programmable/mcp-server) for the full tool list.
## CLI

View file

@ -126,7 +126,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| 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 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 |
@ -162,7 +162,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| 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 |
| 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 |
@ -189,8 +189,8 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| 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 |
| .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 | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; 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 |

View file

@ -4,7 +4,7 @@
### Fase 1: Motore Core ✅
SceneGraph, rendering Skia, forme base, selezione, zoom/pan, annulla/ripristina, guide di snap.
`SceneGraph`, rendering Skia, forme base, selezione, zoom/pan, annulla/ripristina, guide di snap.
### Fase 2: UI Editor + Layout ✅
@ -16,7 +16,7 @@ Import/export .fig, codec Kiwi, appunti, strumento penna, reti vettoriali, grupp
### Fase 4: Componenti + Variabili ✅
Componenti, istanze, override, set di componenti, variabili (COLOR/FLOAT/STRING/BOOLEAN), collezioni, modalità, export immagini, menu contestuale, formattazione testo ricco.
Componenti, istanze, override, set di componenti, variabili (`COLOR`/FLOAT/STRING/BOOLEAN), collezioni, modalità, export immagini, menu contestuale, formattazione testo ricco.
### Fase 5: Integrazione IA & Strumenti ✅
@ -53,7 +53,7 @@ Componenti, istanze, override, set di componenti, variabili (COLOR/FLOAT/STRING/
- Prototipazione (connessioni frame, transizioni, animazioni)
- Commenti (pin, thread, risolvere)
- Supporto PWA
- Cambio varianti, UI variabili FLOAT/STRING/BOOLEAN, theming tramite variabili
- Cambio varianti, UI variabili `FLOAT`/STRING/BOOLEAN, theming tramite variabili
## Tempistica

View file

@ -1,77 +0,0 @@
# `open-pencil eval` — API Plugin Figma-compatibile per scripting headless
## Panoramica
`bun open-pencil eval <file> --code '<js>'` esegue JavaScript su un file `.fig` con un oggetto globale `figma` compatibile con Figma. Questo abilita scripting headless, operazioni batch, esecuzione di strumenti IA e test — tutto senza la GUI.
L'oggetto `figma` rispecchia la superficie dell'API Plugin di Figma il più fedelmente possibile, così le conoscenze esistenti sui plugin Figma e gli snippet di codice sono direttamente trasferibili.
```bash
# Creare un frame, impostare auto-layout, aggiungere figli
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
return { id: frame.id, name: frame.name }
'
# Interrogare nodi
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name }))
'
# Scrivere le modifiche
bun open-pencil eval design.fig --code '...' --write
```
## Architettura
```
CLI: open-pencil eval <file> --code '...'
→ loadDocument(file) → SceneGraph
→ FigmaAPI(sceneGraph) → proxy `figma`
→ AsyncFunction('figma', code)(figmaProxy)
→ stampa risultato / salva file con --write
```
### Classi principali
| Classe | Posizione | Ruolo |
|--------|-----------|-------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Oggetto proxy che implementa metodi `figma.*` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy che avvolge `SceneNode` con accesso proprietà stile Figma |
| Comando `eval` | `packages/cli/src/commands/eval.ts` | Carica documento, crea API, esegue codice |
### Perché in `@open-pencil/core`?
La classe `FigmaAPI` è nel core perché: gli strumenti IA la riutilizzano, i test possono usarla e non ha dipendenze DOM.
## Comando CLI
```
bun open-pencil eval <file> [opzioni]
Argomenti:
file File .fig su cui operare
Opzioni:
--code, -c Codice JavaScript da eseguire
--stdin Leggere codice da stdin
--write, -w Scrivere le modifiche nel file di input
-o, --output Scrivere in un file diverso
--json Output come JSON
--quiet, -q Sopprimere l'output
```
## Implementazione a fasi
- **Fase 1: Core** — creazione nodi, proprietà, operazioni albero, auto-layout, testo (~80% degli script reali)
- **Fase 2: Componenti & Istanze** — createComponent, createInstance, detachInstance
- **Fase 3: Variabili** — getLocalVariables, createVariable, setBoundVariable
- **Fase 4: Stili & Avanzato** — stili paint/testo/effetti, operazioni booleane
[Riferimento API completo in inglese](/eval-command)

View file

@ -2,7 +2,7 @@
## Panoramica del Sistema
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Layout dell'Editor
@ -62,7 +62,7 @@ Yoga di Meta fornisce il calcolo del layout CSS flexbox. Un adattatore sottile m
### Formato File (Kiwi Binary)
Riutilizza il codec binario Kiwi di Figma con 194 definizioni di messaggio/enum/struct. Importazione: analizza l'header → decompressione Zstd → decodifica Kiwi → NodeChange[] → scene graph. L'esportazione inverte il processo con generazione di miniature.
Riutilizza il codec binario Kiwi di Figma con 194 definizioni di messaggio/enum/struct. Importazione: analizza l'header → decompressione Zstd → decodifica Kiwi → `NodeChange`[] → scene graph. L'esportazione inverte il processo con generazione di miniature.
Consulta il [riferimento Formato File](/it/reference/file-format) per i dettagli.

View file

@ -215,7 +215,7 @@ Gestione stato via Potok. Undo con vettori di cambiamenti inversi (max 50 voci),
## 11. Scripting ed estensibilità
OpenPencil include un [comando `eval`](/eval-command) che fornisce un'API Plugin compatibile Figma per scripting headless. Inoltre, 90 strumenti AI disponibili via chat integrata, server MCP (stdio + HTTP) e CLI. Penpot ha un sistema plugin con esecuzione sandboxed ma senza API di scripting headless né integrazione MCP.
OpenPencil include un [comando `eval`](/programmable/cli/scripting) che fornisce un'API Plugin compatibile Figma per scripting headless. Inoltre, 90 strumenti AI disponibili via chat integrata, server MCP (stdio + HTTP) e CLI. Penpot ha un sistema plugin con esecuzione sandboxed ma senza API di scripting headless né integrazione MCP.
## Riepilogo

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
Consulta il [riferimento strumenti MCP](/it/reference/mcp-tools) per l'elenco completo degli strumenti.
Consulta il [riferimento strumenti MCP](/programmable/mcp-server) per l'elenco completo degli strumenti.
## CLI

View file

@ -126,7 +126,7 @@ Confronto funzionalità per funzionalità delle capacità di Figma Design con lo
| Sfocatura sfondo | ✅ | Sfocare contenuto dietro il livello |
| Sfocatura primo piano | ✅ | Sfocatura in primo piano |
| Spessore contorno | ✅ | Configurabile nel pannello proprietà |
| Estremità contorno (round, square, arrow) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Estremità contorno (round, square, arrow) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Giunzione contorno (miter, bevel, round) | ✅ | Tutti e tre i tipi di giunzione |
| Pattern tratteggio | ✅ | Pattern contorno dash-on/dash-off |
| Raggio angolo | ✅ | Raggio uniforme e per angolo con toggle indipendente |
@ -160,7 +160,7 @@ Confronto funzionalità per funzionalità delle capacità di Figma Design con lo
| Varianti | 🔲 | Cambio variante e selezione per proprietà |
| Proprietà componente | 🔲 | Proprietà booleane, testo, scambio istanza |
| Propagazione override | ✅ | Modifiche al componente principale propagate; override preservati |
| Variabili (colore, numero, stringa, booleano) | 🟡 | COLOR con UI completa; FLOAT/STRING/BOOLEAN definiti senza UI di modifica |
| Variabili (colore, numero, stringa, booleano) | 🟡 | `COLOR` con UI completa; `FLOAT`/STRING/BOOLEAN definiti senza UI di modifica |
| Collezioni e modi variabili | 🟡 | Collezioni, modi, cambio activeMode funzionano; nessuna UI tematizzazione |
| Stili (colore, testo, effetto, layout) | 🔲 | Preset stile riutilizzabili nominati |
| Librerie (pubblicare, condividere, aggiornare) | 🔲 | Librerie condivise componenti/stili |
@ -187,7 +187,7 @@ Confronto funzionalità per funzionalità delle capacità di Figma Design con lo
| Funzionalità | Stato | Note |
|-------------|-------|------|
| Import file .fig | ✅ | Codec Kiwi completo: 194 definizioni, ~390 campi per NodeChange |
| Import file .fig | ✅ | Codec Kiwi completo: 194 definizioni, ~390 campi per `NodeChange` |
| Export file .fig | ✅ | Codifica Kiwi + compressione Zstd + generazione miniatura |
| Salva / Salva con nome | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; dialoghi nativi (Tauri), File System Access API (Chrome/Edge), download fallback (Safari) |
| Appunti Figma (incolla) | ✅ | Decodificare binario Kiwi dagli appunti Figma |

View file

@ -1,71 +1,94 @@
# Formato file
# File Format
## Struttura file .fig
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Pipeline di importazione
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Pipeline di esportazione
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,150 +0,0 @@
# Server MCP
OpenPencil include un server MCP (Model Context Protocol) che permette agli strumenti di coding IA — Claude Code, Cursor, Windsurf ecc. — di leggere e modificare file .fig headless.
Due trasporti
## **stdio** per client MCP, **HTTP** per tutto il resto.
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
O eseguire dal codice sorgente:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Starts on port 3100 (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
Install the OpenPencil skill for your AI coding agent:
```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).
## Tools (75)
### 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_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
### 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 |
### 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 |
### 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 |

View file

@ -1,4 +1,4 @@
# Tipi di nodo
# 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 17 of these types.
@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -1,8 +1,8 @@
# Grafo della scena
# Scene Graph
## Rappresentazione in memoria
## In-Memory Representation
I nodi vivono in una mappa piatta `Map<string, Node>` keyed by GUID string. La struttura ad albero è mantenuta via `parentIndex` references. This gives Lookup O(1) by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -4,7 +4,7 @@
### Faza 1: Silnik Core ✅
SceneGraph, renderowanie Skia, podstawowe kształty, selekcja, zoom/pan, cofnij/ponów, linie wyrównania.
`SceneGraph`, renderowanie Skia, podstawowe kształty, selekcja, zoom/pan, cofnij/ponów, linie wyrównania.
### Faza 2: UI Edytora + Layout ✅
@ -16,7 +16,7 @@ Import/eksport .fig, kodek Kiwi, schowek, narzędzie pióra, sieci wektorowe, gr
### Faza 4: Komponenty + Zmienne ✅
Komponenty, instancje, nadpisania, zestawy komponentów, zmienne (COLOR/FLOAT/STRING/BOOLEAN), kolekcje, tryby, eksport obrazów, menu kontekstowe, formatowanie tekstu bogatego.
Komponenty, instancje, nadpisania, zestawy komponentów, zmienne (`COLOR`/FLOAT/STRING/BOOLEAN), kolekcje, tryby, eksport obrazów, menu kontekstowe, formatowanie tekstu bogatego.
### Faza 5: Integracja AI i Narzędzia ✅
@ -53,7 +53,7 @@ Komponenty, instancje, nadpisania, zestawy komponentów, zmienne (COLOR/FLOAT/ST
- Prototypowanie (połączenia ramek, przejścia, animacje)
- Komentarze (pin, wątki, rozwiązywanie)
- Wsparcie PWA
- Przełączanie wariantów, UI zmiennych FLOAT/STRING/BOOLEAN, theming przez zmienne
- Przełączanie wariantów, UI zmiennych `FLOAT`/STRING/BOOLEAN, theming przez zmienne
## Harmonogram

View file

@ -1,77 +0,0 @@
# `open-pencil eval` — API Pluginów kompatybilne z Figmą do skryptowania headless
## Przegląd
`bun open-pencil eval <file> --code '<js>'` wykonuje JavaScript na pliku `.fig` z globalnym obiektem `figma` kompatybilnym z Figmą. Umożliwia to skryptowanie headless, operacje wsadowe, wykonywanie narzędzi AI i testy — wszystko bez GUI.
Obiekt `figma` odzwierciedla powierzchnię API Pluginów Figmy jak najdokładniej, więc istniejąca wiedza o pluginach Figmy i fragmenty kodu są bezpośrednio przenośne.
```bash
# Utworzenie ramki, ustawienie auto-layout, dodanie dzieci
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
return { id: frame.id, name: frame.name }
'
# Zapytanie o węzły
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name }))
'
# Zapis zmian
bun open-pencil eval design.fig --code '...' --write
```
## Architektura
```
CLI: open-pencil eval <file> --code '...'
→ loadDocument(file) → SceneGraph
→ FigmaAPI(sceneGraph) → proxy `figma`
→ AsyncFunction('figma', code)(figmaProxy)
→ wydruk wyniku / zapis pliku z --write
```
### Główne klasy
| Klasa | Lokalizacja | Rola |
|-------|-------------|------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Obiekt proxy implementujący metody `figma.*` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Proxy opakowujący `SceneNode` z dostępem do właściwości w stylu Figmy |
| Polecenie `eval` | `packages/cli/src/commands/eval.ts` | Ładuje dokument, tworzy API, wykonuje kod |
### Dlaczego w `@open-pencil/core`?
Klasa `FigmaAPI` żyje w core, ponieważ: narzędzia AI ją reużywają, testy mogą jej używać i nie ma zależności DOM.
## Polecenie CLI
```
bun open-pencil eval <file> [opcje]
Argumenty:
file Plik .fig do przetworzenia
Opcje:
--code, -c Kod JavaScript do wykonania
--stdin Odczyt kodu ze stdin
--write, -w Zapis zmian do pliku wejściowego
-o, --output Zapis do innego pliku
--json Wynik jako JSON
--quiet, -q Pomiń wydruk
```
## Implementacja fazowa
- **Faza 1: Core** — tworzenie węzłów, właściwości, operacje na drzewie, auto-layout, tekst (~80% realnych skryptów)
- **Faza 2: Komponenty i Instancje** — createComponent, createInstance, detachInstance
- **Faza 3: Zmienne** — getLocalVariables, createVariable, setBoundVariable
- **Faza 4: Style i Zaawansowane** — style paint/tekst/efekty, operacje boolowskie
[Pełna referencja API po angielsku](/eval-command)

View file

@ -2,7 +2,7 @@
## Przegląd systemu
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Układ edytora
@ -62,7 +62,7 @@ Yoga od Mety zapewnia obliczanie layoutu CSS flexbox. Cienki adapter mapuje nazw
### Format pliku (Kiwi binarny)
Wykorzystuje binarny kodek Kiwi Figmy z 194 definicjami wiadomości/enum/struct. Import: parsowanie nagłówka → dekompresja Zstd → dekodowanie Kiwi → NodeChange[] → graf sceny. Eksport odwraca proces z generowaniem miniatur.
Wykorzystuje binarny kodek Kiwi Figmy z 194 definicjami wiadomości/enum/struct. Import: parsowanie nagłówka → dekompresja Zstd → dekodowanie Kiwi → `NodeChange`[] → graf sceny. Eksport odwraca proces z generowaniem miniatur.
Zobacz [Referencja formatu pliku](/reference/file-format) dla szczegółów.

View file

@ -215,7 +215,7 @@ Zarządzanie stanem przez Potok. Cofanie z wektorami zmian odwrotnych (max 50 wp
## 11. Scripting i rozszerzalność
OpenPencil zawiera [komendę `eval`](/eval-command) oferującą API Plugin kompatybilne z Figmą do skryptowania headless. Ponadto 90 narzędzi AI dostępnych przez wbudowany chat, serwer MCP (stdio + HTTP) i CLI. Penpot ma system pluginów z sandboxem, ale bez API skryptowania headless ani integracji MCP.
OpenPencil zawiera [komendę `eval`](/programmable/cli/scripting) oferującą API Plugin kompatybilne z Figmą do skryptowania headless. Ponadto 90 narzędzi AI dostępnych przez wbudowany chat, serwer MCP (stdio + HTTP) i CLI. Penpot ma system pluginów z sandboxem, ale bez API skryptowania headless ani integracji MCP.
## Podsumowanie

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
Zobacz [Referencja narzędzi MCP](/reference/mcp-tools) dla pełnej listy narzędzi.
Zobacz [Referencja narzędzi MCP](/programmable/mcp-server) dla pełnej listy narzędzi.
## CLI

View file

@ -126,7 +126,7 @@ Porównanie funkcja po funkcji możliwości Figma Design z aktualnym stanem impl
| Rozmycie tła | ✅ | Rozmycie zawartości za warstwą |
| Rozmycie pierwszego planu | ✅ | Rozmycie na pierwszym planie |
| Grubość obrysu | ✅ | Konfigurowalny w panelu właściwości |
| Zakończenie obrysu (round, square, arrow) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Zakończenie obrysu (round, square, arrow) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Złączenie obrysu (miter, bevel, round) | ✅ | Wszystkie trzy typy złączeń |
| Wzory przerywane | ✅ | Wzór obrysu dash-on/dash-off |
| Promień narożnika | ✅ | Jednolity i per narożnik z niezależnym przełącznikiem |
@ -160,7 +160,7 @@ Porównanie funkcja po funkcji możliwości Figma Design z aktualnym stanem impl
| Warianty | 🔲 | Przełączanie wariantów i selekcja po właściwościach |
| Właściwości komponentu | 🔲 | Właściwości boolean, tekst, zamiana instancji |
| Propagacja nadpisań | ✅ | Zmiany w głównym komponencie propagowane; nadpisania zachowane |
| Zmienne (kolor, liczba, string, boolean) | 🟡 | COLOR z pełnym UI; FLOAT/STRING/BOOLEAN zdefiniowane bez UI edycji |
| Zmienne (kolor, liczba, string, boolean) | 🟡 | `COLOR` z pełnym UI; `FLOAT`/STRING/BOOLEAN zdefiniowane bez UI edycji |
| Kolekcje i tryby zmiennych | 🟡 | Kolekcje, tryby, zmiana activeMode działają; brak UI tematyzacji |
| Style (kolor, tekst, efekt, layout) | 🔲 | Presety stylów wielokrotnego użytku |
| Biblioteki (publikuj, udostępniaj, aktualizuj) | 🔲 | Współdzielone biblioteki komponentów/stylów |
@ -187,7 +187,7 @@ Porównanie funkcja po funkcji możliwości Figma Design z aktualnym stanem impl
| Funkcja | Status | Uwagi |
|---------|--------|-------|
| Import pliku .fig | ✅ | Pełny kodek Kiwi: 194 definicje, ~390 pól per NodeChange |
| Import pliku .fig | ✅ | Pełny kodek Kiwi: 194 definicje, ~390 pól per `NodeChange` |
| Eksport pliku .fig | ✅ | Kodowanie Kiwi + kompresja Zstd + generowanie miniatur |
| Zapisz / Zapisz jako | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; natywne dialogi (Tauri), File System Access API (Chrome/Edge), fallback pobierania (Safari) |
| Schowek Figmy (wklej) | ✅ | Dekodowanie binarnego Kiwi ze schowka Figmy |

View file

@ -1,71 +1,94 @@
# Format pliku
# File Format
## Struktura pliku .fig
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Pipeline importu
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Pipeline eksportu
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,150 +0,0 @@
# Serwer MCP
OpenPencil zawiera serwer MCP (Model Context Protocol), który umożliwia narzędziom kodowania AI — Claude Code, Cursor, Windsurf itp. — odczyt i modyfikację plików .fig headless.
Dwa transporty
## **stdio** dla klientów MCP, **HTTP** dla reszty.
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
Lub uruchom ze źródeł:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Starts on port 3100 (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
Install the OpenPencil skill for your AI coding agent:
```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).
## Tools (75)
### 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_node` | Get detailed properties of a node by ID |
| `find_nodes` | Find nodes by name pattern and/or type |
| `list_pages` | List all pages |
| `list_variables` | List design variables |
| `list_collections` | List variable collections |
### Create
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
### 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 |
### 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 |
### 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 |

View file

@ -1,4 +1,4 @@
# Typy węzłów
# 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 17 of these types.
@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -1,8 +1,8 @@
# Graf sceny
# Scene Graph
## Reprezentacja w pamięci
## In-Memory Representation
Węzły żyją w płaskiej mapie `Map<string, Node>` keyed by GUID string. Struktura drzewa utrzymywana przez `parentIndex` references. This gives Wyszukiwanie O(1) by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -126,7 +126,7 @@ Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports [sk
| Tool | Description |
|------|-------------|
| `create_shape` | Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `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 |
@ -149,7 +149,7 @@ Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports [sk
| `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_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) |

View file

@ -2,70 +2,93 @@
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["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
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi encode → Compress (Zstd/deflate)
→ Build ZIP (header + schema + message + thumbnail.png)
→ Write .fig file
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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. The ZIP archive is assembled in Rust on desktop for correct Zstd frame headers (content size included).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (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:
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/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
| 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 vendored kiwi-schema parser is patched to handle this correctly.
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. Clipboard encoding also uses `fflate`.
`.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 | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
See [Roadmap](/development/roadmap) for planned format support timeline.
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## 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
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,251 +0,0 @@
# MCP Server
OpenPencil includes an MCP (Model Context Protocol) server that lets AI coding tools — Claude Code, Cursor, Windsurf, etc. — read and modify `.fig` files headlessly.
Two transports: **stdio** for MCP clients, **HTTP** for everything else.
## Install
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor, etc.)
Add to your MCP config (e.g. `~/.claude/settings.json` or `.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/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
For browser extensions, scripts, CI, or any HTTP client:
```sh
openpencil-mcp-http
```
Or from source: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.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 <token>` or `x-mcp-token`)
Server starts on port 3100 (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.

View file

@ -8,41 +8,41 @@ The scene graph supports 28 node types from Figma's Kiwi schema. Each node is id
| 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) | ✅ |
| `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 (17 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)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -81,14 +81,14 @@ Document
## Core Properties
Every node carries these fields (subset of NodeChange):
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
- `phase` — `CREATED` or `REMOVED`
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
@ -103,14 +103,14 @@ Every node carries these fields (subset of NodeChange):
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `strokeAlign` — `INSIDE` / `CENTER` / `OUTSIDE`
- `strokeCap` — `NONE` / `ROUND` / `SQUARE` / `ARROW_LINES` / `ARROW_EQUILATERAL`
- `strokeJoin` — `MITER` / `BEVEL` / `ROUND`
- `dashPattern[]` — dash/gap lengths
### Corners

View file

@ -2,7 +2,7 @@
## In-Memory Representation
Nodes live in a flat `Map<string, Node>` keyed by GUID string. The tree structure is maintained via `parentIndex` references. This gives O(1) lookup by ID and efficient traversal.
Nodes live in a flat `Map<string, Node>` 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 {
@ -31,11 +31,11 @@ interface SceneGraph {
## 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.
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.
`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
@ -86,11 +86,11 @@ For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect
## 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).
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.
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

View file

@ -4,7 +4,7 @@
### Фаза 1: Ядро движка ✅
SceneGraph, отрисовка Skia, базовые фигуры, выделение, масштабирование/панорамирование, отмена/повтор.
`SceneGraph`, отрисовка Skia, базовые фигуры, выделение, масштабирование/панорамирование, отмена/повтор.
**Реализовано:**
- Граф сцены с хранением в плоском Map и древовидной структурой родитель-потомок
@ -77,7 +77,7 @@ SceneGraph, отрисовка Skia, базовые фигуры, выделен
- Изменяемые размеры левой/правой панелей через reka-ui Splitter (сохраняемый макет)
- Алиас импорта @/, модуль общих типов (src/types.ts, src/global.d.ts)
- Чистый код: 0 предупреждений oxlint, 0 ошибок типов tsgo
- Переменные: тип COLOR с коллекциями, режимами, привязками, выбор переменных в FillSection, импорт из .fig
- Переменные: тип `COLOR` с коллекциями, режимами, привязками, выбор переменных в FillSection, импорт из .fig
- Диалог переменных: TanStack Table с изменяемой шириной столбцов, столбцы режимов, вкладки коллекций с переименованием, поиск, демо-коллекции (Primitives/Semantic/Spacing), отмена/повтор для всех операций с переменными
- Экспорт изображений: PNG/JPG/WEBP с ExportSection (масштаб, формат, предпросмотр), сочетание <kbd>⇧</kbd><kbd>⌘</kbd><kbd>E</kbd>, контекстное меню
- Нативное редактирование текста на холсте: класс TextEditor в core, phantom textarea, курсор/выделение/границы слов на холсте, мерцание каретки, подсветка выделения
@ -92,7 +92,7 @@ SceneGraph, отрисовка Skia, базовые фигуры, выделен
**Оставшееся (перенесено в Фазу 6):**
- Переключение вариантов
- UI для типов переменных: FLOAT, STRING, BOOLEAN
- UI для типов переменных: `FLOAT`, `STRING`, `BOOLEAN`
- Тематизация на основе переменных
### Фаза 5: AI-интеграция и инструментарий ✅
@ -146,7 +146,7 @@ SceneGraph, отрисовка Skia, базовые фигуры, выделен
- Прототипирование (связи фреймов, переходы, анимации)
- Комментарии (закрепление, треды, разрешение)
- Поддержка PWA
- Переключение вариантов, UI для FLOAT/STRING/BOOLEAN переменных, тематизация на основе переменных
- Переключение вариантов, UI для `FLOAT`/STRING/BOOLEAN переменных, тематизация на основе переменных
- Полный набор тестов совместимости с Figma
## Сроки

View file

@ -1,437 +0,0 @@
# `open-pencil eval` — Figma-совместимый Plugin API для скриптинга без GUI
## Обзор
`bun open-pencil eval <file> --code '<js>'` выполняет JavaScript-код над `.fig`-файлом с глобальным объектом `figma`, совместимым с Figma. Это позволяет автоматизировать пакетные операции, запускать AI-инструменты и тесты — всё без графического интерфейса.
Объект `figma` максимально точно повторяет API плагинов Figma, поэтому существующие знания и фрагменты кода для плагинов Figma можно использовать напрямую.
```bash
# Создать фрейм, настроить автораскладку, добавить дочерние элементы
bun open-pencil eval design.fig --code '
const frame = figma.createFrame()
frame.name = "Card"
frame.resize(300, 200)
frame.layoutMode = "VERTICAL"
frame.itemSpacing = 12
frame.paddingTop = frame.paddingBottom = 16
frame.paddingLeft = frame.paddingRight = 16
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]
const title = figma.createText()
title.characters = "Hello World"
title.fontSize = 24
frame.appendChild(title)
return { id: frame.id, name: frame.name }
'
# Поиск узлов
bun open-pencil eval design.fig --code '
const buttons = figma.currentPage.findAll(n => n.type === "FRAME" && n.name.includes("Button"))
return buttons.map(b => ({ id: b.id, name: b.name, w: b.width, h: b.height }))
'
# Чтение из stdin (для многострочных скриптов / пайплайнов)
cat transform.js | bun open-pencil eval design.fig --stdin
# Сохранить изменения обратно в файл
bun open-pencil eval design.fig --code '...' --write
bun open-pencil eval design.fig --code '...' -o modified.fig
```
## Архитектура
```
┌──────────────────────────────────────────────────────┐
│ CLI: `open-pencil eval <file> --code '...'` │
│ ↓ │
│ loadDocument(file) → SceneGraph │
│ ↓ │
│ FigmaAPI(sceneGraph) → прокси-объект `figma` │
│ ↓ │
│ AsyncFunction('figma', wrappedCode)(figmaProxy) │
│ ↓ │
│ вывод результата в JSON / agentfmt │
│ опционально: saveDocument(file) при --write │
└──────────────────────────────────────────────────────┘
```
### Основные классы
| Класс | Расположение | Роль |
|-------|--------------|------|
| `FigmaAPI` | `packages/core/src/figma-api.ts` | Прокси-объект, реализующий методы `figma.*` поверх `SceneGraph` |
| `FigmaNode` | `packages/core/src/figma-api.ts` | Прокси-обёртка над `SceneNode` с доступом к свойствам в стиле Figma (`.fills`, `.resize()`, `.appendChild()` и т.д.) |
| Команда `eval` | `packages/cli/src/commands/eval.ts` | CLI-команда, загружающая документ, создающая API и выполняющая код |
### Почему в `@open-pencil/core`?
Класс `FigmaAPI` находится в core (а не в CLI), потому что:
- **AI-инструменты используют его** — инструмент `render` в панели чата выполняет JSX через тот же API
- **Тестовые скрипты** — модульные тесты могут использовать API для подготовки фикстур
- **Нет зависимостей от DOM** — работает в headless-режиме в Bun, без браузерных API
## `FigmaAPI` — поэтапная реализация
### Фаза 1: Базовый функционал (MVP для команды eval)
Покрывает ~80% реальных скриптов для плагинов:
#### Документ и страницы
| Figma API | Наша реализация | Примечания |
|-----------|-----------------|------------|
| `figma.root` | Геттер → прокси для корневого узла | `.children` возвращает прокси страниц |
| `figma.currentPage` | Геттер/сеттер → первая страница по умолчанию | Можно установить на любой прокси страницы |
| `figma.currentPage.selection` | Чтение/запись → отслеживаемый массив выделения | |
| `figma.getNodeById(id)` | `graph.getNode(id)`, обёрнутый в прокси | Синхронный, как устаревшая версия Figma |
#### Создание узлов
| Figma API | Соответствие |
|-----------|--------------|
| `figma.createFrame()` | `graph.createNode('FRAME', currentPageId)` |
| `figma.createRectangle()` | `graph.createNode('RECTANGLE', ...)` |
| `figma.createEllipse()` | `graph.createNode('ELLIPSE', ...)` |
| `figma.createText()` | `graph.createNode('TEXT', ...)` |
| `figma.createLine()` | `graph.createNode('LINE', ...)` |
| `figma.createPolygon()` | `graph.createNode('POLYGON', ...)` |
| `figma.createStar()` | `graph.createNode('STAR', ...)` |
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
| `figma.createPage()` | `graph.addPage(name)` |
| `figma.createSection()` | `graph.createNode('SECTION', ...)` |
#### Свойства узлов (через прокси `FigmaNode`)
Чтение и запись на любом прокси-узле. Обращение к свойствам транслируется в поля `SceneNode`:
```ts
// Геометрия
node.x, node.y // прямое соответствие
node.width, node.height // только чтение, используйте node.resize(w, h)
node.rotation // прямое соответствие
node.resize(w, h) // обновляет width и height
node.resizeWithoutConstraints(w, h) // аналогично (движка ограничений пока нет)
// Визуальные свойства
node.fills // чтение/запись Fill[]
node.strokes // чтение/запись Stroke[]
node.effects // чтение/запись Effect[]
node.opacity // чтение/запись number
node.visible // чтение/запись boolean
node.locked // чтение/запись boolean
node.blendMode // чтение/запись BlendMode
node.clipsContent // чтение/запись boolean
// Скругление углов
node.cornerRadius // чтение/запись (number или figma.mixed)
node.topLeftRadius // чтение/запись
node.topRightRadius // чтение/запись
node.bottomLeftRadius // чтение/запись
node.bottomRightRadius // чтение/запись
node.cornerSmoothing // чтение/запись
// Идентификация
node.id // только чтение
node.name // чтение/запись
node.type // только чтение
node.parent // только чтение → FigmaNode | null
node.removed // только чтение boolean
```
#### Операции с деревом
```ts
node.children // только чтение FigmaNode[]
node.appendChild(child) // перемещает в конец
node.insertChild(index, child) // перемещает на позицию index
node.remove() // graph.deleteNode(id)
// Обход дерева
node.findAll(callback?) // рекурсивный поиск
node.findOne(callback) // первое совпадение
node.findChild(callback) // только среди прямых потомков
node.findChildren(callback?) // только среди прямых потомков
```
#### Автораскладка
```ts
node.layoutMode // 'NONE' | 'HORIZONTAL' | 'VERTICAL'
node.primaryAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
node.counterAxisAlignItems // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
node.itemSpacing // number
node.counterAxisSpacing // number | null
node.paddingTop / Right / Bottom / Left // number
node.layoutWrap // 'NO_WRAP' | 'WRAP'
// Размеры дочерних элементов
node.layoutPositioning // 'AUTO' | 'ABSOLUTE'
node.layoutGrow // 0 | 1
node.layoutSizingHorizontal // 'FIXED' | 'HUG' | 'FILL'
node.layoutSizingVertical // 'FIXED' | 'HUG' | 'FILL'
```
#### Текст
```ts
node.characters // чтение/запись (соответствует node.text)
node.fontSize // чтение/запись
node.fontName // чтение/запись { family, style }
node.fontWeight // чтение/запись
node.textAlignHorizontal // чтение/запись
node.textAlignVertical // чтение/запись
node.textAutoResize // чтение/запись
node.letterSpacing // чтение/запись
node.lineHeight // чтение/запись
node.maxLines // чтение/запись
node.textCase // чтение/запись
node.textDecoration // чтение/запись
```
#### Параметры обводки
```ts
node.strokeWeight // чтение/запись (соответствует strokes[0].weight)
node.strokeAlign // чтение/запись (соответствует strokes[0].align)
node.dashPattern // чтение/запись
```
#### Прочее
```ts
figma.mixed // Symbol-маркер для смешанных значений
figma.group(nodes, parent) // создаёт GROUP с указанными дочерними элементами
figma.ungroup(node) // разгруппировывает, перемещает потомков к родителю
figma.flatten(nodes) // ПОКА НЕ РЕАЛИЗОВАНО — возвращает первый узел
```
#### Экспорт
```ts
node.exportAsync(settings?) // работает только при загруженном CanvasKit
// settings: { format: 'PNG'|'JPG'|'SVG', constraint? }
```
### Фаза 2: Компоненты и экземпляры
| API | Соответствие |
|-----|--------------|
| `figma.createComponent()` | `graph.createNode('COMPONENT', ...)` |
| `figma.createComponentFromNode(node)` | Преобразование существующего фрейма в компонент |
| `figma.combineAsVariants(components, parent)` | Создание COMPONENT_SET |
| Узел: `node.createInstance()` | `graph.createInstance(componentId, parentId)` |
| Узел: `node.detachInstance()` | `graph.detachInstance(id)` |
| `figma.getNodeById(id).mainComponent` | `graph.getMainComponent(id)` |
### Фаза 3: Переменные
| API | Соответствие |
|-----|--------------|
| `figma.variables.getLocalVariables(type?)` | `graph.variables` с фильтрацией |
| `figma.variables.getLocalVariableCollections()` | `graph.variableCollections` |
| `figma.variables.createVariable(name, collection, type)` | `graph.addVariable(...)` |
| `figma.variables.createVariableCollection(name)` | `graph.addCollection(...)` |
| `figma.variables.getVariableById(id)` | `graph.variables.get(id)` |
| `node.setBoundVariable(field, variable)` | `graph.bindVariable(...)` |
| `node.boundVariables` | Геттер из SceneNode |
### Фаза 4: Стили и расширенные возможности
| API | Примечания |
|-----|------------|
| `figma.createPaintStyle()` | Требует хранилища стилей в SceneGraph |
| `figma.createTextStyle()` | Требует хранилища стилей в SceneGraph |
| `figma.createEffectStyle()` | Требует хранилища стилей в SceneGraph |
| `figma.loadFontAsync(fontName)` | No-op (у нас нет ограничений на загрузку шрифтов) |
| `figma.listAvailableFontsAsync()` | Возвращает системные шрифты, если доступны |
| Булевы операции (`union`, `subtract`, `intersect`, `exclude`) | Требует движка булевых операций над путями |
| `figma.createNodeFromJSXAsync(jsx)` | Портирование JSX-рендерера из figma-use |
## Устройство прокси `FigmaNode`
Прокси оборачивает `SceneNode` и транслирует имена свойств Figma во внутренние имена. Основные соответствия:
```ts
const PROPERTY_MAP: Record<string, string> = {
// Имя Figma → поле SceneNode (только при различиях)
'characters': 'text',
'strokeWeight': → вычисляется из strokes[0].weight,
'strokeAlign': → вычисляется из strokes[0].align,
'fontName': → вычисляется из { family: fontFamily, style: ... },
'primaryAxisAlignItems': 'primaryAxisAlign',
'counterAxisAlignItems': 'counterAxisAlign',
'primaryAxisSizingMode': 'primaryAxisSizing', // маппинг значений: 'AUTO' → 'HUG', 'FIXED' → 'FIXED'
'counterAxisSizingMode': 'counterAxisSizing',
'layoutSizingHorizontal': → вычисляется из primaryAxisSizing / counterAxisSizing в зависимости от layoutMode
'layoutSizingVertical': → вычисляется
}
```
Методы прокси:
```ts
class FigmaNode {
// Прокси создаётся через: new Proxy(target, handler)
// handler.get перехватывает чтение свойств, handler.set — запись
resize(width: number, height: number): void
resizeWithoutConstraints(width: number, height: number): void
remove(): void
appendChild(child: FigmaNode): void
insertChild(index: number, child: FigmaNode): void
findAll(callback?: (node: FigmaNode) => boolean): FigmaNode[]
findOne(callback: (node: FigmaNode) => boolean): FigmaNode | null
findChild(callback: (node: FigmaNode) => boolean): FigmaNode | null
findChildren(callback?: (node: FigmaNode) => boolean): FigmaNode[]
exportAsync(settings?: ExportSettings): Promise<Uint8Array>
// Компоненты (Фаза 2)
createInstance(): FigmaNode
detachInstance(): void
get mainComponent(): FigmaNode | null
}
```
## CLI-команда
```
bun open-pencil eval <file> [options]
Аргументы:
file .fig-файл для обработки
Опции:
--code, -c JavaScript-код для выполнения (имеет доступ к глобальному объекту `figma`)
--stdin Читать код из stdin вместо --code
--write, -w Записать изменения обратно в исходный файл
-o, --output Записать в другой файл
--json Вывести результат в формате JSON (по умолчанию для не-TTY)
--quiet, -q Подавить вывод, только записать файл
```
### Модель выполнения
1. Загрузка `.fig` → `SceneGraph`
2. Создание `FigmaAPI(graph)` → прокси `figma`
3. Обёртка пользовательского кода в асинхронную функцию: `return (async () => { <код> })()`
4. Выполнение с `figma` в качестве единственного аргумента
5. Вывод возвращённого значения (JSON или agentfmt)
6. Если указан `--write` или `-o`: сериализация `SceneGraph` обратно в `.fig`
### Форматирование возвращаемого значения
- `undefined` / `void` → без вывода
- Примитивы → выводятся напрямую
- Объекты/массивы → `JSON.stringify(result, null, 2)` или таблицы agentfmt
- `FigmaNode` → сериализуется как `{ id, type, name, x, y, width, height, fills, ... }`
- Массивы `FigmaNode` → сериализуются как список
## Общий код с AI-инструментами
Класс `FigmaAPI` — это **та же поверхность API**, которую используют AI-инструменты. Сейчас `src/ai/tools.ts` вызывает `store.createShape()`, `store.updateNodeWithUndo()` и т.д. — их следует рефакторить для работы через `FigmaAPI`:
```ts
// До (текущие AI-инструменты)
execute: async ({ type, x, y, width, height }) => {
const id = store.createShape(type, x, y, width, height)
return { id }
}
// После (с использованием FigmaAPI)
execute: async ({ type, x, y, width, height }) => {
const frame = figma.createFrame()
frame.resize(width, height)
frame.x = x
frame.y = y
return { id: frame.id }
}
```
Это гарантирует идентичное поведение CLI-скриптов и AI-инструментов.
## Структура файлов
```
packages/core/src/
figma-api.ts # Класс FigmaAPI + прокси FigmaNode (Фазы 1–4)
figma-api.test.ts # Модульные тесты на headless SceneGraph
packages/cli/src/commands/
eval.ts # CLI-команда
packages/cli/src/commands/eval.test.ts # Интеграционные тесты
```
## План тестирования
### Модульные тесты (`packages/core/src/figma-api.test.ts`)
1. **Создание узлов** — каждый `createX()` создаёт узел правильного типа, добавленный на текущую страницу
2. **Доступ к свойствам** — `.fills`, `.x`, `.width`, `.name`, `.characters` корректно читаются и записываются
3. **Изменение размера** — `.resize(w, h)` обновляет width/height
4. **Операции с деревом** — `.appendChild()`, `.insertChild()`, `.remove()`, `.parent`, `.children`
5. **Обход дерева** — `.findAll()`, `.findOne()`, `.findChild()`, `.findChildren()` с колбэками
6. **Автораскладка** — `.layoutMode`, `.itemSpacing`, `.paddingTop` и т.д.
7. **Текст** — `.characters` соответствует `.text`, `.fontName` соответствует `{ family, style }`
8. **Смешанные значения** — `.cornerRadius` возвращает `figma.mixed`, когда углы различаются
9. **Выделение** — `figma.currentPage.selection` чтение/запись
10. **Переключение страниц** — `figma.currentPage = page2` работает
11. **Группировка/разгруппировка** — `figma.group()` создаёт группу, `figma.ungroup()` расформировывает её
12. **Клонирование** — создание узлов порождает независимые копии
### Интеграционные тесты CLI (`packages/cli/src/commands/eval.test.ts`)
1. **Базовый eval** — `eval test.fig --code 'return figma.currentPage.name'` → имя страницы
2. **Создание + чтение** — создать фрейм, вернуть его свойства
3. **Поиск узлов** — `findAll` возвращает правильные узлы
4. **Сохранение** — `--write` сохраняет изменения, при повторной загрузке они видны
5. **Stdin** — `echo 'return 42' | bun open-pencil eval test.fig --stdin` → `42`
6. **JSON-вывод** — `--json` возвращает валидный JSON
7. **Обработка ошибок** — синтаксические и ошибки времени выполнения корректно выводятся
## Порядок реализации
1. **Прокси `FigmaNode`** — маппинг свойств, `.resize()`, `.remove()`, методы работы с деревом
2. **Класс `FigmaAPI`** — `createFrame/Rectangle/...`, `.root`, `.currentPage`, `.getNodeById()`, `.mixed`, `.group()`
3. **CLI-команда `eval`** — разбор аргументов, обёртка кода, форматирование вывода
4. **Модульные тесты** — все 12 групп тестов выше
5. **Интеграционные тесты CLI** — все 7 групп тестов выше
6. **Интеграция с AI-инструментами** — рефакторинг `src/ai/tools.ts` для использования `FigmaAPI` где возможно
7. **Фаза 2** — компоненты и экземпляры
8. **Фаза 3** — переменные
9. **Фаза 4** — стили, булевы операции, JSX-рендерер
## Справочник соответствий свойств
| Свойство Figma | Поле SceneNode | Тип | Примечания |
|----------------|----------------|-----|------------|
| `characters` | `text` | `string` | |
| `fontName` | `fontFamily` + `fontWeight` + `italic` | `{ family, style }` | Вычисляется: `style` = "Bold Italic" и т.д. |
| `strokeWeight` | `strokes[0].weight` | `number` | Вычисляется |
| `strokeAlign` | `strokes[0].align` | `string` | Вычисляется |
| `primaryAxisAlignItems` | `primaryAxisAlign` | `string` | |
| `counterAxisAlignItems` | `counterAxisAlign` | `string` | |
| `layoutSizingHorizontal` | `primaryAxisSizing` или `counterAxisSizing` | `string` | Зависит от `layoutMode` |
| `layoutSizingVertical` | (противоположное horizontal) | `string` | |
| `absoluteTransform` | вычисляется из `x`, `y`, `rotation` | `Transform` | Только чтение |
| `absoluteBoundingBox` | `getAbsoluteBounds(id)` | `Rect` | Только чтение |
| Все остальные | Совпадающее имя | Тот же тип | Прямая передача |
## Открытые вопросы
1. **Загрузка шрифтов**: `figma.loadFontAsync()` — должно ли быть no-op (у нас нет ограничений на шрифты) или нужно отслеживать загруженные шрифты?
→ **Решение: No-op, возвращающий resolved Promise.** Мы не блокируем редактирование текста загрузкой шрифтов.
2. **Экспорт в headless-режиме**: `node.exportAsync()` требует CanvasKit. Должен ли eval загружать CanvasKit?
→ **Решение: Опционально.** Если CanvasKit доступен (через флаг `--with-canvaskit` или переменную окружения), экспорт включается. Иначе выбрасывается ошибка "Export requires CanvasKit".
3. **Символ `figma.mixed`**: Использовать настоящий символ Figma или собственный?
→ **Решение: Собственный `Symbol('mixed')`.** Доступен как `figma.mixed`.
4. **Отмена действий**: `figma.commitUndo()` / `figma.triggerUndo()` — актуально ли в headless-режиме?
→ **Решение: No-op в CLI.** Отмена имеет значение только в интерактивном редакторе. AI-инструменты могут добавить поддержку отмены отдельно через EditorStore.
5. **Формат записи**: Должен ли `--write` создавать `.fig` (бинарный Kiwi) или также поддерживать `.json`?
→ **Решение: Только `.fig` на данный момент.** Экспорт в JSON — отдельная функциональность.

View file

@ -2,7 +2,7 @@
## Обзор системы
```mermaid
`mermaid
graph TB
subgraph Tauri["Tauri v2 Shell"]
subgraph Editor["Editor (Web)"]
@ -22,7 +22,7 @@ graph TB
MCP["MCP Server (90 tools, stdio+HTTP)"]
Collab["P2P Collab (Trystero + Yjs)"]
end
```
`
## Макет редактора
@ -62,7 +62,7 @@ Yoga от Meta обеспечивает вычисление макета CSS fl
### Формат файлов (Kiwi Binary)
Использует бинарный кодек Kiwi от Figma с 194 определениями message/enum/struct. Импорт: разбор заголовка → декомпрессия Zstd → декодирование Kiwi → NodeChange[] → граф сцены. Экспорт выполняет обратный процесс с генерацией миниатюры.
Использует бинарный кодек Kiwi от Figma с 194 определениями message/enum/struct. Импорт: разбор заголовка → декомпрессия Zstd → декодирование Kiwi → `NodeChange`[] → граф сцены. Экспорт выполняет обратный процесс с генерацией миниатюры.
См. [справочник формата файлов](/reference/file-format) для подробностей.

View file

@ -272,7 +272,7 @@ class UndoManager {
## 11. Скриптинг и расширяемость
OpenPencil поставляется с [командой `eval`](/eval-command), предоставляющей Figma-совместимый Plugin API для headless-скриптинга — пакетные операции, автоматизированное тестирование и ИИ-модификации работают без GUI. Кроме того, **90 ИИ-инструментов** доступны через встроенный чат, MCP-сервер (stdio + HTTP) и CLI — охватывающие чтение, создание, модификацию, структуру, переменные, векторные пути, анализ (цвета/типографика/отступы/кластеры), сравнение, булевые операции и упорядочивание. Penpot имеет систему плагинов с изолированным выполнением, но без headless-скриптинга или интеграции MCP.
OpenPencil поставляется с [командой `eval`](/programmable/cli/scripting), предоставляющей Figma-совместимый Plugin API для headless-скриптинга — пакетные операции, автоматизированное тестирование и ИИ-модификации работают без GUI. Кроме того, **90 ИИ-инструментов** доступны через встроенный чат, MCP-сервер (stdio + HTTP) и CLI — охватывающие чтение, создание, модификацию, структуру, переменные, векторные пути, анализ (цвета/типографика/отступы/кластеры), сравнение, булевые операции и упорядочивание. Penpot имеет систему плагинов с изолированным выполнением, но без headless-скриптинга или интеграции MCP.
## Итоги

View file

@ -87,7 +87,7 @@ bun add -g @open-pencil/mcp
}
```
См. [справочник инструментов MCP](/reference/mcp-tools) для полного списка.
См. [справочник инструментов MCP](/programmable/mcp-server) для полного списка.
## CLI

View file

@ -126,7 +126,7 @@
| Размытие фона | ✅ | Размытие содержимого за слоем |
| Размытие переднего плана | ✅ | Размытие на переднем плане |
| Толщина обводки | ✅ | Настраиваемая в панели свойств |
| Наконечник обводки (круглый, квадратный, стрелка) | ✅ | NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL |
| Наконечник обводки (круглый, квадратный, стрелка) | ✅ | `NONE`, `ROUND`, `SQUARE`, `ARROW_LINES`, `ARROW_EQUILATERAL` |
| Соединение обводки (miter, bevel, round) | ✅ | Все три типа соединений |
| Штриховые паттерны | ✅ | Паттерн dash-on/dash-off |
| Выравнивание обводки | ✅ | Inside/Center/Outside с clip-рендерингом, соответствующим поведению Figma |
@ -162,7 +162,7 @@
| Варианты | 🔲 | Переключение вариантов и выбор по свойствам |
| Свойства компонентов | 🔲 | Boolean, text, instance swap свойства |
| Распространение переопределений | ✅ | Изменения главного компонента распространяются на все экземпляры; переопределения сохраняются |
| Переменные (color, number, string, boolean) | 🟡 | COLOR полный UI (диалог, TanStack Table, инлайн-редактирование, отмена/повтор, демо-коллекции); FLOAT/STRING/BOOLEAN определены, но без UI редактирования |
| Переменные (color, number, string, boolean) | 🟡 | `COLOR` полный UI (диалог, TanStack Table, инлайн-редактирование, отмена/повтор, демо-коллекции); `FLOAT`/STRING/BOOLEAN определены, но без UI редактирования |
| Коллекции и режимы переменных | 🟡 | Коллекции, режимы, переключение activeMode работают; нет UI тем на переменных |
| Стили (цвет, текст, эффект, макет) | 🔲 | Переиспользуемые именованные пресеты стилей |
| Библиотеки (публикация, обмен, обновление) | 🔲 | Общие библиотеки компонентов/стилей |
@ -189,8 +189,8 @@
| Функция | Статус | Примечания |
|---------|--------|-------|
| Импорт файлов .fig | ✅ | Полный Kiwi-кодек: 194 определения, ~390 полей на NodeChange |
| Экспорт файлов .fig | ✅ | Кодирование Kiwi + сжатие Zstd + генерация миниатюры; COMPONENT/COMPONENT_SET маппятся в SYMBOL для round-trip |
| Импорт файлов .fig | ✅ | Полный Kiwi-кодек: 194 определения, ~390 полей на `NodeChange` |
| Экспорт файлов .fig | ✅ | Кодирование Kiwi + сжатие Zstd + генерация миниатюры; `COMPONENT`/COMPONENT_SET маппятся в `SYMBOL` для round-trip |
| Сохранить / Сохранить как | ✅ | <kbd>⌘</kbd><kbd>S</kbd> / <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd>; нативные диалоги (Tauri), File System Access API (Chrome/Edge), скачивание (Safari) |
| Буфер обмена Figma (вставка) | ✅ | Декодирование бинарного Kiwi из буфера обмена Figma |
| Буфер обмена Figma (копирование) | ✅ | Кодирование бинарного Kiwi, читаемого Figma |

View file

@ -1,71 +1,94 @@
# Формат файлов
# File Format
## Структура .fig файла
## .fig File Structure
```
┌─────────────────────────────────┐
│ Магический заголовок: "fig-kiwi" (8Б) │
│ Версия (4Б uint32 LE) │
│ Длина схемы (4Б uint32 LE) │
│ Сжатая Kiwi-схема │
│ Длина сообщения (4Б uint32 LE) │
│ Сжатое Kiwi-сообщение │ ← NodeChange[] (весь документ)
│ Бинарные данные │ ← Изображения, векторные сети, шрифты
└─────────────────────────────────┘
A `.fig` file is a ZIP archive containing a Kiwi-encoded binary message:
```mermaid
block-beta
columns 1
A["Magic header: <code>fig-kiwi</code> (8 bytes)"]
B["Version (4 bytes, uint32 LE)"]
C["Schema length (4 bytes, uint32 LE)"]
D["Compressed Kiwi schema"]
E["Message length (4 bytes, uint32 LE)"]
F["Compressed Kiwi message — NodeChange[]"]
G["Blob data — images, vector networks, fonts"]
```
## Конвейер импорта
## Import Pipeline
```
.fig файл → Парсинг заголовка → Декомпрессия Zstd → Декодирование Kiwi-схемы
→ Декодирование сообщения → NodeChange[] → Построение SceneGraph
→ Разрешение ссылок на блобы → Отрисовка на холсте
```mermaid
flowchart LR
A[".fig file"] --> B["Parse header"]
B --> C["Decompress Zstd"]
C --> D["Decode Kiwi schema"]
D --> E["Decode message"]
E --> F["NodeChange[]"]
F --> G["Build SceneGraph"]
G --> H["Resolve blob refs"]
H --> I["Render"]
```
## Конвейер экспорта
## Export Pipeline
```
SceneGraph → NodeChange[] → Kiwi-кодирование → Сжатие (Zstd/deflate)
→ Сборка ZIP (заголовок + схема + сообщение + thumbnail.png)
→ Запись .fig файла
```mermaid
flowchart LR
A["SceneGraph"] --> B["NodeChange[]"]
B --> C["Kiwi encode"]
C --> D["Compress"]
D --> E["Build ZIP"]
E --> F[".fig file"]
```
Экспорт выполняется через <kbd>⌘</kbd><kbd>S</kbd> (Сохранить) и <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (Сохранить как) с нативными диалоговыми окнами ОС в десктопном приложении. Экспортированный файл включает `thumbnail.png`, необходимый Figma для предпросмотра. Сжатие использует Zstd через Tauri Rust-команду на десктопе, с fallback на deflate в браузере. ZIP-архив собирается на Rust в десктопном варианте для корректных заголовков Zstd-фреймов (с указанием размера содержимого).
Export uses <kbd>⌘</kbd><kbd>S</kbd> (Save) and <kbd>⇧</kbd><kbd>⌘</kbd><kbd>S</kbd> (Save As) with native OS dialogs on the desktop app. The exported file includes a `thumbnail.png` required by Figma for file preview.
## Бинарный кодек Kiwi
Compression uses Zstd via Tauri Rust command on desktop, with deflate fallback in the browser.
Кодек обрабатывает 194-определённую Kiwi-схему Figma с NodeChange в качестве центрального типа (~390 полей). Основные компоненты:
## Kiwi Binary Codec
- **kiwi-schema** — вендорённый из evanw/kiwi, с патчем для ESM и разреженных ID полей
- **codec.ts** — кодирование/декодирование сообщений с использованием Kiwi-схемы
- **protocol.ts** — парсинг проводного формата и определение типа сообщения
- **schema.ts** — 194 определения message/enum/struct
The codec handles Figma's 194-definition Kiwi schema with `NodeChange` as the central type (~390 fields). Key components:
### Разреженные ID полей
| 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 |
Схема Figma использует непоследовательные ID полей (например, 1, 2, 5, 10 с пропусками). Вендорённый парсер kiwi-schema пропатчен для корректной обработки этого случая.
### 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.
Файлы .fig используют Zstd-сжатие для полезных данных схемы и сообщения. Декомпрессия выполняется библиотекой `fzstd`. При экспорте Zstd-сжатие делегируется Tauri Rust-команде в десктопном приложении (лучшая производительность, корректные заголовки фреймов). В браузере в качестве fallback используется deflate через `fflate`. Кодирование буфера обмена также использует `fflate`.
### 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.
| Формат | Импорт | Экспорт |
|--------|--------|---------|
| .fig (Figma) | ✅ | ✅ |
| .svg | Планируется | Планируется |
| .png | Планируется | Планируется |
| .pdf | — | Планируется |
## Supported Formats
Подробнее о сроках поддержки форматов см. [Дорожная карта](/ru/development/roadmap).
| Format | Import | Export |
|--------|--------|--------|
| `.fig` (Figma) | ✅ | ✅ |
| `.svg` | Planned | Planned |
| `.png` | Planned | Planned |
| `.pdf` | — | Planned |
## Формат буфера обмена
## Clipboard Format
Копирование/вставка использует то же бинарное Kiwi-кодирование:
Copy/paste uses the same Kiwi binary encoding:
1. **Копирование** — кодирование выбранных NodeChange[] в бинарный Kiwi, сжатие, запись в буфер обмена с MIME-типом `application/x-figma-design`
2. **Вставка** — чтение буфера обмена, декомпрессия, декодирование бинарного Kiwi, создание узлов в графе сцены
3. **Синхронность** — кодирование происходит в обработчике события копирования (не через асинхронный Clipboard API) для совместимости с браузерами
```mermaid
flowchart LR
subgraph Copy
A["Selected nodes"] --> B["Encode NodeChange[]"]
B --> C["Compress"]
C --> D["Clipboard<br/><code>application/x-figma-design</code>"]
end
subgraph Paste
D --> E["Decompress"]
E --> F["Decode Kiwi"]
F --> G["Create nodes"]
end
```
Это обеспечивает двусторонний обмен через буфер обмена между OpenPencil и Figma.
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -1,251 +0,0 @@
# MCP-сервер
OpenPencil включает MCP-сервер (Model Context Protocol), который позволяет AI-инструментам — Claude Code, Cursor, Windsurf и др. — читать и редактировать `.fig` файлы в headless-режиме.
Два транспорта: **stdio** для MCP-клиентов, **HTTP** для всего остального.
## Установка
```sh
bun add -g @open-pencil/mcp
```
## Stdio (Claude Code, Cursor и др.)
Добавьте в конфигурацию MCP (например, `~/.claude/settings.json` или `.cursor/mcp.json`):
```json
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
```
Или запустите из исходников без установки:
::: code-group
```json [Bun]
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
```json [Node.js]
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/index.ts"]
}
}
}
```
:::
## HTTP
Для браузерных расширений, скриптов, CI или любого HTTP-клиента:
```sh
openpencil-mcp-http
```
Или из исходников: `bun packages/mcp/src/http.ts` / `npx tsx packages/mcp/src/http.ts`
Настройки безопасности (HTTP-транспорт):
- По умолчанию привязка к `127.0.0.1` (переопределяется через `HOST`)
- Инструмент `eval` отключён
- Файловые операции ограничены `OPENPENCIL_MCP_ROOT` (по умолчанию — текущий рабочий каталог)
- CORS отключён по умолчанию; установите `OPENPENCIL_MCP_CORS_ORIGIN` для разрешения одного origin
- Опциональный токен аутентификации: `OPENPENCIL_MCP_AUTH_TOKEN` (клиент отправляет `Authorization: Bearer <token>` или `x-mcp-token`)
Сервер запускается на порту 3100 (переопределяется через переменную окружения `PORT`). Эндпоинты:
- `GET /health` — статус сервера
- `POST /mcp` — MCP Streamable HTTP (SSE). Сессии через заголовок `mcp-session-id`.
## Рабочий процесс
1. **Открытие** — `open_file` для загрузки существующего `.fig`, или `new_document` для чистого холста
2. **Чтение** — `get_page_tree`, `find_nodes`, `get_node`, `list_pages`
3. **Создание** — `create_shape`, `render` (JSX)
4. **Изменение** — `set_fill`, `set_stroke`, `set_layout`, `update_node`, `set_effects`
5. **Структура** — `reparent_node`, `group_nodes`, `clone_node`, `delete_node`
6. **Сохранение** — `save_file` для записи обратно в `.fig`
## Навык для AI-агента
Научите вашего AI-агента использовать инструменты OpenPencil:
```sh
npx skills add open-pencil/skills@open-pencil
```
Работает с Claude Code, Cursor, Windsurf, Codex и любым агентом, поддерживающим [skills](https://skills.sh). Навык охватывает CLI, MCP-инструменты, JSX-рендеринг, eval и мост автоматизации запущенного приложения.
## Инструменты (90)
### Документ
| Инструмент | Описание |
|------------|----------|
| `open_file` | Открыть `.fig` файл для редактирования |
| `save_file` | Сохранить текущий документ в `.fig` файл |
| `new_document` | Создать новый пустой документ |
### Чтение
| Инструмент | Описание |
|------------|----------|
| `get_selection` | Получить выделенные узлы |
| `get_page_tree` | Получить полное дерево узлов текущей страницы |
| `get_current_page` | Получить имя и ID текущей страницы |
| `get_node` | Получить свойства узла по ID |
| `find_nodes` | Найти узлы по шаблону имени и/или типу |
| `get_components` | Список всех компонентов в документе |
| `list_pages` | Список всех страниц |
| `list_variables` | Список дизайн-переменных |
| `list_collections` | Список коллекций переменных |
| `list_fonts` | Список шрифтов на текущей странице |
| `page_bounds` | Получить ограничивающий прямоугольник всех объектов на странице |
| `node_bounds` | Получить ограничивающий прямоугольник узла |
| `node_ancestors` | Получить цепочку предков узла |
| `node_children` | Получить прямых потомков узла |
| `node_tree` | Получить поддерево с корнем в узле |
| `node_bindings` | Получить привязки переменных к узлу |
### Создание
| Инструмент | Описание |
|------------|----------|
| `create_shape` | Создать фигуру (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
| `create_vector` | Создать векторный узел из строки пути |
| `create_slice` | Создать экспортную область |
| `create_page` | Создать новую страницу |
| `render` | Рендеринг JSX в узлы дизайна — создание целых деревьев компонентов за один вызов |
| `create_component` | Преобразовать фрейм/группу в компонент |
| `create_instance` | Создать экземпляр компонента |
| `node_to_component` | Преобразовать существующий узел в компонент на месте |
### Изменение
| Инструмент | Описание |
|------------|----------|
| `set_fill` | Установить цвет заливки (hex) |
| `set_stroke` | Установить цвет, толщину и выравнивание обводки |
| `set_effects` | Добавить тень или размытие |
| `update_node` | Обновить позицию, размер, непрозрачность, радиус скругления, текст, шрифт |
| `set_layout` | Установить auto-layout (flexbox) — направление, отступы, выравнивание |
| `set_constraints` | Установить ограничения масштабирования |
| `set_rotation` | Установить угол поворота в градусах |
| `set_opacity` | Установить непрозрачность (0–1) |
| `set_radius` | Установить радиус скругления (единый или для каждого угла) |
| `set_minmax` | Установить ограничения мин./макс. ширины и высоты |
| `set_text` | Установить текстовое содержимое узла TEXT |
| `set_font` | Установить семейство и начертание шрифта |
| `set_font_range` | Установить свойства шрифта для диапазона символов |
| `set_text_resize` | Установить режим авторазмера текста (fixed/auto-width/auto-height) |
| `set_visible` | Показать или скрыть узел |
| `set_blend` | Установить режим наложения |
| `set_locked` | Заблокировать или разблокировать узел |
| `set_stroke_align` | Установить выравнивание обводки (inside/center/outside) |
| `set_text_properties` | Установить свойства текста: выравнивание, авторазмер, регистр, декор, обрезка |
| `set_layout_child` | Настроить дочерний элемент auto-layout: размер, grow, выравнивание, абсолютное позиционирование |
| `node_move` | Переместить узел на новую позицию |
| `node_resize` | Изменить размер узла |
| `node_replace_with` | Заменить узел другим узлом |
| `arrange` | Выровнять или распределить выделенные узлы |
### Структура
| Инструмент | Описание |
|------------|----------|
| `delete_node` | Удалить узел |
| `clone_node` | Дублировать узел |
| `rename_node` | Переименовать узел |
| `reparent_node` | Переместить узел в другой родительский элемент |
| `select_nodes` | Выделить узлы по ID |
| `group_nodes` | Сгруппировать узлы |
| `ungroup_node` | Разгруппировать группу |
| `flatten_nodes` | Объединить узлы в один вектор |
| `boolean_union` | Булево объединение двух и более узлов |
| `boolean_subtract` | Булево вычитание |
| `boolean_intersect` | Булево пересечение |
| `boolean_exclude` | Булево исключение |
### Векторный путь
| Инструмент | Описание |
|------------|----------|
| `path_get` | Получить данные пути векторного узла |
| `path_set` | Установить данные пути векторного узла |
| `path_scale` | Масштабировать векторный путь |
| `path_flip` | Отразить векторный путь по горизонтали или вертикали |
| `path_move` | Сдвинуть векторный путь |
### Экспорт
| Инструмент | Описание |
|------------|----------|
| `export_image` | Экспортировать узлы в PNG, JPG или WEBP. Возвращает base64-кодированные данные |
| `export_svg` | Экспортировать узлы как SVG-разметку |
### Область просмотра
| Инструмент | Описание |
|------------|----------|
| `viewport_get` | Получить текущую позицию и масштаб области просмотра |
| `viewport_set` | Установить позицию и масштаб области просмотра |
| `viewport_zoom_to_fit` | Масштабировать область просмотра по указанным узлам |
### Переменные
| Инструмент | Описание |
|------------|----------|
| `get_variable` | Получить переменную по ID или имени |
| `find_variables` | Найти переменные по шаблону имени или типу |
| `create_variable` | Создать новую переменную в коллекции |
| `set_variable` | Установить значение переменной в режиме |
| `delete_variable` | Удалить переменную |
| `bind_variable` | Привязать переменную к свойству узла |
| `get_collection` | Получить коллекцию переменных по ID или имени |
| `create_collection` | Создать новую коллекцию переменных |
| `delete_collection` | Удалить коллекцию переменных |
### Анализ
| Инструмент | Описание |
|------------|----------|
| `analyze_colors` | Анализ использования цветовой палитры в документе |
| `analyze_typography` | Анализ распределения шрифтов/размеров/начертаний |
| `analyze_spacing` | Анализ значений gap и padding |
| `analyze_clusters` | Обнаружение повторяющихся паттернов (потенциальных компонентов) |
### Сравнение
| Инструмент | Описание |
|------------|----------|
| `diff_create` | Создать снимок текущего состояния документа |
| `diff_show` | Показать различия между текущим состоянием и снимком |
### Навигация
| Инструмент | Описание |
|------------|----------|
| `switch_page` | Переключиться на страницу по имени или ID |
### Произвольный код
| Инструмент | Описание |
|------------|----------|
| `eval` | Выполнить JavaScript с полным доступом к Figma Plugin API |
Примечание: `eval` доступен через stdio, но отключён в HTTP-режиме из соображений безопасности.

View file

@ -1,48 +1,48 @@
# Типы узлов
# Node Types
Граф сцены поддерживает 28 типов узлов из Kiwi-схемы Figma. Каждый узел идентифицируется GUID (`sessionID:localID`) и содержит ссылку на родителя через `parentIndex`. Объединение `NodeType` движка OpenPencil использует 17 из этих типов.
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 17 of these types.
## Таблица типов
## Type Table
28 типов из схемы Figma + 1 синтетический тип движка. Типы, отмеченные ✅, входят в объединение `NodeType` движка (всего 17).
28 Figma schema types + 1 synthetic engine type. Types marked ✅ are in the engine's `NodeType` union (17 total).
| Тип | ID | Описание | Движок |
|-----|----|----------|--------|
| DOCUMENT | 1 | Корневой узел, один на файл | — |
| CANVAS | 2 | Страница | ✅ |
| GROUP | 3 | Контейнер-группа | ✅ |
| FRAME | 4 | Основной контейнер (артборд), поддерживает auto-layout | ✅ |
| BOOLEAN_OPERATION | 5 | Результат union/subtract/intersect/exclude | |
| VECTOR | 6 | Свободный векторный путь | ✅ |
| STAR | 7 | Звезда | ✅ |
| LINE | 8 | Линия | ✅ |
| ELLIPSE | 9 | Эллипс/круг, поддерживает данные дуги | ✅ |
| RECTANGLE | 10 | Прямоугольник | ✅ |
| REGULAR_POLYGON | 11 | Правильный многоугольник (3–12 сторон, в движке используется `POLYGON`) | ✅ |
| ROUNDED_RECTANGLE | 12 | Прямоугольник со сглаженными углами | ✅ |
| TEXT | 13 | Текст с форматированием | ✅ |
| SLICE | 14 | Область экспорта | |
| SYMBOL | 15 | Компонент (основной, в движке используется `COMPONENT`) | ✅ |
| INSTANCE | 16 | Экземпляр компонента | ✅ |
| STICKY | 17 | Стикер FigJam | |
| SHAPE_WITH_TEXT | 18 | Фигура FigJam | ✅ |
| CONNECTOR | 19 | Соединительная линия между узлами | ✅ |
| CODE_BLOCK | 20 | Блок кода FigJam | |
| WIDGET | 21 | Виджет плагина | |
| STAMP | 22 | Штамп FigJam | |
| MEDIA | 23 | Видео/GIF | |
| HIGHLIGHT | 24 | Выделение FigJam | |
| SECTION | 25 | Секция холста (организационная, только верхний уровень) | ✅ |
| SECTION_OVERLAY | 26 | Оверлей секции | |
| WASHI_TAPE | 27 | Васи-тейп FigJam | |
| VARIABLE | 28 | Узел определения переменной | |
| COMPONENT_SET | — | Контейнер группы вариантов (синтетический, отображается из SYMBOL) | ✅ |
| 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`) | ✅ |
### Объединение NodeType движка (17 типов)
### Engine NodeType Union (17 types)
Движок использует упрощённые имена. Некоторые отличаются от Kiwi-схемы:
The engine's `NodeType` uses simplified names. Some differ from the Kiwi schema:
- `COMPONENT` → Kiwi `SYMBOL` (ID 15)
- `COMPONENT_SET` → контейнер группы вариантов (нет выделенного Kiwi ID, отображается из SYMBOL с вариантами)
- `COMPONENT_SET` → variant group container (no dedicated Kiwi ID, mapped from `SYMBOL` with variants)
- `POLYGON` → Kiwi `REGULAR_POLYGON` (ID 11)
```typescript
@ -54,81 +54,81 @@ type NodeType =
| 'CONNECTOR' | 'SHAPE_WITH_TEXT'
```
## Иерархия узлов
## Node Hierarchy
```
Document
├── Canvas (Страница 1)
│ ├── Section (только верхний уровень, плашка с названием, авто-захват соседей)
├── Canvas (Page 1)
│ ├── Section (top-level only, title pill, auto-adopts siblings)
│ │ ├── Frame
│ │ │ └── ...дочерние элементы
│ │ │ └── ...children
│ │ └── Rectangle
│ ├── Frame
│ │ ├── Rectangle
│ │ ├── Text
│ │ └── Frame (вложенный)
│ │ └── Frame (nested)
│ │ ├── Ellipse
│ │ └── Instance (→ ссылается на Component)
│ │ └── Instance (→ references Component)
│ ├── Component
│ │ └── ...дочерние элементы
│ │ └── ...children
│ ├── Group
│ │ └── ...дочерние элементы
│ │ └── ...children
│ └── BooleanOperation
│ └── ...операнды
└── Canvas (Страница 2)
│ └── ...operand shapes
└── Canvas (Page 2)
└── ...
```
## Основные свойства
## Core Properties
Каждый узел содержит следующие поля (подмножество NodeChange):
Every node carries these fields (subset of `NodeChange`):
### Идентификация и дерево
### Identity & Tree
- `guid` — уникальный идентификатор (`sessionID:localID`)
- `type` — перечисление типа узла
- `name` — отображаемое имя
- `phase` — CREATED или REMOVED
- `parentIndex` — GUID родителя + строка позиции для z-упорядочивания
- `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` — вектор ширины/высоты
- `transform` — аффинная матрица 2×3
- `rotation` — градусы
- `size` — width/height vector
- `transform` — 2×3 affine matrix
- `rotation` — degrees
### Внешний вид
### Appearance
- `fillPaints[]` — заливки цветом/градиентом/изображением
- `strokePaints[]` — цвета обводки
- `effects[]` — тени, размытия
- `fillPaints[]` — fill colors/gradients/images
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN и др.
- `blendMode` — `NORMAL`, `MULTIPLY`, `SCREEN`, etc.
### Обводка
### Stroke
- `strokeWeight` — толщина обводки
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `dashPattern[]` — длины штрихов/промежутков
- `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` — единый радиус
- `cornerSmoothing` — степень сквиркла (0–1)
- Индивидуальные радиусы углов, когда `rectangleCornerRadiiIndependent` равно true
- `cornerRadius` — uniform radius
- `cornerSmoothing` — squircle amount (0–1)
- Per-corner radii when `rectangleCornerRadiiIndependent` is true
### Видимость
### Visibility
- `visible` — показать/скрыть
- `locked` — предотвратить редактирование
- `visible` — show/hide
- `locked` — prevent editing
## Свойства отдельных типов
## Type-Specific Properties
### Text
`fontSize`, `fontName`, `lineHeight`, `letterSpacing`, `textAlignHorizontal`, `textAlignVertical`, `textAutoResize`, `textData` (символы, переопределения стилей, базовые линии, глифы)
`fontSize`, `fontName`, `lineHeight`, `letterSpacing`, `textAlignHorizontal`, `textAlignVertical`, `textAutoResize`, `textData` (characters, style overrides, baselines, glyphs)
### Vector
@ -156,9 +156,9 @@ interface Fill {
opacity: number // 0–1
visible: boolean
blendMode?: BlendMode
gradientStops?: GradientStop[] // для градиентов
gradientTransform?: GradientTransform // матрица 2×3
imageHash?: string // для заливок изображением
gradientStops?: GradientStop[] // for gradients
gradientTransform?: GradientTransform // 2×3 matrix
imageHash?: string // for image fills
imageScaleMode?: 'FILL' | 'FIT' | 'CROP' | 'TILE'
imageTransform?: GradientTransform
}

View file

@ -1,8 +1,8 @@
# Граф сцены
# Scene Graph
## Представление в памяти
## In-Memory Representation
Узлы хранятся в плоском `Map<string, Node>` с ключами-GUID. Древовидная структура поддерживается через ссылки `parentIndex`. Это обеспечивает O(1) поиск по ID и эффективный обход.
Nodes live in a flat `Map<string, Node>` 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 {
@ -29,38 +29,38 @@ interface SceneGraph {
}
```
## Страницы
## Pages
Документы поддерживают несколько страниц (узлы CANVAS как прямые потомки корня DOCUMENT). Каждая страница имеет собственное дерево дочерних элементов и независимое состояние области просмотра (panX, panY, zoom, pageColor). Редактор отслеживает `currentPageId` и отрисовывает только дочерние элементы активной страницы.
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 — это организационные контейнеры верхнего уровня (только прямые потомки CANVAS). Они не могут вкладываться во фреймы или группы. Создание секции автоматически захватывает перекрывающихся соседей. Секции отображают плашку с заголовком, цвет текста которой адаптируется в зависимости от яркости фона.
`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
Состояние редактора отслеживает `hoveredNodeId` — узел, находящийся под курсором. Отрисовщик рисует контур наведения, повторяющий форму объекта (по фактической геометрии для эллипсов, скруглённых прямоугольников, векторов), для визуальной обратной связи перед выделением.
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
Система использует паттерн **обратных команд** Figma. Каждая запись отмены содержит прямые изменения и их автоматически вычисленную инверсию:
The system uses Figma's **inverse command** pattern. Each undo entry contains the forward changes and their automatically-computed inverse:
| Операция | Прямое действие | Обратное действие |
|----------|-----------------|-------------------|
| Создание узла | `{guid, phase: CREATED, ...props}` | `{guid, phase: REMOVED}` |
| Удаление узла | `{guid, phase: REMOVED}` | `{guid, phase: CREATED, ...allProps}` |
| Изменение свойства | `{guid, fill: "#F00"}` | `{guid, fill: "#00F"}` |
| Перемещение узла | `{guid, parentIndex: newParent}` | `{guid, parentIndex: oldParent}` |
| 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.
**Группировка** — такие операции, как перетаскивание, генерируют сотни изменений позиции в секунду. Они объединяются в одну запись отмены с помощью дебаунса. `beginBatch`/`commitBatch` оборачивает многоступенчатые операции.
**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.
## Движок компоновки (Yoga)
## Layout Engine (Yoga)
Свойства auto-layout Figma отображаются на Yoga flexbox:
Figma's auto-layout properties map to Yoga flexbox:
| Свойство Figma | Эквивалент Yoga |
| Figma Property | Yoga Equivalent |
|---|---|
| `stackMode: HORIZONTAL` | `flexDirection: row` |
| `stackMode: VERTICAL` | `flexDirection: column` |
@ -73,27 +73,27 @@ interface SceneGraph {
| `stackChildAlignSelf` | `alignSelf` |
| `stackPositioning: ABSOLUTE` | `position: absolute` |
## Проверка попадания (Hit Testing)
## Hit Testing
Для заданной точки в координатах холста граф сцены возвращает самый верхний видимый узел в этой позиции. Алгоритм:
Given a point in canvas coordinates, the scene graph returns the topmost visible node at that position. The algorithm:
1. Обход видимых узлов в обратном z-порядке (сверху вниз)
2. Преобразование точки проверки в локальную систему координат каждого узла
3. Проверка, находится ли точка в пределах границ узла (с учётом поворота)
4. Возврат первого совпадения
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
Для выделения рамкой `getNodesInRect` возвращает все узлы, чьи границы пересекаются с заданным прямоугольником.
For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect the given rectangle.
## Расширенные типы заливок
## Extended Fill Types
Заливки поддерживают шесть типов: SOLID, GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_ANGULAR, GRADIENT_DIAMOND и IMAGE. Градиентные заливки содержат `gradientStops` (пары цвет + позиция) и `gradientTransform` (матрица 2×3). Заливки изображением ссылаются на бинарные данные через `imageHash` с режимами масштабирования (FILL, FIT, CROP, TILE).
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
Обводки поддерживают `cap` (NONE, ROUND, SQUARE, ARROW_LINES, ARROW_EQUILATERAL), `join` (MITER, BEVEL, ROUND) и `dashPattern` (массив длин штрихов/промежутков) в дополнение к базовым свойствам color, weight, opacity, visible и align.
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.
Поворот хранится в градусах и применяется как часть аффинной матрицы 2×3. Направляющие привязки и проверка попадания учитывают поворот при вычислении визуальных границ.
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.