- Replace all no-explicit-any with proper types or justified eslint-disable - Extract scene-graph-hit-test.ts (hit testing), scene-graph-instances.ts (component/instance sync) from scene-graph.ts - Extract renderer/text.ts (text rendering), renderer/pen-overlay.ts (vector pen tool overlay) from renderer.ts - Extract figma-api-proxy.ts (FigmaNodeProxy class) from figma-api.ts - Extract kiwi-convert-overrides.ts from kiwi-convert.ts - Add eslint-disable with justification for remaining large class files - Fix unused imports from splits - Warnings: 42 → 2 (only unavoidable no-implied-eval in vendored code)
978 lines
25 KiB
TypeScript
978 lines
25 KiB
TypeScript
/* eslint-disable max-lines -- Figma Plugin API proxy; FigmaAPI already in separate file */
|
|
import type {
|
|
SceneGraph,
|
|
SceneNode,
|
|
NodeType,
|
|
Fill,
|
|
Stroke,
|
|
Effect,
|
|
LayoutMode,
|
|
} from './scene-graph'
|
|
import type { Rect } from './types'
|
|
import { copyFills, copyStrokes, copyEffects } from './copy'
|
|
|
|
const MIXED = Symbol('mixed')
|
|
|
|
export type FigmaFontName = { family: string; style: string }
|
|
|
|
export function weightToStyleName(weight: number, italic: boolean): string {
|
|
const names: Record<number, string> = {
|
|
100: 'Thin',
|
|
200: 'Extra Light',
|
|
300: 'Light',
|
|
400: 'Regular',
|
|
500: 'Medium',
|
|
600: 'Semi Bold',
|
|
700: 'Bold',
|
|
800: 'Extra Bold',
|
|
900: 'Black'
|
|
}
|
|
const base = names[weight] ?? 'Regular'
|
|
return italic ? `${base} Italic` : base
|
|
}
|
|
|
|
export function styleNameToWeight(style: string): { weight: number; italic: boolean } {
|
|
const lower = style.toLowerCase()
|
|
const italic = lower.includes('italic')
|
|
const clean = lower.replace(/italic/i, '').trim()
|
|
const map: Record<string, number> = {
|
|
thin: 100,
|
|
'extra light': 200,
|
|
'ultra light': 200,
|
|
light: 300,
|
|
regular: 400,
|
|
'': 400,
|
|
medium: 500,
|
|
'semi bold': 600,
|
|
'demi bold': 600,
|
|
bold: 700,
|
|
'extra bold': 800,
|
|
'ultra bold': 800,
|
|
black: 900,
|
|
heavy: 900
|
|
}
|
|
return { weight: map[clean] ?? 400, italic }
|
|
}
|
|
|
|
export const INTERNAL_ID = Symbol('id')
|
|
export const INTERNAL_GRAPH = Symbol('graph')
|
|
export const INTERNAL_API = Symbol('api')
|
|
|
|
export interface NodeProxyHost {
|
|
wrapNode(id: string): FigmaNodeProxy
|
|
readonly currentPageId: string
|
|
}
|
|
|
|
export { MIXED }
|
|
|
|
export class FigmaNodeProxy {
|
|
[INTERNAL_ID]: string;
|
|
[INTERNAL_GRAPH]: SceneGraph;
|
|
[INTERNAL_API]: NodeProxyHost
|
|
|
|
constructor(id: string, graph: SceneGraph, api: NodeProxyHost) {
|
|
this[INTERNAL_ID] = id
|
|
this[INTERNAL_GRAPH] = graph
|
|
this[INTERNAL_API] = api
|
|
}
|
|
|
|
private _raw(): SceneNode {
|
|
const n = this[INTERNAL_GRAPH].getNode(this[INTERNAL_ID])
|
|
if (!n) throw new Error(`Node ${this[INTERNAL_ID]} has been removed`)
|
|
return n
|
|
}
|
|
|
|
get id(): string {
|
|
return this[INTERNAL_ID]
|
|
}
|
|
|
|
get type(): NodeType {
|
|
return this._raw().type
|
|
}
|
|
|
|
get name(): string {
|
|
return this._raw().name
|
|
}
|
|
|
|
set name(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { name: v })
|
|
}
|
|
|
|
get removed(): boolean {
|
|
return !this[INTERNAL_GRAPH].getNode(this[INTERNAL_ID])
|
|
}
|
|
|
|
// --- Geometry ---
|
|
|
|
get x(): number {
|
|
return this._raw().x
|
|
}
|
|
|
|
set x(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { x: v })
|
|
}
|
|
|
|
get y(): number {
|
|
return this._raw().y
|
|
}
|
|
|
|
set y(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { y: v })
|
|
}
|
|
|
|
get width(): number {
|
|
return this._raw().width
|
|
}
|
|
|
|
get height(): number {
|
|
return this._raw().height
|
|
}
|
|
|
|
get rotation(): number {
|
|
return this._raw().rotation
|
|
}
|
|
|
|
set rotation(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { rotation: v })
|
|
}
|
|
|
|
resize(width: number, height: number): void {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { width, height })
|
|
}
|
|
|
|
resizeWithoutConstraints(width: number, height: number): void {
|
|
this.resize(width, height)
|
|
}
|
|
|
|
get absoluteTransform(): [[number, number, number], [number, number, number]] {
|
|
const pos = this[INTERNAL_GRAPH].getAbsolutePosition(this[INTERNAL_ID])
|
|
return [
|
|
[1, 0, pos.x],
|
|
[0, 1, pos.y]
|
|
]
|
|
}
|
|
|
|
get absoluteBoundingBox(): Rect {
|
|
return this[INTERNAL_GRAPH].getAbsoluteBounds(this[INTERNAL_ID])
|
|
}
|
|
|
|
get absoluteRenderBounds(): Rect {
|
|
return this.absoluteBoundingBox
|
|
}
|
|
|
|
// --- Visual ---
|
|
|
|
get fills(): readonly Fill[] {
|
|
return Object.freeze(copyFills(this._raw().fills))
|
|
}
|
|
|
|
set fills(v: readonly Fill[]) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { fills: [...v] })
|
|
}
|
|
|
|
get strokes(): readonly Stroke[] {
|
|
return Object.freeze(copyStrokes(this._raw().strokes))
|
|
}
|
|
|
|
set strokes(v: readonly Stroke[]) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokes: [...v] })
|
|
}
|
|
|
|
get effects(): readonly Effect[] {
|
|
return Object.freeze(copyEffects(this._raw().effects))
|
|
}
|
|
|
|
set effects(v: readonly Effect[]) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { effects: [...v] })
|
|
}
|
|
|
|
get opacity(): number {
|
|
return this._raw().opacity
|
|
}
|
|
|
|
set opacity(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { opacity: v })
|
|
}
|
|
|
|
get visible(): boolean {
|
|
return this._raw().visible
|
|
}
|
|
|
|
set visible(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { visible: v })
|
|
}
|
|
|
|
get locked(): boolean {
|
|
return this._raw().locked
|
|
}
|
|
|
|
set locked(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { locked: v })
|
|
}
|
|
|
|
get blendMode(): string {
|
|
return this._raw().blendMode
|
|
}
|
|
|
|
set blendMode(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { blendMode: v as SceneNode['blendMode'] })
|
|
}
|
|
|
|
get clipsContent(): boolean {
|
|
return this._raw().clipsContent
|
|
}
|
|
|
|
set clipsContent(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { clipsContent: v })
|
|
}
|
|
|
|
// --- Corner Radius ---
|
|
|
|
get cornerRadius(): number | typeof MIXED {
|
|
const n = this._raw()
|
|
if (n.independentCorners) return MIXED
|
|
return n.cornerRadius
|
|
}
|
|
|
|
set cornerRadius(v: number | typeof MIXED) {
|
|
if (v === MIXED) return
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
cornerRadius: v,
|
|
topLeftRadius: v,
|
|
topRightRadius: v,
|
|
bottomRightRadius: v,
|
|
bottomLeftRadius: v,
|
|
independentCorners: false
|
|
})
|
|
}
|
|
|
|
get topLeftRadius(): number {
|
|
return this._raw().topLeftRadius
|
|
}
|
|
|
|
set topLeftRadius(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
topLeftRadius: v,
|
|
independentCorners: true
|
|
})
|
|
}
|
|
|
|
get topRightRadius(): number {
|
|
return this._raw().topRightRadius
|
|
}
|
|
|
|
set topRightRadius(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
topRightRadius: v,
|
|
independentCorners: true
|
|
})
|
|
}
|
|
|
|
get bottomLeftRadius(): number {
|
|
return this._raw().bottomLeftRadius
|
|
}
|
|
|
|
set bottomLeftRadius(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
bottomLeftRadius: v,
|
|
independentCorners: true
|
|
})
|
|
}
|
|
|
|
get bottomRightRadius(): number {
|
|
return this._raw().bottomRightRadius
|
|
}
|
|
|
|
set bottomRightRadius(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
bottomRightRadius: v,
|
|
independentCorners: true
|
|
})
|
|
}
|
|
|
|
get cornerSmoothing(): number {
|
|
return this._raw().cornerSmoothing
|
|
}
|
|
|
|
set cornerSmoothing(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { cornerSmoothing: v })
|
|
}
|
|
|
|
// --- Stroke details ---
|
|
|
|
get strokeWeight(): number {
|
|
const s = this._raw().strokes
|
|
return s.length > 0 ? s[0].weight : 0
|
|
}
|
|
|
|
set strokeWeight(v: number) {
|
|
const n = this._raw()
|
|
if (n.strokes.length > 0) {
|
|
const strokes = copyStrokes(n.strokes)
|
|
strokes[0].weight = v
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokes })
|
|
}
|
|
}
|
|
|
|
get strokeAlign(): string {
|
|
const s = this._raw().strokes
|
|
return s.length > 0 ? s[0].align : 'INSIDE'
|
|
}
|
|
|
|
set strokeAlign(v: string) {
|
|
const n = this._raw()
|
|
if (n.strokes.length > 0) {
|
|
const strokes = copyStrokes(n.strokes)
|
|
strokes[0].align = v as Stroke['align']
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokes })
|
|
}
|
|
}
|
|
|
|
get dashPattern(): readonly number[] {
|
|
return Object.freeze([...this._raw().dashPattern])
|
|
}
|
|
|
|
set dashPattern(v: readonly number[]) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { dashPattern: [...v] })
|
|
}
|
|
|
|
get strokeCap(): string {
|
|
return this._raw().strokeCap
|
|
}
|
|
|
|
set strokeCap(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeCap: v as SceneNode['strokeCap'] })
|
|
}
|
|
|
|
get strokeJoin(): string {
|
|
return this._raw().strokeJoin
|
|
}
|
|
|
|
set strokeJoin(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeJoin: v as SceneNode['strokeJoin'] })
|
|
}
|
|
|
|
get strokeMiterLimit(): number {
|
|
return this._raw().strokeMiterLimit
|
|
}
|
|
|
|
set strokeMiterLimit(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeMiterLimit: v })
|
|
}
|
|
|
|
get strokeTopWeight(): number {
|
|
return this._raw().borderTopWeight
|
|
}
|
|
|
|
set strokeTopWeight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
borderTopWeight: v,
|
|
independentStrokeWeights: true
|
|
})
|
|
}
|
|
|
|
get strokeBottomWeight(): number {
|
|
return this._raw().borderBottomWeight
|
|
}
|
|
|
|
set strokeBottomWeight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
borderBottomWeight: v,
|
|
independentStrokeWeights: true
|
|
})
|
|
}
|
|
|
|
get strokeLeftWeight(): number {
|
|
return this._raw().borderLeftWeight
|
|
}
|
|
|
|
set strokeLeftWeight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
borderLeftWeight: v,
|
|
independentStrokeWeights: true
|
|
})
|
|
}
|
|
|
|
get strokeRightWeight(): number {
|
|
return this._raw().borderRightWeight
|
|
}
|
|
|
|
set strokeRightWeight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
borderRightWeight: v,
|
|
independentStrokeWeights: true
|
|
})
|
|
}
|
|
|
|
// --- Text ---
|
|
|
|
get characters(): string {
|
|
return this._raw().text
|
|
}
|
|
|
|
set characters(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { text: v })
|
|
}
|
|
|
|
get fontSize(): number {
|
|
return this._raw().fontSize
|
|
}
|
|
|
|
set fontSize(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { fontSize: v })
|
|
}
|
|
|
|
get fontName(): FigmaFontName {
|
|
const n = this._raw()
|
|
return { family: n.fontFamily, style: weightToStyleName(n.fontWeight, n.italic) }
|
|
}
|
|
|
|
set fontName(v: FigmaFontName) {
|
|
const { weight, italic } = styleNameToWeight(v.style)
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
fontFamily: v.family,
|
|
fontWeight: weight,
|
|
italic
|
|
})
|
|
}
|
|
|
|
get fontWeight(): number {
|
|
return this._raw().fontWeight
|
|
}
|
|
|
|
set fontWeight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { fontWeight: v })
|
|
}
|
|
|
|
get textAlignHorizontal(): string {
|
|
return this._raw().textAlignHorizontal
|
|
}
|
|
|
|
set textAlignHorizontal(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
textAlignHorizontal: v as SceneNode['textAlignHorizontal']
|
|
})
|
|
}
|
|
|
|
get textAlignVertical(): string {
|
|
return this._raw().textAlignVertical
|
|
}
|
|
|
|
set textAlignVertical(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
textAlignVertical: v as SceneNode['textAlignVertical']
|
|
})
|
|
}
|
|
|
|
get textAutoResize(): string {
|
|
return this._raw().textAutoResize
|
|
}
|
|
|
|
set textAutoResize(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
textAutoResize: v as SceneNode['textAutoResize']
|
|
})
|
|
}
|
|
|
|
get letterSpacing(): number {
|
|
return this._raw().letterSpacing
|
|
}
|
|
|
|
set letterSpacing(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { letterSpacing: v })
|
|
}
|
|
|
|
get lineHeight(): number | null {
|
|
return this._raw().lineHeight
|
|
}
|
|
|
|
set lineHeight(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { lineHeight: v })
|
|
}
|
|
|
|
get textCase(): string {
|
|
return this._raw().textCase
|
|
}
|
|
|
|
set textCase(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { textCase: v as SceneNode['textCase'] })
|
|
}
|
|
|
|
get textDecoration(): string {
|
|
return this._raw().textDecoration
|
|
}
|
|
|
|
set textDecoration(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
textDecoration: v as SceneNode['textDecoration']
|
|
})
|
|
}
|
|
|
|
get maxLines(): number | null {
|
|
return this._raw().maxLines
|
|
}
|
|
|
|
set maxLines(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxLines: v })
|
|
}
|
|
|
|
get textTruncation(): string {
|
|
return this._raw().textTruncation
|
|
}
|
|
|
|
set textTruncation(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
textTruncation: v as SceneNode['textTruncation']
|
|
})
|
|
}
|
|
|
|
get autoRename(): boolean {
|
|
return this._raw().autoRename
|
|
}
|
|
|
|
set autoRename(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { autoRename: v })
|
|
}
|
|
|
|
insertCharacters(start: number, characters: string): void {
|
|
const n = this._raw()
|
|
const text = n.text.slice(0, start) + characters + n.text.slice(start)
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { text })
|
|
}
|
|
|
|
deleteCharacters(start: number, end: number): void {
|
|
const n = this._raw()
|
|
const text = n.text.slice(0, start) + n.text.slice(end)
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { text })
|
|
}
|
|
|
|
// --- Auto-layout ---
|
|
|
|
get layoutMode(): LayoutMode {
|
|
return this._raw().layoutMode
|
|
}
|
|
|
|
set layoutMode(v: LayoutMode) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutMode: v })
|
|
}
|
|
|
|
get primaryAxisAlignItems(): string {
|
|
return this._raw().primaryAxisAlign
|
|
}
|
|
|
|
set primaryAxisAlignItems(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
primaryAxisAlign: v as SceneNode['primaryAxisAlign']
|
|
})
|
|
}
|
|
|
|
get counterAxisAlignItems(): string {
|
|
return this._raw().counterAxisAlign
|
|
}
|
|
|
|
set counterAxisAlignItems(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
counterAxisAlign: v as SceneNode['counterAxisAlign']
|
|
})
|
|
}
|
|
|
|
get itemSpacing(): number {
|
|
return this._raw().itemSpacing
|
|
}
|
|
|
|
set itemSpacing(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { itemSpacing: v })
|
|
}
|
|
|
|
get counterAxisSpacing(): number {
|
|
return this._raw().counterAxisSpacing
|
|
}
|
|
|
|
set counterAxisSpacing(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { counterAxisSpacing: v })
|
|
}
|
|
|
|
get paddingTop(): number {
|
|
return this._raw().paddingTop
|
|
}
|
|
|
|
set paddingTop(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { paddingTop: v })
|
|
}
|
|
|
|
get paddingRight(): number {
|
|
return this._raw().paddingRight
|
|
}
|
|
|
|
set paddingRight(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { paddingRight: v })
|
|
}
|
|
|
|
get paddingBottom(): number {
|
|
return this._raw().paddingBottom
|
|
}
|
|
|
|
set paddingBottom(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { paddingBottom: v })
|
|
}
|
|
|
|
get paddingLeft(): number {
|
|
return this._raw().paddingLeft
|
|
}
|
|
|
|
set paddingLeft(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { paddingLeft: v })
|
|
}
|
|
|
|
get layoutWrap(): string {
|
|
return this._raw().layoutWrap
|
|
}
|
|
|
|
set layoutWrap(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutWrap: v as SceneNode['layoutWrap'] })
|
|
}
|
|
|
|
get primaryAxisSizingMode(): string {
|
|
return this._raw().primaryAxisSizing === 'HUG' ? 'AUTO' : this._raw().primaryAxisSizing
|
|
}
|
|
|
|
set primaryAxisSizingMode(v: string) {
|
|
const mapped = v === 'AUTO' ? 'HUG' : v
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
primaryAxisSizing: mapped as SceneNode['primaryAxisSizing']
|
|
})
|
|
}
|
|
|
|
get counterAxisSizingMode(): string {
|
|
return this._raw().counterAxisSizing === 'HUG' ? 'AUTO' : this._raw().counterAxisSizing
|
|
}
|
|
|
|
set counterAxisSizingMode(v: string) {
|
|
const mapped = v === 'AUTO' ? 'HUG' : v
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
counterAxisSizing: mapped as SceneNode['counterAxisSizing']
|
|
})
|
|
}
|
|
|
|
get counterAxisAlignContent(): string {
|
|
return this._raw().counterAxisAlignContent
|
|
}
|
|
|
|
set counterAxisAlignContent(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
counterAxisAlignContent: v as SceneNode['counterAxisAlignContent']
|
|
})
|
|
}
|
|
|
|
get itemReverseZIndex(): boolean {
|
|
return this._raw().itemReverseZIndex
|
|
}
|
|
|
|
set itemReverseZIndex(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { itemReverseZIndex: v })
|
|
}
|
|
|
|
get strokesIncludedInLayout(): boolean {
|
|
return this._raw().strokesIncludedInLayout
|
|
}
|
|
|
|
set strokesIncludedInLayout(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokesIncludedInLayout: v })
|
|
}
|
|
|
|
// --- Layout child props ---
|
|
|
|
get layoutPositioning(): string {
|
|
return this._raw().layoutPositioning
|
|
}
|
|
|
|
set layoutPositioning(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
layoutPositioning: v as SceneNode['layoutPositioning']
|
|
})
|
|
}
|
|
|
|
get layoutGrow(): number {
|
|
return this._raw().layoutGrow
|
|
}
|
|
|
|
set layoutGrow(v: number) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutGrow: v })
|
|
}
|
|
|
|
get layoutAlign(): string {
|
|
const n = this._raw()
|
|
if (n.layoutAlignSelf === 'AUTO') return 'INHERIT'
|
|
return n.layoutAlignSelf
|
|
}
|
|
|
|
set layoutAlign(v: string) {
|
|
const mapped = v === 'INHERIT' ? 'AUTO' : v
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
layoutAlignSelf: mapped as SceneNode['layoutAlignSelf']
|
|
})
|
|
}
|
|
|
|
get layoutSizingHorizontal(): string {
|
|
const n = this._raw()
|
|
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
|
const refLayout = parent && parent.layoutMode !== 'NONE' ? parent.layoutMode : n.layoutMode
|
|
if (refLayout === 'NONE') return 'FIXED'
|
|
return refLayout === 'HORIZONTAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
|
}
|
|
|
|
set layoutSizingHorizontal(v: string) {
|
|
const n = this._raw()
|
|
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
|
const refLayout = parent && parent.layoutMode !== 'NONE' ? parent.layoutMode : n.layoutMode
|
|
if (refLayout === 'VERTICAL') {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
counterAxisSizing: v as SceneNode['counterAxisSizing']
|
|
})
|
|
} else {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
primaryAxisSizing: v as SceneNode['primaryAxisSizing']
|
|
})
|
|
}
|
|
}
|
|
|
|
get layoutSizingVertical(): string {
|
|
const n = this._raw()
|
|
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
|
const refLayout = parent && parent.layoutMode !== 'NONE' ? parent.layoutMode : n.layoutMode
|
|
if (refLayout === 'NONE') return 'FIXED'
|
|
return refLayout === 'VERTICAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
|
}
|
|
|
|
set layoutSizingVertical(v: string) {
|
|
const n = this._raw()
|
|
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
|
const refLayout = parent && parent.layoutMode !== 'NONE' ? parent.layoutMode : n.layoutMode
|
|
if (refLayout === 'HORIZONTAL') {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
counterAxisSizing: v as SceneNode['counterAxisSizing']
|
|
})
|
|
} else {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
primaryAxisSizing: v as SceneNode['primaryAxisSizing']
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- Constraints ---
|
|
|
|
get constraints(): { horizontal: string; vertical: string } {
|
|
const n = this._raw()
|
|
return { horizontal: n.horizontalConstraint, vertical: n.verticalConstraint }
|
|
}
|
|
|
|
set constraints(v: { horizontal: string; vertical: string }) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
|
horizontalConstraint: v.horizontal as SceneNode['horizontalConstraint'],
|
|
verticalConstraint: v.vertical as SceneNode['verticalConstraint']
|
|
})
|
|
}
|
|
|
|
// --- Dimension constraints ---
|
|
|
|
get minWidth(): number | null {
|
|
return this._raw().minWidth
|
|
}
|
|
|
|
set minWidth(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { minWidth: v })
|
|
}
|
|
|
|
get maxWidth(): number | null {
|
|
return this._raw().maxWidth
|
|
}
|
|
|
|
set maxWidth(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxWidth: v })
|
|
}
|
|
|
|
get minHeight(): number | null {
|
|
return this._raw().minHeight
|
|
}
|
|
|
|
set minHeight(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { minHeight: v })
|
|
}
|
|
|
|
get maxHeight(): number | null {
|
|
return this._raw().maxHeight
|
|
}
|
|
|
|
set maxHeight(v: number | null) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxHeight: v })
|
|
}
|
|
|
|
// --- Mask ---
|
|
|
|
get isMask(): boolean {
|
|
return this._raw().isMask
|
|
}
|
|
|
|
set isMask(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { isMask: v })
|
|
}
|
|
|
|
get maskType(): string {
|
|
return this._raw().maskType
|
|
}
|
|
|
|
set maskType(v: string) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maskType: v as SceneNode['maskType'] })
|
|
}
|
|
|
|
// --- UI state ---
|
|
|
|
get expanded(): boolean {
|
|
return this._raw().expanded
|
|
}
|
|
|
|
set expanded(v: boolean) {
|
|
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { expanded: v })
|
|
}
|
|
|
|
// --- Components ---
|
|
|
|
get mainComponent(): FigmaNodeProxy | null {
|
|
const n = this._raw()
|
|
if (!n.componentId) return null
|
|
const comp = this[INTERNAL_GRAPH].getNode(n.componentId)
|
|
if (!comp) return null
|
|
return this[INTERNAL_API].wrapNode(comp.id)
|
|
}
|
|
|
|
createInstance(): FigmaNodeProxy {
|
|
const n = this._raw()
|
|
if (n.type !== 'COMPONENT') throw new Error('createInstance() can only be called on components')
|
|
const pageId = this[INTERNAL_API].currentPageId
|
|
const inst = this[INTERNAL_GRAPH].createInstance(n.id, pageId)
|
|
if (!inst) throw new Error('Failed to create instance')
|
|
return this[INTERNAL_API].wrapNode(inst.id)
|
|
}
|
|
|
|
// --- Tree ---
|
|
|
|
get parent(): FigmaNodeProxy | null {
|
|
const n = this._raw()
|
|
if (!n.parentId) return null
|
|
return this[INTERNAL_API].wrapNode(n.parentId)
|
|
}
|
|
|
|
get children(): FigmaNodeProxy[] {
|
|
return this[INTERNAL_GRAPH]
|
|
.getChildren(this[INTERNAL_ID])
|
|
.map((c) => this[INTERNAL_API].wrapNode(c.id))
|
|
}
|
|
|
|
appendChild(child: FigmaNodeProxy): void {
|
|
this[INTERNAL_GRAPH].reparentNode(child[INTERNAL_ID], this[INTERNAL_ID])
|
|
}
|
|
|
|
insertChild(index: number, child: FigmaNodeProxy): void {
|
|
this[INTERNAL_GRAPH].reparentNode(child[INTERNAL_ID], this[INTERNAL_ID])
|
|
this[INTERNAL_GRAPH].reorderChild(child[INTERNAL_ID], this[INTERNAL_ID], index)
|
|
}
|
|
|
|
clone(): FigmaNodeProxy {
|
|
const n = this._raw()
|
|
const parentId = n.parentId ?? this[INTERNAL_API].currentPageId
|
|
const cloned = this[INTERNAL_GRAPH].cloneTree(this[INTERNAL_ID], parentId)
|
|
if (!cloned) throw new Error(`Failed to clone node ${this[INTERNAL_ID]}`)
|
|
return this[INTERNAL_API].wrapNode(cloned.id)
|
|
}
|
|
|
|
remove(): void {
|
|
this[INTERNAL_GRAPH].deleteNode(this[INTERNAL_ID])
|
|
}
|
|
|
|
findAll(callback?: (node: FigmaNodeProxy) => boolean): FigmaNodeProxy[] {
|
|
const results: FigmaNodeProxy[] = []
|
|
const walk = (id: string) => {
|
|
for (const child of this[INTERNAL_GRAPH].getChildren(id)) {
|
|
const proxy = this[INTERNAL_API].wrapNode(child.id)
|
|
if (!callback || callback(proxy)) results.push(proxy)
|
|
walk(child.id)
|
|
}
|
|
}
|
|
walk(this[INTERNAL_ID])
|
|
return results
|
|
}
|
|
|
|
findOne(callback: (node: FigmaNodeProxy) => boolean): FigmaNodeProxy | null {
|
|
const walk = (id: string): FigmaNodeProxy | null => {
|
|
for (const child of this[INTERNAL_GRAPH].getChildren(id)) {
|
|
const proxy = this[INTERNAL_API].wrapNode(child.id)
|
|
if (callback(proxy)) return proxy
|
|
const found = walk(child.id)
|
|
if (found) return found
|
|
}
|
|
return null
|
|
}
|
|
return walk(this[INTERNAL_ID])
|
|
}
|
|
|
|
findChild(callback: (node: FigmaNodeProxy) => boolean): FigmaNodeProxy | null {
|
|
for (const child of this[INTERNAL_GRAPH].getChildren(this[INTERNAL_ID])) {
|
|
const proxy = this[INTERNAL_API].wrapNode(child.id)
|
|
if (callback(proxy)) return proxy
|
|
}
|
|
return null
|
|
}
|
|
|
|
findChildren(callback?: (node: FigmaNodeProxy) => boolean): FigmaNodeProxy[] {
|
|
return this[INTERNAL_GRAPH]
|
|
.getChildren(this[INTERNAL_ID])
|
|
.map((c) => this[INTERNAL_API].wrapNode(c.id))
|
|
.filter((proxy) => !callback || callback(proxy))
|
|
}
|
|
|
|
findAllWithCriteria(criteria: { types?: string[] }): FigmaNodeProxy[] {
|
|
const types = criteria.types ? new Set(criteria.types) : null
|
|
return this.findAll((node) => !types || types.has(node.type))
|
|
}
|
|
|
|
// --- Serialization ---
|
|
|
|
toJSON(): Record<string, unknown> {
|
|
const n = this._raw()
|
|
const obj: Record<string, unknown> = {
|
|
id: n.id,
|
|
type: n.type,
|
|
name: n.name,
|
|
x: n.x,
|
|
y: n.y,
|
|
width: n.width,
|
|
height: n.height
|
|
}
|
|
if (n.fills.length > 0) obj.fills = n.fills
|
|
if (n.strokes.length > 0) obj.strokes = n.strokes
|
|
if (n.effects.length > 0) obj.effects = n.effects
|
|
if (n.opacity !== 1) obj.opacity = n.opacity
|
|
if (n.cornerRadius > 0) obj.cornerRadius = n.cornerRadius
|
|
if (!n.visible) obj.visible = false
|
|
if (n.text) obj.characters = n.text
|
|
if (n.layoutMode !== 'NONE') {
|
|
obj.layoutMode = n.layoutMode
|
|
obj.itemSpacing = n.itemSpacing
|
|
}
|
|
const children = this[INTERNAL_GRAPH].getChildren(this[INTERNAL_ID])
|
|
if (children.length > 0) {
|
|
obj.children = children.map((c) => this[INTERNAL_API].wrapNode(c.id).toJSON())
|
|
}
|
|
return obj
|
|
}
|
|
|
|
toString(): string {
|
|
const n = this._raw()
|
|
return `[${n.type} "${n.name}" ${n.id}]`
|
|
}
|
|
|
|
[Symbol.for('nodejs.util.inspect.custom')](): string {
|
|
return this.toString()
|
|
}
|
|
}
|