Fix component override resolution for nested instances

- Fix population ordering: ensure source instances are populated
  before their clones (bottom-up via ensurePopulated recursive walk)
- Fix componentPropAssignment resolution: walk full componentId chain
  to find kiwi source with assignments, not just single-hop lookup
- Fixes visibility overrides not applied in deeply nested instances
- Fixes text content overrides not applied through clone chains
- Reduces missing nodes from 41 to 16 in gold-preview.fig test
This commit is contained in:
Danila Poyarkov 2026-03-13 17:29:36 +03:00
parent 00da8bde1b
commit c5e8bb431f

View file

@ -64,38 +64,55 @@ export function populateAndApplyOverrides(
guidToNodeId: Map<string, string>,
blobs: Uint8Array[] = []
): void {
// Work-queue population: seed with empty instances, then discover newly
// cloned nested instances by walking the subtree (avoids full graph scan per round).
const populateQueue: string[] = []
for (const node of graph.getAllNodes()) {
if (node.type === 'INSTANCE' && node.componentId && node.childIds.length === 0) {
populateQueue.push(node.id)
}
}
// Populate empty INSTANCE nodes from their source components. Instances
// must be populated bottom-up: if an instance's source is itself an
// unpopulated instance, populate the source first so cloned children
// are complete.
function ensurePopulated(nodeId: string, visiting: Set<string>): void {
const node = graph.getNode(nodeId)
if (!node || node.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) return
if (visiting.has(nodeId)) return
visiting.add(nodeId)
function collectEmptyInstances(parentId: string, out: string[]) {
const parent = graph.getNode(parentId)
if (!parent) return
for (const childId of parent.childIds) {
const comp = graph.getNode(node.componentId)
if (!comp) return
// If the source is an unpopulated instance, populate it first
if (comp.type === 'INSTANCE' && comp.componentId && comp.childIds.length === 0) {
ensurePopulated(comp.id, visiting)
}
// Also ensure children of the source are populated (nested instances)
for (const childId of comp.childIds) {
const child = graph.getNode(childId)
if (!child) continue
if (child.type === 'INSTANCE' && child.componentId && child.childIds.length === 0) {
out.push(child.id)
} else if (child.childIds.length > 0) {
collectEmptyInstances(childId, out)
if (child?.type === 'INSTANCE' && child.componentId && child.childIds.length === 0) {
ensurePopulated(childId, visiting)
}
}
if (comp.childIds.length > 0 && node.childIds.length === 0) {
graph.populateInstanceChildren(nodeId, node.componentId)
}
}
while (populateQueue.length > 0) {
const nodeId = populateQueue.pop()
if (!nodeId) continue
const node = graph.getNode(nodeId)
if (node?.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) continue
const comp = graph.getNode(node.componentId)
if (comp && comp.childIds.length > 0) {
graph.populateInstanceChildren(nodeId, node.componentId)
collectEmptyInstances(nodeId, populateQueue)
const visiting = new Set<string>()
for (const node of graph.getAllNodes()) {
if (node.type === 'INSTANCE' && node.componentId && node.childIds.length === 0) {
ensurePopulated(node.id, visiting)
}
}
// Second pass: cloning may have introduced new empty instances not seen
// in the first pass (nested clones). Repeat until stable.
let changed = true
while (changed) {
changed = false
for (const node of graph.getAllNodes()) {
if (node.type === 'INSTANCE' && node.componentId && node.childIds.length === 0) {
const comp = graph.getNode(node.componentId)
if (comp && comp.childIds.length > 0) {
graph.populateInstanceChildren(node.id, node.componentId)
changed = true
}
}
}
}
@ -282,15 +299,24 @@ export function populateAndApplyOverrides(
}
}
// Also apply from cloned instance sources — after population, cloned
// instances have componentId pointing to the original kiwi node
// Walk the componentId chain to find a kiwi source with assignments.
// Cloned instances may be several levels deep (clone of clone of …),
// so a single-hop lookup is insufficient.
if (!node.componentId) continue
const sourceFigmaId = nodeIdToGuid.get(node.componentId)
if (!sourceFigmaId) continue
const assignments = assignmentSources.get(sourceFigmaId)
if (!assignments) continue
applyPropAssignments(node.id, assignmentsToValueMap(assignments), propRefsMap)
let sourceId: string | undefined = node.componentId
for (let depth = 0; sourceId && depth < 20; depth++) {
const figmaId = nodeIdToGuid.get(sourceId)
if (figmaId) {
const assignments = assignmentSources.get(figmaId)
if (assignments) {
applyPropAssignments(node.id, assignmentsToValueMap(assignments), propRefsMap)
break
}
}
const n = graph.getNode(sourceId)
if (!n?.componentId || n.componentId === sourceId) break
sourceId = n.componentId
}
}
}
@ -528,7 +554,8 @@ export function populateAndApplyOverrides(
overriddenNodes.add(targetId)
if (ov.overriddenSymbolID) {
const newCompId = guidToNodeId.get(guidToString(ov.overriddenSymbolID))
const swapGuid = guidToString(ov.overriddenSymbolID)
const newCompId = guidToNodeId.get(swapGuid)
if (newCompId) repopulateInstance(targetId, newCompId)
}
@ -666,11 +693,9 @@ export function populateAndApplyOverrides(
}
// Order matters:
// 1. symbolOverrides — set property values and swap instances
// 2. transitive sync — propagate overrides through clone chains (may
// repopulate INSTANCE children, wiping any earlier property changes)
// 1. symbolOverrides — set property values and swap instances (kiwi + clones)
// 2. transitive sync — propagate overrides through remaining clone chains
// 3. componentProperties — toggle visibility / swap via prop assignments
// (must run AFTER sync so repopulated children aren't lost)
// 4. derivedSymbolData — apply Figma's pre-computed sizes last
const overriddenNodes = applySymbolOverrides()
@ -678,7 +703,5 @@ export function populateAndApplyOverrides(
applyComponentProperties()
// DSD resolution runs AFTER overrides so guidPaths can reach children
// of instance-swapped nodes (repopulateInstance replaces children).
applyDerivedSymbolData()
}