refactor(fig): split override sync modules
This commit is contained in:
parent
1fc0b62e9e
commit
22991daf77
|
|
@ -1,9 +1,9 @@
|
|||
import {
|
||||
applyInstanceDirectAssignments,
|
||||
applyOverrideAssignments
|
||||
} from './component-props/assignments'
|
||||
import { collectAssignmentsMap, collectPropRefsMap } from './component-props/maps'
|
||||
import type { OverrideContext } from './types'
|
||||
} from './assignments'
|
||||
import { collectAssignmentsMap, collectPropRefsMap } from './maps'
|
||||
import type { OverrideContext } from '#core/kiwi/instance-overrides/types'
|
||||
|
||||
/**
|
||||
* Apply all component property assignments (visibility toggles, instance swaps).
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { resolveGeometryPaths } from '#core/kiwi/node-change/convert'
|
||||
import type { DerivedSymbolOverride } from '#core/kiwi/instance-overrides/types'
|
||||
import type { GeometryPath, SceneNode } from '#core/scene-graph'
|
||||
|
||||
function scaleGeometryBlobs(geom: GeometryPath[], sx: number, sy: number): GeometryPath[] {
|
||||
if (sx === 1 && sy === 1) return geom
|
||||
return geom.map((g) => {
|
||||
const scaled = g.commandsBlob.slice()
|
||||
const dv = new DataView(scaled.buffer, scaled.byteOffset, scaled.byteLength)
|
||||
let o = 0
|
||||
while (o < scaled.length) {
|
||||
const cmd = scaled[o++]
|
||||
if (cmd === 0) continue
|
||||
let coords = -1
|
||||
if (cmd === 1 || cmd === 2) coords = 1
|
||||
else if (cmd === 4) coords = 3
|
||||
if (coords < 0) break
|
||||
for (let i = 0; i < coords; i++) {
|
||||
dv.setFloat32(o, dv.getFloat32(o, true) * sx, true)
|
||||
dv.setFloat32(o + 4, dv.getFloat32(o + 4, true) * sy, true)
|
||||
o += 8
|
||||
}
|
||||
}
|
||||
return { windingRule: g.windingRule, commandsBlob: scaled }
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveDsdGeometry(
|
||||
d: DerivedSymbolOverride,
|
||||
target: SceneNode,
|
||||
blobs: Uint8Array[]
|
||||
): Pick<Partial<SceneNode>, 'fillGeometry' | 'strokeGeometry'> {
|
||||
const result: Pick<Partial<SceneNode>, 'fillGeometry' | 'strokeGeometry'> = {}
|
||||
const fg = resolveGeometryPaths(d.fillGeometry, blobs)
|
||||
const sg = resolveGeometryPaths(d.strokeGeometry, blobs)
|
||||
|
||||
if (fg.length > 0) result.fillGeometry = fg
|
||||
else if (d.size && target.fillGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.fillGeometry = scaleGeometryBlobs(target.fillGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
}
|
||||
|
||||
if (sg.length > 0) result.strokeGeometry = sg
|
||||
else if (d.size && target.strokeGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.strokeGeometry = scaleGeometryBlobs(target.strokeGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { applyOverridePatch } from '#core/kiwi/instance-overrides/patches'
|
||||
import { resolveOverrideTarget } from '#core/kiwi/instance-overrides/resolve'
|
||||
import type { DerivedSymbolOverride, OverrideContext } from '#core/kiwi/instance-overrides/types'
|
||||
|
||||
import { buildDsdLayoutUpdates } from './layout'
|
||||
import { propagateDsdChanges } from './propagate'
|
||||
|
||||
function applyDsdOverride(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
nodeId: string,
|
||||
d: DerivedSymbolOverride,
|
||||
modified: Set<string>,
|
||||
sizeSet: Set<string>
|
||||
): void {
|
||||
const guids = d.guidPath?.guids
|
||||
if (!guids?.length) return
|
||||
|
||||
const targetId = resolveOverrideTarget(ctx, nodeId, guids)
|
||||
if (!targetId) return
|
||||
|
||||
const target = ctx.graph.getNode(targetId)
|
||||
if (!target) return
|
||||
|
||||
const { updates, hasSize } = buildDsdLayoutUpdates(ctx, visibleSiblingCount, d, target)
|
||||
if (d.fillGeometry?.length || d.strokeGeometry?.length) ctx.geometryOverrideNodes.add(targetId)
|
||||
if (Object.keys(updates).length === 0) return
|
||||
|
||||
if (applyOverridePatch(ctx, { targetId, source: 'derived-symbol-data', props: updates })) {
|
||||
modified.add(targetId)
|
||||
}
|
||||
if (hasSize) sizeSet.add(targetId)
|
||||
}
|
||||
|
||||
function resolveDsdUpdates(ctx: OverrideContext): { modified: Set<string>; sizeSet: Set<string> } {
|
||||
const modified = new Set<string>()
|
||||
const sizeSet = new Set<string>()
|
||||
const visibleSiblingCount = new Map<string, number>()
|
||||
|
||||
for (const [ncId, nc] of ctx.changeMap) {
|
||||
if (nc.type !== 'INSTANCE') continue
|
||||
const derived = nc.derivedSymbolData
|
||||
if (!derived?.length) continue
|
||||
|
||||
const nodeId = ctx.guidToNodeId.get(ncId)
|
||||
if (!nodeId || (ctx.activeNodeIds && !ctx.activeNodeIds.has(nodeId))) continue
|
||||
|
||||
for (const d of derived) applyDsdOverride(ctx, visibleSiblingCount, nodeId, d, modified, sizeSet)
|
||||
}
|
||||
|
||||
return { modified, sizeSet }
|
||||
}
|
||||
|
||||
export function applyDerivedSymbolData(ctx: OverrideContext): void {
|
||||
const { modified, sizeSet } = resolveDsdUpdates(ctx)
|
||||
propagateDsdChanges(ctx, modified, sizeSet)
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { convertLetterSpacing, convertLineHeight } from '#core/kiwi/node-change/convert'
|
||||
import type { DerivedSymbolOverride, OverrideContext } from '#core/kiwi/instance-overrides/types'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { resolveDsdGeometry } from './geometry'
|
||||
|
||||
function getVisibleSiblingCount(ctx: OverrideContext, cache: Map<string, number>, parentId: string): number {
|
||||
const cached = cache.get(parentId)
|
||||
if (cached !== undefined) return cached
|
||||
const count = ctx.graph.getChildren(parentId).filter((child) => child.visible).length
|
||||
cache.set(parentId, count)
|
||||
return count
|
||||
}
|
||||
|
||||
function resolveSizeOnlyPosition(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
node: SceneNode
|
||||
): Pick<SceneNode, 'x' | 'y'> | null {
|
||||
if (!node.parentId || getVisibleSiblingCount(ctx, visibleSiblingCount, node.parentId) !== 1 || !node.componentId) return null
|
||||
const source = ctx.graph.getNode(node.componentId)
|
||||
if (!source) return null
|
||||
const sourceParent = source.parentId ? ctx.graph.getNode(source.parentId) : null
|
||||
if (!sourceParent) return { x: source.x, y: source.y }
|
||||
const withinParent = source.x >= 0 && source.y >= 0 && source.x + source.width <= sourceParent.width + 0.01 && source.y + source.height <= sourceParent.height + 0.01
|
||||
return withinParent ? { x: source.x, y: source.y } : { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
function buildDsdTextUpdates(d: DerivedSymbolOverride): Partial<SceneNode> {
|
||||
const updates: Partial<SceneNode> = {}
|
||||
if (d.fontSize !== undefined) updates.fontSize = d.fontSize
|
||||
if (d.lineHeight !== undefined) updates.lineHeight = convertLineHeight(d.lineHeight, d.fontSize)
|
||||
if (d.letterSpacing !== undefined) updates.letterSpacing = convertLetterSpacing(d.letterSpacing, d.fontSize)
|
||||
return updates
|
||||
}
|
||||
|
||||
export function buildDsdLayoutUpdates(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
d: DerivedSymbolOverride,
|
||||
target: SceneNode
|
||||
): { updates: Partial<SceneNode>; hasSize: boolean } {
|
||||
const updates: Partial<SceneNode> = buildDsdTextUpdates(d)
|
||||
const figmaDerivedLayout: NonNullable<SceneNode['figmaDerivedLayout']> = {}
|
||||
|
||||
if (d.size) {
|
||||
updates.width = d.size.x
|
||||
updates.height = d.size.y
|
||||
figmaDerivedLayout.width = d.size.x
|
||||
figmaDerivedLayout.height = d.size.y
|
||||
}
|
||||
if (d.transform) {
|
||||
updates.x = d.transform.m02
|
||||
updates.y = d.transform.m12
|
||||
figmaDerivedLayout.x = d.transform.m02
|
||||
figmaDerivedLayout.y = d.transform.m12
|
||||
} else if (d.size) {
|
||||
const position = resolveSizeOnlyPosition(ctx, visibleSiblingCount, target)
|
||||
if (position) {
|
||||
updates.x = position.x
|
||||
updates.y = position.y
|
||||
figmaDerivedLayout.x = position.x
|
||||
figmaDerivedLayout.y = position.y
|
||||
}
|
||||
}
|
||||
if (Object.keys(figmaDerivedLayout).length > 0) updates.figmaDerivedLayout = figmaDerivedLayout
|
||||
Object.assign(updates, resolveDsdGeometry(d, target, ctx.blobs))
|
||||
return { updates, hasSize: d.size !== undefined }
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { buildClonesMap } from '#core/kiwi/instance-overrides/sync'
|
||||
import type { OverrideContext } from '#core/kiwi/instance-overrides/types'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import { copyGeometryPaths } from '#core/scene-graph/copy'
|
||||
|
||||
export function propagateDsdChanges(
|
||||
ctx: OverrideContext,
|
||||
modified: Set<string>,
|
||||
sizeSet: Set<string>
|
||||
): void {
|
||||
if (modified.size === 0) return
|
||||
|
||||
const clonesOf = buildClonesMap(ctx.graph, ctx.activeNodeIds)
|
||||
const queue = [...modified]
|
||||
const visited = new Set<string>()
|
||||
|
||||
let index = 0
|
||||
while (index < queue.length) {
|
||||
const sourceId = queue[index]
|
||||
index++
|
||||
const source = ctx.graph.getNode(sourceId)
|
||||
if (!source) continue
|
||||
const clones = clonesOf.get(sourceId)
|
||||
if (!clones) continue
|
||||
for (const cloneId of clones) {
|
||||
if (visited.has(cloneId)) continue
|
||||
visited.add(cloneId)
|
||||
const clone = ctx.graph.getNode(cloneId)
|
||||
if (!clone) continue
|
||||
if (!sizeSet.has(cloneId)) {
|
||||
const cu: Partial<SceneNode> = {}
|
||||
if (source.width !== clone.width) cu.width = source.width
|
||||
if (source.height !== clone.height) cu.height = source.height
|
||||
if (source.x !== clone.x) cu.x = source.x
|
||||
if (source.y !== clone.y) cu.y = source.y
|
||||
if (!ctx.geometryOverrideNodes.has(cloneId)) {
|
||||
if (source.fillGeometry !== clone.fillGeometry) cu.fillGeometry = copyGeometryPaths(source.fillGeometry)
|
||||
if (source.strokeGeometry !== clone.strokeGeometry) cu.strokeGeometry = copyGeometryPaths(source.strokeGeometry)
|
||||
}
|
||||
if (Object.keys(cu).length > 0) ctx.graph.updateNode(cloneId, cu)
|
||||
}
|
||||
queue.push(cloneId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
import {
|
||||
convertLetterSpacing,
|
||||
convertLineHeight,
|
||||
resolveGeometryPaths
|
||||
} from '#core/kiwi/node-change/convert'
|
||||
import { applyOverridePatch } from '#core/kiwi/instance-overrides/patches'
|
||||
import type { SceneNode, GeometryPath } from '#core/scene-graph'
|
||||
import { copyGeometryPaths } from '#core/scene-graph/copy'
|
||||
|
||||
import { resolveOverrideTarget } from './resolve'
|
||||
import { buildClonesMap } from './sync'
|
||||
import type { OverrideContext, DerivedSymbolOverride } from './types'
|
||||
|
||||
function scaleGeometryBlobs(geom: GeometryPath[], sx: number, sy: number): GeometryPath[] {
|
||||
if (sx === 1 && sy === 1) return geom
|
||||
return geom.map((g) => {
|
||||
const scaled = g.commandsBlob.slice()
|
||||
const dv = new DataView(scaled.buffer, scaled.byteOffset, scaled.byteLength)
|
||||
let o = 0
|
||||
while (o < scaled.length) {
|
||||
const cmd = scaled[o++]
|
||||
if (cmd === 0) continue
|
||||
let coords = -1
|
||||
if (cmd === 1 || cmd === 2) coords = 1
|
||||
else if (cmd === 4) coords = 3
|
||||
if (coords < 0) {
|
||||
console.warn(`scaleGeometryBlobs: unknown path command ${cmd} at offset ${o - 1}`)
|
||||
break
|
||||
}
|
||||
for (let i = 0; i < coords; i++) {
|
||||
dv.setFloat32(o, dv.getFloat32(o, true) * sx, true)
|
||||
dv.setFloat32(o + 4, dv.getFloat32(o + 4, true) * sy, true)
|
||||
o += 8
|
||||
}
|
||||
}
|
||||
return { windingRule: g.windingRule, commandsBlob: scaled }
|
||||
})
|
||||
}
|
||||
|
||||
function resolveDsdGeometry(
|
||||
d: DerivedSymbolOverride,
|
||||
target: SceneNode,
|
||||
blobs: Uint8Array[]
|
||||
): Pick<Partial<SceneNode>, 'fillGeometry' | 'strokeGeometry'> {
|
||||
const result: Pick<Partial<SceneNode>, 'fillGeometry' | 'strokeGeometry'> = {}
|
||||
const fg = resolveGeometryPaths(d.fillGeometry, blobs)
|
||||
const sg = resolveGeometryPaths(d.strokeGeometry, blobs)
|
||||
|
||||
if (fg.length > 0) {
|
||||
result.fillGeometry = fg
|
||||
} else if (d.size && target.fillGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.fillGeometry = scaleGeometryBlobs(
|
||||
target.fillGeometry,
|
||||
d.size.x / target.width,
|
||||
d.size.y / target.height
|
||||
)
|
||||
}
|
||||
|
||||
if (sg.length > 0) {
|
||||
result.strokeGeometry = sg
|
||||
} else if (d.size && target.strokeGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.strokeGeometry = scaleGeometryBlobs(
|
||||
target.strokeGeometry,
|
||||
d.size.x / target.width,
|
||||
d.size.y / target.height
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function getVisibleSiblingCount(
|
||||
ctx: OverrideContext,
|
||||
cache: Map<string, number>,
|
||||
parentId: string
|
||||
): number {
|
||||
const cached = cache.get(parentId)
|
||||
if (cached !== undefined) return cached
|
||||
const count = ctx.graph.getChildren(parentId).filter((child) => child.visible).length
|
||||
cache.set(parentId, count)
|
||||
return count
|
||||
}
|
||||
|
||||
function hasSingleVisibleSibling(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
node: SceneNode
|
||||
): boolean {
|
||||
if (!node.parentId) return false
|
||||
return getVisibleSiblingCount(ctx, visibleSiblingCount, node.parentId) === 1
|
||||
}
|
||||
|
||||
function resolveSizeOnlyPosition(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
node: SceneNode
|
||||
): Pick<SceneNode, 'x' | 'y'> | null {
|
||||
if (!hasSingleVisibleSibling(ctx, visibleSiblingCount, node) || !node.componentId) return null
|
||||
|
||||
const source = ctx.graph.getNode(node.componentId)
|
||||
if (!source) return null
|
||||
const sourceParent = source.parentId ? ctx.graph.getNode(source.parentId) : null
|
||||
if (!sourceParent) return { x: source.x, y: source.y }
|
||||
|
||||
const withinParent =
|
||||
source.x >= 0 &&
|
||||
source.y >= 0 &&
|
||||
source.x + source.width <= sourceParent.width + 0.01 &&
|
||||
source.y + source.height <= sourceParent.height + 0.01
|
||||
return withinParent ? { x: source.x, y: source.y } : { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
function buildDsdTextUpdates(d: DerivedSymbolOverride): Partial<SceneNode> {
|
||||
const updates: Partial<SceneNode> = {}
|
||||
if (d.fontSize !== undefined) updates.fontSize = d.fontSize
|
||||
if (d.lineHeight !== undefined) updates.lineHeight = convertLineHeight(d.lineHeight, d.fontSize)
|
||||
if (d.letterSpacing !== undefined) {
|
||||
updates.letterSpacing = convertLetterSpacing(d.letterSpacing, d.fontSize)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
function buildDsdLayoutUpdates(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
d: DerivedSymbolOverride,
|
||||
target: SceneNode
|
||||
): { updates: Partial<SceneNode>; hasSize: boolean } {
|
||||
const updates: Partial<SceneNode> = buildDsdTextUpdates(d)
|
||||
const figmaDerivedLayout: NonNullable<SceneNode['figmaDerivedLayout']> = {}
|
||||
|
||||
if (d.size) {
|
||||
updates.width = d.size.x
|
||||
updates.height = d.size.y
|
||||
figmaDerivedLayout.width = d.size.x
|
||||
figmaDerivedLayout.height = d.size.y
|
||||
}
|
||||
if (d.transform) {
|
||||
updates.x = d.transform.m02
|
||||
updates.y = d.transform.m12
|
||||
figmaDerivedLayout.x = d.transform.m02
|
||||
figmaDerivedLayout.y = d.transform.m12
|
||||
} else if (d.size) {
|
||||
const position = resolveSizeOnlyPosition(ctx, visibleSiblingCount, target)
|
||||
if (position) {
|
||||
updates.x = position.x
|
||||
updates.y = position.y
|
||||
figmaDerivedLayout.x = position.x
|
||||
figmaDerivedLayout.y = position.y
|
||||
}
|
||||
}
|
||||
if (Object.keys(figmaDerivedLayout).length > 0) {
|
||||
updates.figmaDerivedLayout = figmaDerivedLayout
|
||||
}
|
||||
Object.assign(updates, resolveDsdGeometry(d, target, ctx.blobs))
|
||||
|
||||
return { updates, hasSize: d.size !== undefined }
|
||||
}
|
||||
|
||||
function applyDsdOverride(
|
||||
ctx: OverrideContext,
|
||||
visibleSiblingCount: Map<string, number>,
|
||||
nodeId: string,
|
||||
d: DerivedSymbolOverride,
|
||||
modified: Set<string>,
|
||||
sizeSet: Set<string>
|
||||
): void {
|
||||
const guids = d.guidPath?.guids
|
||||
if (!guids?.length) return
|
||||
|
||||
const targetId = resolveOverrideTarget(ctx, nodeId, guids)
|
||||
if (!targetId) return
|
||||
|
||||
const target = ctx.graph.getNode(targetId)
|
||||
if (!target) return
|
||||
|
||||
const { updates, hasSize } = buildDsdLayoutUpdates(ctx, visibleSiblingCount, d, target)
|
||||
if (d.fillGeometry?.length || d.strokeGeometry?.length) ctx.geometryOverrideNodes.add(targetId)
|
||||
if (Object.keys(updates).length === 0) return
|
||||
|
||||
if (applyOverridePatch(ctx, { targetId, source: 'derived-symbol-data', props: updates })) {
|
||||
modified.add(targetId)
|
||||
}
|
||||
if (hasSize) sizeSet.add(targetId)
|
||||
}
|
||||
|
||||
function resolveDsdUpdates(ctx: OverrideContext): { modified: Set<string>; sizeSet: Set<string> } {
|
||||
const modified = new Set<string>()
|
||||
const sizeSet = new Set<string>()
|
||||
const visibleSiblingCount = new Map<string, number>()
|
||||
|
||||
for (const [ncId, nc] of ctx.changeMap) {
|
||||
if (nc.type !== 'INSTANCE') continue
|
||||
const derived = nc.derivedSymbolData
|
||||
if (!derived?.length) continue
|
||||
|
||||
const nodeId = ctx.guidToNodeId.get(ncId)
|
||||
if (!nodeId || (ctx.activeNodeIds && !ctx.activeNodeIds.has(nodeId))) continue
|
||||
|
||||
for (const d of derived) {
|
||||
applyDsdOverride(ctx, visibleSiblingCount, nodeId, d, modified, sizeSet)
|
||||
}
|
||||
}
|
||||
|
||||
return { modified, sizeSet }
|
||||
}
|
||||
|
||||
function propagateDsdChanges(
|
||||
ctx: OverrideContext,
|
||||
modified: Set<string>,
|
||||
sizeSet: Set<string>
|
||||
): void {
|
||||
if (modified.size === 0) return
|
||||
|
||||
const clonesOf = buildClonesMap(ctx.graph, ctx.activeNodeIds)
|
||||
const queue = [...modified]
|
||||
const visited = new Set<string>()
|
||||
|
||||
let index = 0
|
||||
while (index < queue.length) {
|
||||
const sourceId = queue[index]
|
||||
index++
|
||||
const source = ctx.graph.getNode(sourceId)
|
||||
if (!source) continue
|
||||
const clones = clonesOf.get(sourceId)
|
||||
if (!clones) continue
|
||||
for (const cloneId of clones) {
|
||||
if (visited.has(cloneId)) continue
|
||||
visited.add(cloneId)
|
||||
const clone = ctx.graph.getNode(cloneId)
|
||||
if (!clone) continue
|
||||
if (!sizeSet.has(cloneId)) {
|
||||
const cu: Partial<SceneNode> = {}
|
||||
if (source.width !== clone.width) cu.width = source.width
|
||||
if (source.height !== clone.height) cu.height = source.height
|
||||
if (source.x !== clone.x) cu.x = source.x
|
||||
if (source.y !== clone.y) cu.y = source.y
|
||||
if (!ctx.geometryOverrideNodes.has(cloneId)) {
|
||||
if (source.fillGeometry !== clone.fillGeometry)
|
||||
cu.fillGeometry = copyGeometryPaths(source.fillGeometry)
|
||||
if (source.strokeGeometry !== clone.strokeGeometry)
|
||||
cu.strokeGeometry = copyGeometryPaths(source.strokeGeometry)
|
||||
}
|
||||
if (Object.keys(cu).length > 0) ctx.graph.updateNode(cloneId, cu)
|
||||
}
|
||||
queue.push(cloneId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply derivedSymbolData — Figma's pre-computed sizes, positions,
|
||||
* and geometry for instance overrides. Runs last in the pipeline.
|
||||
*/
|
||||
export function applyDerivedSymbolData(ctx: OverrideContext): void {
|
||||
const { modified, sizeSet } = resolveDsdUpdates(ctx)
|
||||
propagateDsdChanges(ctx, modified, sizeSet)
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ import type { SceneGraph } from '#core/scene-graph'
|
|||
import { copyFills } from '#core/scene-graph/copy'
|
||||
|
||||
import { applyConstraintScaling } from './constraints'
|
||||
import { applyDerivedSymbolData } from './dsd'
|
||||
import { applyDerivedSymbolData } from './derived-symbol-data'
|
||||
import { populateInstances } from './populate'
|
||||
import { applyComponentProperties } from './props'
|
||||
import { applyComponentProperties } from './component-props'
|
||||
import { preComputeRoots } from './resolve'
|
||||
import { applySymbolOverrides } from './symbol/overrides'
|
||||
import { propagateOverridesTransitively } from './sync'
|
||||
|
|
|
|||
|
|
@ -1,311 +0,0 @@
|
|||
import type { ProtectionMap, ProtectedField } from '#core/kiwi/instance-overrides/patches'
|
||||
import { isFieldProtected } from '#core/kiwi/instance-overrides/patches'
|
||||
import type { SceneGraph, SceneNode } from '#core/scene-graph'
|
||||
import { copyFills, copyStrokes, copyEffects, copyStyleRuns } from '#core/scene-graph/copy'
|
||||
|
||||
/**
|
||||
* Copy appearance props from source to target (text, visibility, fills, etc.).
|
||||
* Only writes properties that actually differ.
|
||||
*/
|
||||
function canSync(
|
||||
protections: ProtectionMap | undefined,
|
||||
targetId: string,
|
||||
field: ProtectedField
|
||||
): boolean {
|
||||
return !isFieldProtected(protections, targetId, field)
|
||||
}
|
||||
|
||||
type SyncFn = (
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
updates: Partial<SceneNode>,
|
||||
protections?: ProtectionMap
|
||||
) => void
|
||||
|
||||
type DirectSyncKey = 'text' | 'visible' | 'opacity' | 'locked' | 'layoutGrow' | 'textAutoResize'
|
||||
type CopiedSyncKey = 'fills' | 'strokes' | 'effects' | 'styleRuns'
|
||||
|
||||
function assignDirectUpdate(
|
||||
key: DirectSyncKey,
|
||||
source: SceneNode,
|
||||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'text':
|
||||
updates.text = source.text
|
||||
break
|
||||
case 'visible':
|
||||
updates.visible = source.visible
|
||||
break
|
||||
case 'opacity':
|
||||
updates.opacity = source.opacity
|
||||
break
|
||||
case 'locked':
|
||||
updates.locked = source.locked
|
||||
break
|
||||
case 'layoutGrow':
|
||||
updates.layoutGrow = source.layoutGrow
|
||||
break
|
||||
case 'textAutoResize':
|
||||
updates.textAutoResize = source.textAutoResize
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function directSync(key: DirectSyncKey, field: ProtectedField): SyncFn {
|
||||
return (source, target, updates, protections) => {
|
||||
if (source[key] !== target[key] && canSync(protections, target.id, field)) {
|
||||
assignDirectUpdate(key, source, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DIRECT_SYNCERS: SyncFn[] = [
|
||||
directSync('text', 'text'),
|
||||
directSync('visible', 'visible'),
|
||||
directSync('opacity', 'opacity'),
|
||||
directSync('locked', 'locked'),
|
||||
directSync('layoutGrow', 'layoutGrow'),
|
||||
directSync('textAutoResize', 'textAutoResize')
|
||||
]
|
||||
|
||||
function syncDirectFields(
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
updates: Partial<SceneNode>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
for (const sync of DIRECT_SYNCERS) sync(source, target, updates, protections)
|
||||
}
|
||||
|
||||
function assignCopiedUpdate(
|
||||
key: CopiedSyncKey,
|
||||
source: SceneNode,
|
||||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'fills':
|
||||
updates.fills = copyFills(source.fills)
|
||||
break
|
||||
case 'strokes':
|
||||
updates.strokes = copyStrokes(source.strokes)
|
||||
break
|
||||
case 'effects':
|
||||
updates.effects = copyEffects(source.effects)
|
||||
break
|
||||
case 'styleRuns':
|
||||
updates.styleRuns = copyStyleRuns(source.styleRuns)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function copiedSync(key: CopiedSyncKey, field: ProtectedField): SyncFn {
|
||||
return (source, target, updates, protections) => {
|
||||
if (source[key] !== target[key] && canSync(protections, target.id, field)) {
|
||||
assignCopiedUpdate(key, source, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const COPIED_SYNCERS: SyncFn[] = [
|
||||
copiedSync('fills', 'fills'),
|
||||
copiedSync('strokes', 'strokes'),
|
||||
copiedSync('effects', 'effects'),
|
||||
copiedSync('styleRuns', 'styleRuns')
|
||||
]
|
||||
|
||||
function syncCopiedFields(
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
updates: Partial<SceneNode>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
for (const sync of COPIED_SYNCERS) sync(source, target, updates, protections)
|
||||
}
|
||||
|
||||
export function syncNodeProps(
|
||||
graph: SceneGraph,
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const updates: Partial<SceneNode> = {}
|
||||
syncDirectFields(source, target, updates, protections)
|
||||
syncCopiedFields(source, target, updates, protections)
|
||||
if (Object.keys(updates).length > 0) graph.updateNode(target.id, updates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-clone an instance's children from a new source (after instance swap).
|
||||
* Marks the target in `swappedInstances` so transitive sync propagates the swap.
|
||||
*/
|
||||
export function recloneChildren(
|
||||
graph: SceneGraph,
|
||||
srcChildId: string,
|
||||
tgtNode: SceneNode,
|
||||
swappedInstances: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const srcChild = graph.getNode(srcChildId)
|
||||
if (!srcChild) return
|
||||
|
||||
for (const childId of Array.from(tgtNode.childIds)) graph.deleteNode(childId)
|
||||
graph.updateNode(tgtNode.id, { name: srcChild.name, componentId: srcChild.componentId })
|
||||
syncNodeProps(graph, srcChild, tgtNode, protections)
|
||||
if (srcChild.childIds.length > 0) {
|
||||
graph.populateInstanceChildren(tgtNode.id, srcChildId)
|
||||
}
|
||||
swappedInstances.add(tgtNode.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sync children between source and target trees.
|
||||
* Handles instance swap propagation via `swappedInstances`.
|
||||
*/
|
||||
export function syncChildrenDeep(
|
||||
graph: SceneGraph,
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
swappedInstances: Set<string>,
|
||||
skip?: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const src = graph.getNode(sourceId)
|
||||
const tgt = graph.getNode(targetId)
|
||||
if (!src || !tgt) return
|
||||
const len = Math.min(src.childIds.length, tgt.childIds.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (skip?.has(tgt.childIds[i])) continue
|
||||
const srcNode = graph.getNode(src.childIds[i])
|
||||
const tgtNode = graph.getNode(tgt.childIds[i])
|
||||
if (!srcNode || !tgtNode || srcNode.type !== tgtNode.type) continue
|
||||
|
||||
if (
|
||||
srcNode.type === 'INSTANCE' &&
|
||||
swappedInstances.has(src.childIds[i]) &&
|
||||
srcNode.componentId !== tgtNode.componentId
|
||||
) {
|
||||
recloneChildren(graph, src.childIds[i], tgtNode, swappedInstances, protections)
|
||||
continue
|
||||
}
|
||||
|
||||
syncNodeProps(graph, srcNode, tgtNode, protections)
|
||||
syncChildrenDeep(graph, src.childIds[i], tgt.childIds[i], swappedInstances, skip, protections)
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a map of componentId → list of clone node IDs. */
|
||||
export function buildClonesMap(
|
||||
graph: SceneGraph,
|
||||
activeNodeIds?: Set<string>
|
||||
): Map<string, string[]> {
|
||||
const clonesOf = new Map<string, string[]>()
|
||||
for (const node of graph.getAllNodes()) {
|
||||
if (activeNodeIds && !activeNodeIds.has(node.id)) continue
|
||||
if (!node.componentId) continue
|
||||
let arr = clonesOf.get(node.componentId)
|
||||
if (!arr) {
|
||||
arr = []
|
||||
clonesOf.set(node.componentId, arr)
|
||||
}
|
||||
arr.push(node.id)
|
||||
}
|
||||
return clonesOf
|
||||
}
|
||||
|
||||
/** Expand seeds to include INSTANCE/COMPONENT parents up the tree. */
|
||||
function expandSeedsToParents(graph: SceneGraph, seeds: Set<string>): Set<string> {
|
||||
const expanded = new Set(seeds)
|
||||
for (const seedId of seeds) {
|
||||
let cur = graph.getNode(seedId)
|
||||
while (cur?.parentId) {
|
||||
const parent = graph.getNode(cur.parentId)
|
||||
if (!parent) break
|
||||
if (parent.type === 'INSTANCE' || parent.type === 'COMPONENT') {
|
||||
expanded.add(parent.id)
|
||||
}
|
||||
cur = parent
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
/** BFS from expanded seeds through clone chains to find all nodes needing sync. */
|
||||
function buildNeedsSyncSet(
|
||||
expandedSeeds: Set<string>,
|
||||
clonesOf: Map<string, string[]>
|
||||
): Set<string> {
|
||||
const needsSync = new Set<string>()
|
||||
const queue = [...expandedSeeds]
|
||||
for (let id = queue.pop(); id !== undefined; id = queue.pop()) {
|
||||
const clones = clonesOf.get(id)
|
||||
if (!clones) continue
|
||||
for (const cloneId of clones) {
|
||||
if (needsSync.has(cloneId)) continue
|
||||
needsSync.add(cloneId)
|
||||
queue.push(cloneId)
|
||||
}
|
||||
}
|
||||
return needsSync
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate overrides transitively through clone chains.
|
||||
*
|
||||
* Nodes in `seeds` are override sources — their clones are synced but
|
||||
* the seeds themselves are NOT overwritten (they carry explicit overrides).
|
||||
*/
|
||||
export function propagateOverridesTransitively(
|
||||
graph: SceneGraph,
|
||||
seeds: Set<string>,
|
||||
swappedInstances: Set<string>,
|
||||
componentIdRoot: Map<string, string>,
|
||||
protect?: Set<string>,
|
||||
activeNodeIds?: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
if (seeds.size === 0) return
|
||||
|
||||
componentIdRoot.clear()
|
||||
const clonesOf = buildClonesMap(graph, activeNodeIds)
|
||||
const expandedSeeds = expandSeedsToParents(graph, seeds)
|
||||
const needsSync = buildNeedsSyncSet(expandedSeeds, clonesOf)
|
||||
|
||||
// Merge seeds + protect into a single skip set for syncChildrenDeep
|
||||
const skip = protect && protect.size > 0 ? new Set([...seeds, ...protect]) : seeds
|
||||
|
||||
const visited = new Set<string>()
|
||||
const syncQueue = [...expandedSeeds]
|
||||
let index = 0
|
||||
while (index < syncQueue.length) {
|
||||
const sourceId = syncQueue[index]
|
||||
index++
|
||||
const clones = clonesOf.get(sourceId)
|
||||
if (!clones) continue
|
||||
const source = graph.getNode(sourceId)
|
||||
if (!source) continue
|
||||
|
||||
for (const cloneId of clones) {
|
||||
if (!needsSync.has(cloneId) || visited.has(cloneId)) continue
|
||||
visited.add(cloneId)
|
||||
const node = graph.getNode(cloneId)
|
||||
if (!node) continue
|
||||
|
||||
if (skip.has(cloneId)) {
|
||||
syncQueue.push(cloneId)
|
||||
continue
|
||||
}
|
||||
|
||||
syncNodeProps(graph, source, node, protections)
|
||||
if (source.childIds.length !== node.childIds.length) {
|
||||
for (const childId of Array.from(node.childIds)) graph.deleteNode(childId)
|
||||
if (source.childIds.length > 0) {
|
||||
graph.populateInstanceChildren(node.id, sourceId)
|
||||
}
|
||||
} else if (source.childIds.length > 0 && node.childIds.length > 0) {
|
||||
syncChildrenDeep(graph, sourceId, node.id, swappedInstances, skip, protections)
|
||||
}
|
||||
syncQueue.push(cloneId)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/core/src/kiwi/instance-overrides/sync/clones.ts
Normal file
71
packages/core/src/kiwi/instance-overrides/sync/clones.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { ProtectionMap } from '#core/kiwi/instance-overrides/patches'
|
||||
import type { SceneGraph, SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { syncNodeProps } from './fields'
|
||||
|
||||
export function recloneChildren(
|
||||
graph: SceneGraph,
|
||||
srcChildId: string,
|
||||
tgtNode: SceneNode,
|
||||
swappedInstances: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const srcChild = graph.getNode(srcChildId)
|
||||
if (!srcChild) return
|
||||
|
||||
for (const childId of Array.from(tgtNode.childIds)) graph.deleteNode(childId)
|
||||
graph.updateNode(tgtNode.id, { name: srcChild.name, componentId: srcChild.componentId })
|
||||
syncNodeProps(graph, srcChild, tgtNode, protections)
|
||||
if (srcChild.childIds.length > 0) graph.populateInstanceChildren(tgtNode.id, srcChildId)
|
||||
swappedInstances.add(tgtNode.id)
|
||||
}
|
||||
|
||||
export function syncChildrenDeep(
|
||||
graph: SceneGraph,
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
swappedInstances: Set<string>,
|
||||
skip?: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const src = graph.getNode(sourceId)
|
||||
const tgt = graph.getNode(targetId)
|
||||
if (!src || !tgt) return
|
||||
const len = Math.min(src.childIds.length, tgt.childIds.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (skip?.has(tgt.childIds[i])) continue
|
||||
const srcNode = graph.getNode(src.childIds[i])
|
||||
const tgtNode = graph.getNode(tgt.childIds[i])
|
||||
if (!srcNode || !tgtNode || srcNode.type !== tgtNode.type) continue
|
||||
|
||||
if (
|
||||
srcNode.type === 'INSTANCE' &&
|
||||
swappedInstances.has(src.childIds[i]) &&
|
||||
srcNode.componentId !== tgtNode.componentId
|
||||
) {
|
||||
recloneChildren(graph, src.childIds[i], tgtNode, swappedInstances, protections)
|
||||
continue
|
||||
}
|
||||
|
||||
syncNodeProps(graph, srcNode, tgtNode, protections)
|
||||
syncChildrenDeep(graph, src.childIds[i], tgt.childIds[i], swappedInstances, skip, protections)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildClonesMap(
|
||||
graph: SceneGraph,
|
||||
activeNodeIds?: Set<string>
|
||||
): Map<string, string[]> {
|
||||
const clonesOf = new Map<string, string[]>()
|
||||
for (const node of graph.getAllNodes()) {
|
||||
if (activeNodeIds && !activeNodeIds.has(node.id)) continue
|
||||
if (!node.componentId) continue
|
||||
let arr = clonesOf.get(node.componentId)
|
||||
if (!arr) {
|
||||
arr = []
|
||||
clonesOf.set(node.componentId, arr)
|
||||
}
|
||||
arr.push(node.id)
|
||||
}
|
||||
return clonesOf
|
||||
}
|
||||
103
packages/core/src/kiwi/instance-overrides/sync/fields.ts
Normal file
103
packages/core/src/kiwi/instance-overrides/sync/fields.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import type { ProtectionMap, ProtectedField } from '#core/kiwi/instance-overrides/patches'
|
||||
import { isFieldProtected } from '#core/kiwi/instance-overrides/patches'
|
||||
import type { SceneGraph, SceneNode } from '#core/scene-graph'
|
||||
import { copyFills, copyStrokes, copyEffects, copyStyleRuns } from '#core/scene-graph/copy'
|
||||
|
||||
function canSync(
|
||||
protections: ProtectionMap | undefined,
|
||||
targetId: string,
|
||||
field: ProtectedField
|
||||
): boolean {
|
||||
return !isFieldProtected(protections, targetId, field)
|
||||
}
|
||||
|
||||
type SyncFn = (
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
updates: Partial<SceneNode>,
|
||||
protections?: ProtectionMap
|
||||
) => void
|
||||
|
||||
type DirectSyncKey = 'text' | 'visible' | 'opacity' | 'locked' | 'layoutGrow' | 'textAutoResize'
|
||||
type CopiedSyncKey = 'fills' | 'strokes' | 'effects' | 'styleRuns'
|
||||
|
||||
function assignDirectUpdate(
|
||||
key: DirectSyncKey,
|
||||
source: SceneNode,
|
||||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'text': updates.text = source.text; break
|
||||
case 'visible': updates.visible = source.visible; break
|
||||
case 'opacity': updates.opacity = source.opacity; break
|
||||
case 'locked': updates.locked = source.locked; break
|
||||
case 'layoutGrow': updates.layoutGrow = source.layoutGrow; break
|
||||
case 'textAutoResize': updates.textAutoResize = source.textAutoResize; break
|
||||
}
|
||||
}
|
||||
|
||||
function directSync(key: DirectSyncKey, field: ProtectedField): SyncFn {
|
||||
return (source, target, updates, protections) => {
|
||||
if (source[key] !== target[key] && canSync(protections, target.id, field)) {
|
||||
assignDirectUpdate(key, source, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DIRECT_SYNCERS: SyncFn[] = [
|
||||
directSync('text', 'text'),
|
||||
directSync('visible', 'visible'),
|
||||
directSync('opacity', 'opacity'),
|
||||
directSync('locked', 'locked'),
|
||||
directSync('layoutGrow', 'layoutGrow'),
|
||||
directSync('textAutoResize', 'textAutoResize')
|
||||
]
|
||||
|
||||
function assignCopiedUpdate(
|
||||
key: CopiedSyncKey,
|
||||
source: SceneNode,
|
||||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'fills': updates.fills = copyFills(source.fills); break
|
||||
case 'strokes': updates.strokes = copyStrokes(source.strokes); break
|
||||
case 'effects': updates.effects = copyEffects(source.effects); break
|
||||
case 'styleRuns': updates.styleRuns = copyStyleRuns(source.styleRuns); break
|
||||
}
|
||||
}
|
||||
|
||||
function copiedSync(key: CopiedSyncKey, field: ProtectedField): SyncFn {
|
||||
return (source, target, updates, protections) => {
|
||||
if (source[key] !== target[key] && canSync(protections, target.id, field)) {
|
||||
assignCopiedUpdate(key, source, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const COPIED_SYNCERS: SyncFn[] = [
|
||||
copiedSync('fills', 'fills'),
|
||||
copiedSync('strokes', 'strokes'),
|
||||
copiedSync('effects', 'effects'),
|
||||
copiedSync('styleRuns', 'styleRuns')
|
||||
]
|
||||
|
||||
function syncFields(
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
updates: Partial<SceneNode>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
for (const sync of DIRECT_SYNCERS) sync(source, target, updates, protections)
|
||||
for (const sync of COPIED_SYNCERS) sync(source, target, updates, protections)
|
||||
}
|
||||
|
||||
export function syncNodeProps(
|
||||
graph: SceneGraph,
|
||||
source: SceneNode,
|
||||
target: SceneNode,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
const updates: Partial<SceneNode> = {}
|
||||
syncFields(source, target, updates, protections)
|
||||
if (Object.keys(updates).length > 0) graph.updateNode(target.id, updates)
|
||||
}
|
||||
3
packages/core/src/kiwi/instance-overrides/sync/index.ts
Normal file
3
packages/core/src/kiwi/instance-overrides/sync/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { syncNodeProps } from './fields'
|
||||
export { buildClonesMap, recloneChildren, syncChildrenDeep } from './clones'
|
||||
export { propagateOverridesTransitively } from './propagate'
|
||||
88
packages/core/src/kiwi/instance-overrides/sync/propagate.ts
Normal file
88
packages/core/src/kiwi/instance-overrides/sync/propagate.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { ProtectionMap } from '#core/kiwi/instance-overrides/patches'
|
||||
import type { SceneGraph } from '#core/scene-graph'
|
||||
|
||||
import { buildClonesMap, syncChildrenDeep } from './clones'
|
||||
import { syncNodeProps } from './fields'
|
||||
|
||||
function expandSeedsToParents(graph: SceneGraph, seeds: Set<string>): Set<string> {
|
||||
const expanded = new Set(seeds)
|
||||
for (const seedId of seeds) {
|
||||
let cur = graph.getNode(seedId)
|
||||
while (cur?.parentId) {
|
||||
const parent = graph.getNode(cur.parentId)
|
||||
if (!parent) break
|
||||
if (parent.type === 'INSTANCE' || parent.type === 'COMPONENT') expanded.add(parent.id)
|
||||
cur = parent
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
function buildNeedsSyncSet(
|
||||
expandedSeeds: Set<string>,
|
||||
clonesOf: Map<string, string[]>
|
||||
): Set<string> {
|
||||
const needsSync = new Set<string>()
|
||||
const queue = [...expandedSeeds]
|
||||
for (let id = queue.pop(); id !== undefined; id = queue.pop()) {
|
||||
const clones = clonesOf.get(id)
|
||||
if (!clones) continue
|
||||
for (const cloneId of clones) {
|
||||
if (needsSync.has(cloneId)) continue
|
||||
needsSync.add(cloneId)
|
||||
queue.push(cloneId)
|
||||
}
|
||||
}
|
||||
return needsSync
|
||||
}
|
||||
|
||||
export function propagateOverridesTransitively(
|
||||
graph: SceneGraph,
|
||||
seeds: Set<string>,
|
||||
swappedInstances: Set<string>,
|
||||
componentIdRoot: Map<string, string>,
|
||||
protect?: Set<string>,
|
||||
activeNodeIds?: Set<string>,
|
||||
protections?: ProtectionMap
|
||||
): void {
|
||||
if (seeds.size === 0) return
|
||||
|
||||
componentIdRoot.clear()
|
||||
const clonesOf = buildClonesMap(graph, activeNodeIds)
|
||||
const expandedSeeds = expandSeedsToParents(graph, seeds)
|
||||
const needsSync = buildNeedsSyncSet(expandedSeeds, clonesOf)
|
||||
const skip = protect && protect.size > 0 ? new Set([...seeds, ...protect]) : seeds
|
||||
|
||||
const visited = new Set<string>()
|
||||
const syncQueue = [...expandedSeeds]
|
||||
let index = 0
|
||||
while (index < syncQueue.length) {
|
||||
const sourceId = syncQueue[index]
|
||||
index++
|
||||
const clones = clonesOf.get(sourceId)
|
||||
if (!clones) continue
|
||||
const source = graph.getNode(sourceId)
|
||||
if (!source) continue
|
||||
|
||||
for (const cloneId of clones) {
|
||||
if (!needsSync.has(cloneId) || visited.has(cloneId)) continue
|
||||
visited.add(cloneId)
|
||||
const node = graph.getNode(cloneId)
|
||||
if (!node) continue
|
||||
|
||||
if (skip.has(cloneId)) {
|
||||
syncQueue.push(cloneId)
|
||||
continue
|
||||
}
|
||||
|
||||
syncNodeProps(graph, source, node, protections)
|
||||
if (source.childIds.length !== node.childIds.length) {
|
||||
for (const childId of Array.from(node.childIds)) graph.deleteNode(childId)
|
||||
if (source.childIds.length > 0) graph.populateInstanceChildren(node.id, sourceId)
|
||||
} else if (source.childIds.length > 0 && node.childIds.length > 0) {
|
||||
syncChildrenDeep(graph, sourceId, node.id, swappedInstances, skip, protections)
|
||||
}
|
||||
syncQueue.push(cloneId)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue