openpencil/packages/core/src/scene-graph-instances.ts
Danila Poyarkov e97a4cfda4 Fix lint warnings: eliminate any types, split oversized files
- 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)
2026-03-12 15:13:30 +03:00

215 lines
6.4 KiB
TypeScript

import { copyEffects, copyFills, copyStrokes, copyStyleRuns } from './copy'
import type { SceneGraph, SceneNode, Fill, Stroke, Effect, StyleRun } from './scene-graph'
const INSTANCE_SYNC_PROPS: (keyof SceneNode)[] = [
'width',
'height',
'fills',
'strokes',
'effects',
'opacity',
'cornerRadius',
'topLeftRadius',
'topRightRadius',
'bottomRightRadius',
'bottomLeftRadius',
'independentCorners',
'layoutMode',
'layoutWrap',
'primaryAxisAlign',
'counterAxisAlign',
'primaryAxisSizing',
'counterAxisSizing',
'itemSpacing',
'counterAxisSpacing',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'gridTemplateColumns',
'gridTemplateRows',
'gridColumnGap',
'gridRowGap',
'gridPosition',
'clipsContent',
'independentStrokeWeights',
'borderTopWeight',
'borderRightWeight',
'borderBottomWeight',
'borderLeftWeight'
]
function copyProp(
target: Partial<SceneNode> | SceneNode,
source: SceneNode,
key: keyof SceneNode
): void {
const val = source[key]
if (key === 'fills') {
;(target as Record<string, unknown>)[key] = copyFills(val as Fill[])
} else if (key === 'strokes') {
;(target as Record<string, unknown>)[key] = copyStrokes(val as Stroke[])
} else if (key === 'effects') {
;(target as Record<string, unknown>)[key] = copyEffects(val as Effect[])
} else if (key === 'styleRuns') {
;(target as Record<string, unknown>)[key] = copyStyleRuns(val as StyleRun[])
} else {
;(target as Record<string, unknown>)[key] = Array.isArray(val) ? structuredClone(val) : val
}
}
function cloneChildrenWithMapping(graph: SceneGraph, sourceParentId: string, destParentId: string): void {
const sourceParent = graph.nodes.get(sourceParentId)
if (!sourceParent) return
for (const childId of sourceParent.childIds) {
const src = graph.nodes.get(childId)
if (!src) continue
const { id: _, parentId: _p, childIds: _c, ...rest } = src
const clone = graph.createNode(src.type, destParentId, {
...rest,
componentId: childId
})
if (src.childIds.length > 0) {
cloneChildrenWithMapping(graph, childId, clone.id)
}
}
}
function syncChildren(
graph: SceneGraph,
compParentId: string,
instParentId: string,
overrides: Record<string, unknown>
): void {
const compParent = graph.nodes.get(compParentId)
const instParent = graph.nodes.get(instParentId)
if (!compParent || !instParent) return
const instChildMap = new Map<string, SceneNode>()
for (const childId of instParent.childIds) {
const child = graph.nodes.get(childId)
if (child?.componentId) instChildMap.set(child.componentId, child)
}
for (const compChildId of compParent.childIds) {
if (!instChildMap.has(compChildId)) {
const src = graph.nodes.get(compChildId)
if (!src) continue
const { id: _, parentId: _p, childIds: _c, ...rest } = src
const clone = graph.createNode(src.type, instParentId, {
...rest,
componentId: compChildId
})
if (src.childIds.length > 0) {
cloneChildrenWithMapping(graph, compChildId, clone.id)
}
instChildMap.set(compChildId, clone)
}
}
for (const compChildId of compParent.childIds) {
const compChild = graph.nodes.get(compChildId)
const instChild = instChildMap.get(compChildId)
if (!compChild || !instChild) continue
for (const key of INSTANCE_SYNC_PROPS) {
const overrideKey = `${instChild.id}:${key}`
if (overrideKey in overrides) continue
copyProp(instChild, compChild, key)
}
for (const key of ['name', 'text', 'fontSize', 'fontWeight', 'fontFamily'] as const) {
const overrideKey = `${instChild.id}:${key}`
if (overrideKey in overrides) continue
copyProp(instChild, compChild, key)
}
if (compChild.childIds.length > 0) {
syncChildren(graph, compChildId, instChild.id, overrides)
}
}
const compChildOrder = compParent.childIds
instParent.childIds.sort((a, b) => {
const nodeA = graph.nodes.get(a)
const nodeB = graph.nodes.get(b)
const idxA = nodeA?.componentId ? compChildOrder.indexOf(nodeA.componentId) : -1
const idxB = nodeB?.componentId ? compChildOrder.indexOf(nodeB.componentId) : -1
return idxA - idxB
})
}
export function createInstance(
graph: SceneGraph,
componentId: string,
parentId: string,
overrides: Partial<SceneNode> = {}
): SceneNode | null {
const component = graph.nodes.get(componentId)
if (component?.type !== 'COMPONENT') return null
const props: Partial<SceneNode> = { name: component.name, componentId }
for (const key of INSTANCE_SYNC_PROPS) {
copyProp(props, component, key)
}
const instance = graph.createNode('INSTANCE', parentId, { ...props, ...overrides })
cloneChildrenWithMapping(graph, component.id, instance.id)
return instance
}
export function populateInstanceChildren(graph: SceneGraph, instanceId: string, componentId: string): void {
const instance = graph.nodes.get(instanceId)
const component = graph.nodes.get(componentId)
if (!instance || !component || instance.type !== 'INSTANCE') return
cloneChildrenWithMapping(graph, componentId, instanceId)
}
export function syncInstances(graph: SceneGraph, componentId: string): void {
const component = graph.nodes.get(componentId)
if (component?.type !== 'COMPONENT') return
for (const instance of getInstances(graph, componentId)) {
for (const key of INSTANCE_SYNC_PROPS) {
if (key in instance.overrides) continue
copyProp(instance, component, key)
}
syncChildren(graph, component.id, instance.id, instance.overrides)
}
}
export function detachInstance(graph: SceneGraph, instanceId: string): void {
const node = graph.nodes.get(instanceId)
if (node?.type !== 'INSTANCE') return
if (node.componentId) {
graph.instanceIndex.get(node.componentId)?.delete(instanceId)
}
node.type = 'FRAME'
node.componentId = null
node.overrides = {}
}
export function getMainComponent(graph: SceneGraph, instanceId: string): SceneNode | undefined {
const node = graph.nodes.get(instanceId)
if (!node?.componentId) return undefined
return graph.nodes.get(node.componentId)
}
export function getInstances(graph: SceneGraph, componentId: string): SceneNode[] {
const ids = graph.instanceIndex.get(componentId)
if (!ids) return []
const instances: SceneNode[] = []
for (const id of ids) {
const node = graph.nodes.get(id)
if (node) instances.push(node)
}
return instances
}