openpencil/packages/core/src/tools/create.ts
Danila Poyarkov a2d7b89709 Adopt motion-v for mobile drawer and toolbar animations
- MobileDrawer: spring-animated height via motion.div :animate + @pan,
  replaces useSwipe + manual rAF. Tab state always set (never null),
  content stays rendered when drawer is closed.
- Toolbar: AnimatePresence with directional slide variants and
  layout-animated width via motion.div layout, replaces manual
  scrollWidth measuring + inline CSS transitions.
- Add SWIPE_VELOCITY_THRESHOLD constant.
- activeRibbonTab type narrowed from union | null to union.
2026-03-06 08:50:38 +03:00

189 lines
6.6 KiB
TypeScript

import { parseColor } from '../color'
import { defineTool, nodeSummary } from './schema'
import type { FigmaNodeProxy } from '../figma-api'
export const createShape = defineTool({
name: 'create_shape',
mutates: true,
description:
'Create a shape on the canvas. Use FRAME for containers/cards, RECTANGLE for solid blocks, ELLIPSE for circles, TEXT for labels, SECTION for page sections.',
params: {
type: {
type: 'string',
description: 'Node type',
required: true,
enum: ['FRAME', 'RECTANGLE', 'ELLIPSE', 'TEXT', 'LINE', 'STAR', 'POLYGON', 'SECTION']
},
x: { type: 'number', description: 'X position', required: true },
y: { type: 'number', description: 'Y position', required: true },
width: { type: 'number', description: 'Width in pixels', required: true, min: 1 },
height: { type: 'number', description: 'Height in pixels', required: true, min: 1 },
name: { type: 'string', description: 'Node name shown in layers panel' },
parent_id: { type: 'string', description: 'Parent node ID to nest inside' }
},
execute: (figma, args) => {
const parentId = args.parent_id
const parent = parentId ? figma.getNodeById(parentId) : null
const createMap: Record<string, () => FigmaNodeProxy> = {
FRAME: () => figma.createFrame(),
RECTANGLE: () => figma.createRectangle(),
ELLIPSE: () => figma.createEllipse(),
TEXT: () => figma.createText(),
LINE: () => figma.createLine(),
STAR: () => figma.createStar(),
POLYGON: () => figma.createPolygon(),
SECTION: () => figma.createSection()
}
const node = createMap[args.type]!()
node.x = args.x
node.y = args.y
node.resize(args.width, args.height)
if (args.name) node.name = args.name
if (parent) parent.appendChild(node)
return nodeSummary(node)
}
})
export const render = defineTool({
name: 'render',
mutates: true,
description:
'Render JSX to design nodes. Primary creation tool — creates entire component trees in one call. Example: <Frame name="Card" w={320} h="hug" flex="col" gap={16} p={24} bg="#FFF" rounded={16}><Text size={18} weight="bold">Title</Text></Frame>',
params: {
jsx: { type: 'string', description: 'JSX string to render', required: true },
x: { type: 'number', description: 'X position of the root node' },
y: { type: 'number', description: 'Y position of the root node' },
parent_id: { type: 'string', description: 'Parent node ID to render into' }
},
execute: async (figma, args) => {
const { renderJsx } = await import('../render/render-jsx.js')
const result = await renderJsx(figma.graph, args.jsx, {
parentId: args.parent_id ?? figma.currentPageId,
x: args.x,
y: args.y
})
return { id: result.id, name: result.name, type: result.type, children: result.childIds }
}
})
export const createComponent = defineTool({
name: 'create_component',
mutates: true,
description: 'Convert a frame/group into a component.',
params: {
id: { type: 'string', description: 'Node ID to convert', required: true }
},
execute: (figma, { id }) => {
const node = figma.getNodeById(id)
if (!node) return { error: `Node "${id}" not found` }
const comp = figma.createComponentFromNode(node)
return nodeSummary(comp)
}
})
export const createInstance = defineTool({
name: 'create_instance',
mutates: true,
description: 'Create an instance of a component.',
params: {
component_id: { type: 'string', description: 'Component node ID', required: true },
x: { type: 'number', description: 'X position' },
y: { type: 'number', description: 'Y position' }
},
execute: (figma, args) => {
const comp = figma.getNodeById(args.component_id)
if (!comp) return { error: `Component "${args.component_id}" not found` }
const instance = comp.createInstance()
if (args.x !== undefined) instance.x = args.x
if (args.y !== undefined) instance.y = args.y
return nodeSummary(instance)
}
})
export const createPage = defineTool({
name: 'create_page',
mutates: true,
description: 'Create a new page.',
params: {
name: { type: 'string', description: 'Page name', required: true }
},
execute: (figma, { name }) => {
const page = figma.createPage()
page.name = name
return { id: page.id, name }
}
})
export const createVector = defineTool({
name: 'create_vector',
mutates: true,
description: 'Create a vector node with optional path data.',
params: {
x: { type: 'number', description: 'X position', required: true },
y: { type: 'number', description: 'Y position', required: true },
name: { type: 'string', description: 'Node name' },
path: { type: 'string', description: 'VectorNetwork JSON' },
fill: { type: 'color', description: 'Fill color (hex)' },
stroke: { type: 'color', description: 'Stroke color (hex)' },
stroke_weight: { type: 'number', description: 'Stroke weight' },
parent_id: { type: 'string', description: 'Parent node ID' }
},
execute: (figma, args) => {
const node = figma.createVector()
node.x = args.x
node.y = args.y
if (args.name) node.name = args.name
if (args.path) {
figma.graph.updateNode(node.id, { vectorNetwork: JSON.parse(args.path) } as any)
}
if (args.fill) {
node.fills = [{ type: 'SOLID', color: parseColor(args.fill), opacity: 1, visible: true }]
}
if (args.stroke) {
node.strokes = [
{
color: parseColor(args.stroke),
weight: args.stroke_weight ?? 1,
opacity: 1,
visible: true,
align: 'CENTER'
}
]
}
if (args.parent_id) {
const parent = figma.getNodeById(args.parent_id)
if (parent) parent.appendChild(node)
}
return nodeSummary(node)
}
})
export const createSlice = defineTool({
name: 'create_slice',
mutates: true,
description: 'Create a slice (export region) on the canvas.',
params: {
x: { type: 'number', description: 'X position', required: true },
y: { type: 'number', description: 'Y position', required: true },
width: { type: 'number', description: 'Width', required: true, min: 1 },
height: { type: 'number', description: 'Height', required: true, min: 1 },
name: { type: 'string', description: 'Slice name' },
parent_id: { type: 'string', description: 'Parent node ID' }
},
execute: (figma, args) => {
const node = figma.createFrame()
node.x = args.x
node.y = args.y
node.resize(args.width, args.height)
node.name = args.name ?? 'Slice'
node.fills = []
if (args.parent_id) {
const parent = figma.getNodeById(args.parent_id)
if (parent) parent.appendChild(node)
}
return nodeSummary(node)
}
})