Port figma-use tools: variables, set_*, node ops, boolean, paths, viewport

Add 40+ new tool definitions ported from figma-use:

Granular set tools: set_rotation, set_opacity, set_radius, set_minmax,
set_text, set_font, set_font_range, set_text_resize, set_visible,
set_blend, set_locked, set_stroke_align

Node operations: node_bounds, node_move, node_resize, node_ancestors,
node_children, node_tree, node_bindings, node_replace_with

Variable CRUD: get_variable, find_variables, create_variable,
set_variable, delete_variable, bind_variable

Collection CRUD: get_collection, create_collection, delete_collection

Boolean operations: boolean_union, boolean_subtract, boolean_intersect,
boolean_exclude

Vector path tools: path_get, path_set, path_scale, path_flip, path_move

Create tools: create_page, create_vector, create_slice

Misc: flatten_nodes, viewport_get, viewport_set, viewport_zoom_to_fit,
page_bounds, list_fonts

Also adds createVariable/createCollection to SceneGraph, and viewport,
boolean operation, and flatten methods to FigmaAPI.
This commit is contained in:
Danila Poyarkov 2026-03-02 19:27:42 +03:00
parent b7a634f1ae
commit 537663aa3c
4 changed files with 1151 additions and 3 deletions

View file

@ -8,7 +8,9 @@ import type {
Effect,
LayoutMode,
Variable,
VariableCollection
VariableCollection,
VariableType,
VariableValue
} from './scene-graph'
import type { Rect } from './types'
@ -1163,6 +1165,100 @@ export class FigmaAPI {
return this.graph.variableCollections.get(id) ?? null
}
// --- Variable/Collection CRUD ---
createVariable(
name: string,
type: VariableType,
collectionId: string,
value?: VariableValue
): Variable {
return this.graph.createVariable(name, type, collectionId, value)
}
setVariableValue(variableId: string, modeId: string, value: VariableValue): void {
const variable = this.graph.variables.get(variableId)
if (!variable) throw new Error(`Variable "${variableId}" not found`)
variable.valuesByMode[modeId] = value
}
deleteVariable(id: string): void {
this.graph.removeVariable(id)
}
createVariableCollection(name: string): VariableCollection {
return this.graph.createCollection(name)
}
deleteVariableCollection(id: string): void {
this.graph.removeCollection(id)
}
bindVariable(nodeId: string, field: string, variableId: string): void {
this.graph.bindVariable(nodeId, field, variableId)
}
unbindVariable(nodeId: string, field: string): void {
this.graph.unbindVariable(nodeId, field)
}
// --- Boolean Operations ---
booleanOperation(
operation: 'UNION' | 'SUBTRACT' | 'INTERSECT' | 'EXCLUDE',
nodeIds: string[]
): FigmaNodeProxy {
if (nodeIds.length < 2) throw new Error('Need at least 2 nodes for boolean operation')
const nodes = nodeIds.map((id) => this.graph.getNode(id))
if (nodes.some((n) => !n)) throw new Error('One or more nodes not found')
const first = nodes[0]!
const parentId = first.parentId ?? this._currentPageId
const group = this.graph.createNode('GROUP', parentId, {
name: `Boolean ${operation.toLowerCase()}`,
x: first.x,
y: first.y,
width: first.width,
height: first.height
})
for (const id of nodeIds) {
this.graph.reparentNode(id, group.id)
}
return this.wrapNode(group.id)
}
// --- Flatten ---
flattenNode(nodeIds: string[]): FigmaNodeProxy {
if (nodeIds.length === 0) throw new Error('Need at least 1 node to flatten')
const first = this.graph.getNode(nodeIds[0])
if (!first) throw new Error('Node not found')
const parentId = first.parentId ?? this._currentPageId
const vector = this.graph.createNode('VECTOR', parentId, {
name: 'Flatten',
x: first.x,
y: first.y,
width: first.width,
height: first.height,
fills: structuredClone(first.fills)
})
for (const id of nodeIds) {
this.graph.deleteNode(id)
}
return this.wrapNode(vector.id)
}
// --- Viewport ---
private _viewport = { x: 0, y: 0, zoom: 1 }
get viewport(): { center: { x: number; y: number }; zoom: number } {
return { center: { x: this._viewport.x, y: this._viewport.y }, zoom: this._viewport.zoom }
}
set viewport(v: { center: { x: number; y: number }; zoom: number }) {
this._viewport = { x: v.center.x, y: v.center.y, zoom: v.zoom }
}
// --- Stubs ---
async loadFontAsync(_fontName: FigmaFontName): Promise<void> {

View file

@ -4,6 +4,7 @@ export * from './constants'
export {
SceneGraph,
generateId,
type SceneNode,
type NodeType,
type Fill,

View file

@ -295,7 +295,7 @@ export interface VariableCollection {
let nextLocalID = 1
function generateId(): string {
export function generateId(): string {
return `0:${nextLocalID++}`
}
@ -467,6 +467,55 @@ export class SceneGraph {
}
}
createVariable(
name: string,
type: VariableType,
collectionId: string,
value?: VariableValue
): Variable {
const collection = this.variableCollections.get(collectionId)
if (!collection) throw new Error(`Collection "${collectionId}" not found`)
const id = generateId()
const defaultValue =
value ??
(type === 'COLOR'
? { r: 0, g: 0, b: 0, a: 1 }
: type === 'FLOAT'
? 0
: type === 'BOOLEAN'
? false
: '')
const valuesByMode: Record<string, VariableValue> = {}
for (const mode of collection.modes) {
valuesByMode[mode.modeId] = structuredClone(defaultValue)
}
const variable: Variable = {
id,
name,
type,
collectionId,
valuesByMode,
description: '',
hiddenFromPublishing: false
}
this.addVariable(variable)
return variable
}
createCollection(name: string): VariableCollection {
const id = generateId()
const modeId = generateId()
const collection: VariableCollection = {
id,
name,
modes: [{ modeId, name: 'Mode 1' }],
defaultModeId: modeId,
variableIds: []
}
this.addCollection(collection)
return collection
}
removeCollection(id: string): void {
const collection = this.variableCollections.get(id)
if (collection) {

File diff suppressed because it is too large Load diff