Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates). |
||
|---|---|---|
| .. | ||
| src | ||
| CLAUDE.md | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@zseven-w/pen-engine
Headless design engine for OpenPencil — framework-free document management, selection, history, viewport, and spatial queries. Build your own design tool UI on top of this engine.
Install
npm install @zseven-w/pen-engine
# or
bun add @zseven-w/pen-engine
Overview
pen-engine is the core runtime that powers OpenPencil's editor. It manages the entire document lifecycle without any framework dependency — React, Vue, Svelte, or vanilla JS all work. The optional browser.ts entry adds GPU-accelerated canvas rendering via CanvasKit/Skia.
DesignEngine
|- DocumentManager Node CRUD, tree operations
|- SelectionManager Multi-select, hover tracking
|- HistoryManager Undo/redo with debounce + batch
|- PageManager Multi-page support
|- VariableManager Design variables ($refs)
|- ViewportController Zoom, pan, coordinate transforms
|- SpatialIndex R-tree for hit testing & spatial queries
|- EventEmitter Typed event system
Quick Start
import { DesignEngine } from '@zseven-w/pen-engine';
const engine = new DesignEngine();
// Load or create a document
engine.loadDocument(myDocument);
// Add a node
engine.addNode(null, {
id: 'frame-1',
type: 'frame',
name: 'Header',
width: 1200,
height: 80,
layout: 'horizontal',
});
// Select, undo, inspect
engine.select(['frame-1']);
engine.undo();
console.log(engine.getDocument());
Features
Document Operations
Create, query, and mutate the node tree:
engine.addNode(parentId, node, index?);
engine.updateNode(id, { fill: [{ type: 'solid', color: '#FF0000' }] });
engine.removeNode(id);
engine.moveNode(id, newParentId, index);
engine.duplicateNode(id);
engine.groupNodes(['node-1', 'node-2']);
engine.ungroupNode(groupId);
engine.getNodeById(id);
Selection & Hover
engine.select(['node-1', 'node-2']);
engine.clearSelection();
engine.getSelection(); // string[]
engine.setHoveredId('node-3');
engine.getHoveredId(); // string | null
History (Undo / Redo)
Structural history with debouncing and batch support:
engine.undo();
engine.redo();
engine.canUndo; // boolean
engine.canRedo; // boolean
// Batch multiple mutations into a single history entry
engine.batch(() => {
engine.updateNode('a', { x: 100 });
engine.updateNode('b', { x: 200 });
});
Viewport
Pan, zoom, and coordinate conversion:
engine.setViewport(zoom, panX, panY);
engine.zoomToRect(x, y, w, h, containerW, containerH);
engine.getContentBounds(); // { x, y, w, h } | null
engine.screenToScene(screenX, screenY); // { x, y }
engine.sceneToScreen(sceneX, sceneY); // { x, y }
Hit Testing (Spatial Index)
R-tree backed queries for click and marquee selection:
engine.hitTest(x, y); // PenNode | null
engine.searchRect(x, y, w, h); // PenNode[]
Multi-Page
engine.addPage(); // returns pageId
engine.removePage(pageId);
engine.setActivePage(pageId);
engine.getActivePage();
Design Variables
engine.setVariable('primary', { type: 'color', value: '#2563EB' });
engine.removeVariable('primary');
engine.renameVariable('primary', 'brand');
engine.resolveVariable('$primary'); // '#2563EB'
SVG Import
Isomorphic SVG parser (DOM in browser, regex fallback in Node.js):
import { parseSvgToNodes } from '@zseven-w/pen-engine';
const nodes = parseSvgToNodes(svgString, 400);
engine.addNode(null, nodes[0]);
Events
Typed event system for reactive UI binding:
const unsub = engine.on('document:change', (doc) => {
/* re-render */
});
engine.on('selection:change', (ids) => {
/* update UI */
});
engine.on('viewport:change', (viewport) => {
/* update zoom indicator */
});
unsub(); // unsubscribe
Browser Canvas (Optional)
GPU-accelerated rendering via CanvasKit/Skia — import from @zseven-w/pen-engine/browser:
import { attachCanvas, attachInteraction } from '@zseven-w/pen-engine/browser';
const binding = await attachCanvas(engine, canvasElement);
const detach = attachInteraction(engine, canvasElement);
// Later
binding.dispose();
detach();
API Reference
| Method | Description |
|---|---|
loadDocument(doc) |
Load a PenDocument |
getDocument() |
Get current document |
createDocument() |
Create empty document |
addNode(parent, node, index?) |
Insert node |
updateNode(id, updates) |
Partial update |
removeNode(id) |
Delete node + children |
moveNode(id, parent, index) |
Reparent node |
duplicateNode(id) |
Deep clone |
groupNodes(ids) |
Group into frame |
ungroupNode(id) |
Dissolve group |
select(ids) |
Set selection |
undo() / redo() |
History navigation |
batch(fn) |
Batch mutations |
setViewport(z, x, y) |
Set viewport |
hitTest(x, y) |
Point query |
searchRect(x, y, w, h) |
Area query |
importSVG(svg, parent?) |
Parse and insert SVG |
dispose() |
Clean up resources |