Fix override resolution for deleted internal page nodes
Two fixes for component instances with multi-level nesting: 1. getComponentRoot now falls back to kiwi symbolData when graph nodes are deleted (internal page cleanup). Resolves the component chain via symbolData.symbolID so findNodeByComponentId can match clones whose componentId points to a deleted source node. 2. propagateOverridesTransitively skips nodes that were directly targeted by applySymbolOverrides. Previously the sync would overwrite override values (e.g. text 'Health' → 'Badge') with the source component's defaults.
This commit is contained in:
parent
8b05fa6ea4
commit
872c53dd9b
|
|
@ -1,4 +1,4 @@
|
|||
import type { SceneGraph, SceneNode } from '../scene-graph'
|
||||
import type { SceneGraph, SceneNode, GeometryPath } from '../scene-graph'
|
||||
import { guidToString, convertOverrideToProps, resolveGeometryPaths } from './kiwi-convert'
|
||||
import type { GUID } from './codec'
|
||||
|
||||
|
|
@ -80,18 +80,46 @@ export function populateAndApplyOverrides(
|
|||
if (nc.overrideKey) overrideKeyToGuid.set(guidToString(nc.overrideKey), id)
|
||||
}
|
||||
|
||||
// Reverse map: graph node ID → figma GUID (used by getComponentRoot kiwi fallback)
|
||||
const nodeIdToGuid = new Map<string, string>()
|
||||
for (const [figmaId, nodeId] of guidToNodeId) {
|
||||
nodeIdToGuid.set(nodeId, figmaId)
|
||||
}
|
||||
|
||||
// Component root resolution (walks componentId chain to the ultimate source)
|
||||
const componentIdRoot = new Map<string, string>()
|
||||
function getComponentRoot(nodeId: string, depth = 0): string {
|
||||
if (componentIdRoot.has(nodeId)) return componentIdRoot.get(nodeId) ?? nodeId
|
||||
const node = graph.getNode(nodeId)
|
||||
if (!node?.componentId || depth > 20) {
|
||||
if (depth > 20) {
|
||||
componentIdRoot.set(nodeId, nodeId)
|
||||
return nodeId
|
||||
}
|
||||
const root = getComponentRoot(node.componentId, depth + 1)
|
||||
componentIdRoot.set(nodeId, root)
|
||||
return root
|
||||
|
||||
// Try graph first
|
||||
const node = graph.getNode(nodeId)
|
||||
if (node?.componentId) {
|
||||
const root = getComponentRoot(node.componentId, depth + 1)
|
||||
componentIdRoot.set(nodeId, root)
|
||||
return root
|
||||
}
|
||||
|
||||
// For deleted nodes (internal page), resolve via kiwi symbolData
|
||||
const figmaId = nodeIdToGuid.get(nodeId)
|
||||
if (figmaId) {
|
||||
const nc = changeMap.get(figmaId)
|
||||
const symId = nc?.symbolData?.symbolID
|
||||
if (symId) {
|
||||
const compNodeId = guidToNodeId.get(guidToString(symId))
|
||||
if (compNodeId && compNodeId !== nodeId) {
|
||||
const root = getComponentRoot(compNodeId, depth + 1)
|
||||
componentIdRoot.set(nodeId, root)
|
||||
return root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentIdRoot.set(nodeId, nodeId)
|
||||
return nodeId
|
||||
}
|
||||
|
||||
function findNodeByComponentId(parentId: string, componentId: string): string | null {
|
||||
|
|
@ -131,12 +159,6 @@ export function populateAndApplyOverrides(
|
|||
return currentId
|
||||
}
|
||||
|
||||
// Reverse map: graph node ID → figma GUID
|
||||
const nodeIdToGuid = new Map<string, string>()
|
||||
for (const [figmaId, nodeId] of guidToNodeId) {
|
||||
nodeIdToGuid.set(nodeId, figmaId)
|
||||
}
|
||||
|
||||
// Apply component property assignments (boolean visibility, instance swap).
|
||||
// Component children reference property definitions via componentPropRefs.
|
||||
// Instances set values via componentPropAssignments. After cloning, we walk
|
||||
|
|
@ -179,27 +201,44 @@ export function populateAndApplyOverrides(
|
|||
}
|
||||
if (propRefsMap.size === 0) return
|
||||
|
||||
// Collect all assignment sources: figmaGuid → assignments[]
|
||||
// Sources: top-level on instance nodes, and inside symbolOverrides
|
||||
const assignmentSources = new Map<string, ComponentPropAssignment[]>()
|
||||
for (const [figmaId, nc] of changeMap) {
|
||||
if (nc.componentPropAssignments?.length) {
|
||||
assignmentSources.set(figmaId, nc.componentPropAssignments)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply assignments from cloned instance sources. After population,
|
||||
// cloned instances have componentId pointing to the original kiwi node.
|
||||
// If that node had componentPropAssignments, apply them to the clone.
|
||||
for (const node of graph.getAllNodes()) {
|
||||
if (node.type !== 'INSTANCE' || !node.componentId) continue
|
||||
const sourceFigmaId = nodeIdToGuid.get(node.componentId)
|
||||
if (!sourceFigmaId) continue
|
||||
const assignments = assignmentSources.get(sourceFigmaId)
|
||||
if (!assignments) continue
|
||||
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of assignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(node.id, valueByDef, propRefsMap)
|
||||
}
|
||||
|
||||
// Apply assignments from symbolOverrides, scoped to the nested
|
||||
// instance their guidPath resolves to. These override the defaults
|
||||
// set above.
|
||||
for (const [figmaId, nc] of changeMap) {
|
||||
const instanceNodeId = guidToNodeId.get(figmaId)
|
||||
if (!instanceNodeId) continue
|
||||
if (graph.getNode(instanceNodeId)?.type !== 'INSTANCE') continue
|
||||
|
||||
// Top-level assignments apply to the instance itself
|
||||
if (nc.componentPropAssignments?.length) {
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of nc.componentPropAssignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(instanceNodeId, valueByDef, propRefsMap)
|
||||
}
|
||||
|
||||
// symbolOverride entries with componentPropAssignments are scoped to
|
||||
// the nested instance their guidPath resolves to
|
||||
const overrides = nc.symbolData?.symbolOverrides
|
||||
if (!overrides) continue
|
||||
for (const ov of overrides) {
|
||||
if (!ov.componentPropAssignments?.length) continue
|
||||
const nested = ov.componentPropAssignments
|
||||
|
||||
const guids = ov.guidPath?.guids
|
||||
if (!guids?.length) continue
|
||||
|
|
@ -208,7 +247,7 @@ export function populateAndApplyOverrides(
|
|||
if (!targetId) continue
|
||||
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of nested) {
|
||||
for (const a of ov.componentPropAssignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(targetId, valueByDef, propRefsMap)
|
||||
|
|
@ -256,11 +295,7 @@ export function populateAndApplyOverrides(
|
|||
// Apply derivedSymbolData — pre-computed sizes for the current set of
|
||||
// component property values. Uses the same guidPath resolution as
|
||||
// symbolOverrides.
|
||||
function scaleGeometryBlobs(
|
||||
geom: import('../scene-graph').GeometryPath[],
|
||||
sx: number,
|
||||
sy: number
|
||||
): import('../scene-graph').GeometryPath[] {
|
||||
function scaleGeometryBlobs(geom: GeometryPath[], sx: number, sy: number): GeometryPath[] {
|
||||
if (sx === 1 && sy === 1) return geom
|
||||
return geom.map((g) => {
|
||||
const src = g.commandsBlob
|
||||
|
|
@ -331,10 +366,8 @@ export function populateAndApplyOverrides(
|
|||
}
|
||||
}
|
||||
|
||||
// Apply overrides from each INSTANCE's symbolData
|
||||
const overriddenNodes = new Set<string>()
|
||||
|
||||
function applySymbolOverrides() {
|
||||
function applySymbolOverrides(): Set<string> {
|
||||
const overriddenNodes = new Set<string>()
|
||||
componentIdRoot.clear()
|
||||
|
||||
for (const [ncId, nc] of changeMap) {
|
||||
|
|
@ -368,6 +401,7 @@ export function populateAndApplyOverrides(
|
|||
}
|
||||
}
|
||||
}
|
||||
return overriddenNodes
|
||||
}
|
||||
|
||||
function propagateOverridesTransitively(seeds: Set<string>) {
|
||||
|
|
@ -410,6 +444,12 @@ export function populateAndApplyOverrides(
|
|||
const node = graph.getNode(cloneId)
|
||||
if (!node) continue
|
||||
|
||||
// Don't overwrite nodes that were directly targeted by symbolOverrides
|
||||
if (seeds.has(cloneId)) {
|
||||
syncQueue.push(cloneId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.type === 'INSTANCE' && source.type === 'INSTANCE' && node.componentId) {
|
||||
repopulateInstance(node.id, node.componentId)
|
||||
} else {
|
||||
|
|
@ -442,7 +482,7 @@ export function populateAndApplyOverrides(
|
|||
// 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
|
||||
applySymbolOverrides()
|
||||
const overriddenNodes = applySymbolOverrides()
|
||||
|
||||
propagateOverridesTransitively(overriddenNodes)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue