Fix DSD resolution for instance-swapped children

derivedSymbolData guidPaths reference the post-swap tree structure
(e.g., heart Vector inside a swapped home slot), but resolution was
running before overrides modified the tree. After repopulateInstance,
the old children are deleted and replaced with new ones from the
swapped component, making pre-resolved targets stale.

Changes:
- Move DSD resolution to after all overrides (symbolOverrides,
  transitive sync, componentProperties) so guidPaths can reach
  the swapped-in children
- Pre-compute componentId root map while all internal page nodes
  are alive — instance swaps delete intermediate clones, breaking
  componentId chains used by findNodeByComponentId
- Use preComputedRoot in findNodeByComponentId to match across
  clone levels even when intermediate nodes are deleted
- Propagate DSD changes through clone chains via BFS
This commit is contained in:
Danila Poyarkov 2026-03-04 21:26:00 +03:00
parent d55eb7ce9e
commit 7a645d8576

View file

@ -86,6 +86,26 @@ export function populateAndApplyOverrides(
nodeIdToGuid.set(nodeId, figmaId)
}
// Pre-compute componentId root for every node while all internal page nodes
// are still alive. After overrides, instance swaps delete intermediate clones,
// breaking the chain. DSD resolution uses this to match across clone levels.
const preComputedRoot = new Map<string, string>()
function getPreComputedRoot(nodeId: string, depth = 0): string {
if (preComputedRoot.has(nodeId)) return preComputedRoot.get(nodeId) ?? nodeId
if (depth > 20) return nodeId
const node = graph.getNode(nodeId)
if (node?.componentId && node.componentId !== nodeId) {
const root = getPreComputedRoot(node.componentId, depth + 1)
preComputedRoot.set(nodeId, root)
return root
}
preComputedRoot.set(nodeId, nodeId)
return nodeId
}
for (const node of graph.getAllNodes()) {
if (node.componentId) getPreComputedRoot(node.id)
}
// Component root resolution (walks componentId chain to the ultimate source)
const componentIdRoot = new Map<string, string>()
function getComponentRoot(nodeId: string, depth = 0): string {
@ -123,14 +143,15 @@ export function populateAndApplyOverrides(
}
function findNodeByComponentId(parentId: string, componentId: string): string | null {
const targetRoot = getComponentRoot(componentId)
const targetRoot = preComputedRoot.get(componentId) ?? getComponentRoot(componentId)
const parent = graph.getNode(parentId)
if (!parent) return null
for (const childId of parent.childIds) {
const child = graph.getNode(childId)
if (!child) continue
if (child.componentId === componentId) return childId
if (child.componentId && getComponentRoot(child.componentId) === targetRoot) return childId
const childRoot = preComputedRoot.get(childId) ?? (child.componentId ? getComponentRoot(child.componentId) : null)
if (childRoot && childRoot === targetRoot) return childId
const deep = findNodeByComponentId(childId, componentId)
if (deep) return deep
}
@ -183,6 +204,7 @@ export function populateAndApplyOverrides(
function repopulateInstance(nodeId: string, compId: string) {
const node = graph.getNode(nodeId)
if (!node || node.type !== 'INSTANCE') return
for (const childId of [...node.childIds]) graph.deleteNode(childId)
graph.updateNode(nodeId, { componentId: compId })
const comp = graph.getNode(compId)
@ -318,7 +340,15 @@ export function populateAndApplyOverrides(
})
}
// Pre-resolve DSD guidPaths while the original componentId chains are
// intact. Instance swaps (symbolOverrides, componentProperties) replace
// children and break the chains, but DSD guidPaths reference the original
// structure.
function applyDerivedSymbolData() {
const dsdModified = new Set<string>()
for (const [ncId, nc] of changeMap) {
if (nc.type !== 'INSTANCE') continue
const derived = nc.derivedSymbolData
@ -327,7 +357,8 @@ export function populateAndApplyOverrides(
const nodeId = guidToNodeId.get(ncId)
if (!nodeId) continue
for (const d of derived) {
for (let i = 0; i < derived.length; i++) {
const d = derived[i]
const guids = d.guidPath?.guids
if (!guids?.length) continue
@ -361,6 +392,49 @@ export function populateAndApplyOverrides(
if (Object.keys(updates).length > 0) {
graph.updateNode(targetId, updates)
dsdModified.add(targetId)
}
}
}
// Propagate DSD changes through clone chains. Clones inherit size,
// position, and geometry from their source but the normal transitive
// sync already ran before DSD.
if (dsdModified.size > 0) {
const clonesOf = new Map<string, string[]>()
for (const node of graph.getAllNodes()) {
if (!node.componentId) continue
let arr = clonesOf.get(node.componentId)
if (!arr) {
arr = []
clonesOf.set(node.componentId, arr)
}
arr.push(node.id)
}
const queue = [...dsdModified]
const visited = new Set(dsdModified)
for (let sourceId = queue.shift(); sourceId !== undefined; sourceId = queue.shift()) {
const source = 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 = graph.getNode(cloneId)
if (!clone) continue
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 (source.fillGeometry !== clone.fillGeometry) cu.fillGeometry = structuredClone(source.fillGeometry)
if (source.strokeGeometry !== clone.strokeGeometry) cu.strokeGeometry = structuredClone(source.strokeGeometry)
if (Object.keys(cu).length > 0) graph.updateNode(cloneId, cu)
queue.push(cloneId)
}
}
}
@ -487,5 +561,8 @@ export function populateAndApplyOverrides(
propagateOverridesTransitively(overriddenNodes)
applyComponentProperties()
// DSD resolution runs AFTER overrides so guidPaths can reach children
// of instance-swapped nodes (repopulateInstance replaces children).
applyDerivedSymbolData()
}