fix(fig): preserve component prop reference maps
- Export component sets with Figma-compatible frame state-group metadata - Emit internal component definitions before page instances - Preserve prop-ref variable maps while continuing to strip unsafe alias maps
This commit is contained in:
parent
2897e0b20f
commit
b784882e88
|
|
@ -29,6 +29,13 @@ const THUMBNAIL_1X1 = Uint8Array.from(
|
|||
)
|
||||
|
||||
type KiwiNodeChange = NodeChange & Record<string, unknown>
|
||||
type FigExportPage = ReturnType<SceneGraph['getPages']>[number]
|
||||
|
||||
interface CanvasExportEntry {
|
||||
page: FigExportPage
|
||||
canvasGuid: GUID
|
||||
canvasNc: KiwiNodeChange
|
||||
}
|
||||
|
||||
function variableValueToKiwi(
|
||||
value: VariableValue,
|
||||
|
|
@ -170,6 +177,50 @@ function appendVariablesForCollection(
|
|||
}
|
||||
}
|
||||
|
||||
function buildCanvasEntries(
|
||||
graph: SceneGraph,
|
||||
pages: FigExportPage[],
|
||||
docGuid: GUID,
|
||||
localIdCounter: { value: number },
|
||||
nodeIdToGuid: Map<string, GUID>
|
||||
): { canvasEntries: CanvasExportEntry[]; internalCanvasGuid: GUID | null } {
|
||||
const canvasEntries: CanvasExportEntry[] = []
|
||||
let internalCanvasGuid: GUID | null = null
|
||||
for (let p = 0; p < pages.length; p++) {
|
||||
const page = pages[p]
|
||||
const canvasGuid = page.figmaGuid
|
||||
? stringToGuid(page.figmaGuid)
|
||||
: { sessionID: 0, localID: localIdCounter.value++ }
|
||||
nodeIdToGuid.set(page.id, canvasGuid)
|
||||
if (page.internalOnly) internalCanvasGuid = canvasGuid
|
||||
|
||||
const canvasNc = makeCanvasNodeChange(canvasGuid, docGuid, fractionalPosition(p), page.name, {
|
||||
backgroundOpacity: 1,
|
||||
backgroundColor: { ...CANVAS_BG_COLOR },
|
||||
backgroundEnabled: true
|
||||
})
|
||||
if (page.internalOnly) canvasNc.internalOnly = true
|
||||
canvasEntries.push({ page, canvasGuid, canvasNc })
|
||||
}
|
||||
|
||||
if (graph.variableCollections.size > 0 && internalCanvasGuid === null) {
|
||||
internalCanvasGuid = { sessionID: 0, localID: localIdCounter.value++ }
|
||||
canvasEntries.push({
|
||||
page: { id: '', name: 'Internal Only Canvas', internalOnly: true } as FigExportPage,
|
||||
canvasGuid: internalCanvasGuid,
|
||||
canvasNc: makeCanvasNodeChange(
|
||||
internalCanvasGuid,
|
||||
docGuid,
|
||||
fractionalPosition(canvasEntries.length),
|
||||
'Internal Only Canvas',
|
||||
{ internalOnly: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return { canvasEntries, internalCanvasGuid }
|
||||
}
|
||||
|
||||
export async function exportFigFile(
|
||||
graph: SceneGraph,
|
||||
ck?: CanvasKit,
|
||||
|
|
@ -193,25 +244,24 @@ export async function exportFigFile(
|
|||
const modeIdToGuid = new Map<string, GUID>()
|
||||
const fontDigestMap = await buildFontDigestMap(graph)
|
||||
const glyphBlobMap = new Map<string, number>()
|
||||
let internalCanvasGuid: GUID | null = null
|
||||
|
||||
assignVariableGuids(graph, localIdCounter, varIdToGuid, modeIdToGuid)
|
||||
|
||||
for (let p = 0; p < pages.length; p++) {
|
||||
const page = pages[p]
|
||||
const canvasLocalID = localIdCounter.value++
|
||||
const canvasGuid = { sessionID: 0, localID: canvasLocalID }
|
||||
const { canvasEntries, internalCanvasGuid } = buildCanvasEntries(
|
||||
graph,
|
||||
pages,
|
||||
docGuid,
|
||||
localIdCounter,
|
||||
nodeIdToGuid
|
||||
)
|
||||
|
||||
if (page.internalOnly) internalCanvasGuid = canvasGuid
|
||||
|
||||
const canvasNc = makeCanvasNodeChange(canvasGuid, docGuid, fractionalPosition(p), page.name, {
|
||||
backgroundOpacity: 1,
|
||||
backgroundColor: { ...CANVAS_BG_COLOR },
|
||||
backgroundEnabled: true
|
||||
})
|
||||
if (page.internalOnly) canvasNc.internalOnly = true
|
||||
nodeChanges.push(canvasNc)
|
||||
for (const entry of canvasEntries) nodeChanges.push(entry.canvasNc)
|
||||
|
||||
const orderedCanvasEntries = [
|
||||
...canvasEntries.filter((entry) => entry.page.internalOnly),
|
||||
...canvasEntries.filter((entry) => !entry.page.internalOnly)
|
||||
]
|
||||
for (const { page, canvasGuid } of orderedCanvasEntries) {
|
||||
const children = graph.getChildren(page.id).filter((child) => !child.internalOnly)
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
nodeChanges.push(
|
||||
|
|
@ -231,21 +281,7 @@ export async function exportFigFile(
|
|||
}
|
||||
}
|
||||
|
||||
if (graph.variableCollections.size > 0) {
|
||||
if (!internalCanvasGuid) {
|
||||
const internalLocalID = localIdCounter.value++
|
||||
internalCanvasGuid = { sessionID: 0, localID: internalLocalID }
|
||||
nodeChanges.push(
|
||||
makeCanvasNodeChange(
|
||||
internalCanvasGuid,
|
||||
docGuid,
|
||||
fractionalPosition(pages.length),
|
||||
'Internal Only Canvas',
|
||||
{ internalOnly: true }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (graph.variableCollections.size > 0 && internalCanvasGuid) {
|
||||
appendVariableNodeChanges(graph, nodeChanges, internalCanvasGuid, varIdToGuid, modeIdToGuid)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2147,6 +2147,10 @@ message SymbolId {
|
|||
GUID guid = 1;
|
||||
}
|
||||
|
||||
message PropRefValue {
|
||||
GUID defId = 1;
|
||||
}
|
||||
|
||||
message VariableSetID {
|
||||
GUID guid = 1;
|
||||
}
|
||||
|
|
@ -2158,6 +2162,7 @@ message VariableAnyValue {
|
|||
VariableID alias = 4;
|
||||
Color colorValue = 5;
|
||||
SymbolId symbolIdValue = 8;
|
||||
PropRefValue propRefValue = 13;
|
||||
}
|
||||
|
||||
message VariableData {
|
||||
|
|
|
|||
|
|
@ -710,6 +710,8 @@ const FIGMA_RAW_NODE_FIELD_KEYS = [
|
|||
'componentPropRefs',
|
||||
'variantPropSpecs',
|
||||
'stateGroupPropertyValueOrders',
|
||||
'isStateGroup',
|
||||
'version',
|
||||
'sourceLibraryKey',
|
||||
'userFacingVersion',
|
||||
'sortPosition',
|
||||
|
|
|
|||
|
|
@ -102,6 +102,19 @@ function parseGuidOrNull(value: string) {
|
|||
const FIGMA_PAYLOAD_VARIABLE_MAP_FIELDS = new Set(['variableConsumptionMap', 'parameterConsumptionMap'])
|
||||
const FIGMA_PAYLOAD_PAINT_VARIABLE_FIELDS = new Set(['colorVar', 'opacityVar'])
|
||||
|
||||
function isPropRefVariableMapEntry(value: unknown): boolean {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const entry = value as { variableData?: { dataType?: string; value?: { propRefValue?: unknown } } }
|
||||
return entry.variableData?.dataType === 'PROP_REF' || !!entry.variableData?.value?.propRefValue
|
||||
}
|
||||
|
||||
function materializeSafeVariableMap(value: unknown, blobs: Uint8Array[]): unknown {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
||||
const entries = (value as { entries?: unknown[] }).entries?.filter(isPropRefVariableMapEntry) ?? []
|
||||
if (entries.length === 0) return undefined
|
||||
return { entries: entries.map((entry) => materializeFigmaPayload(entry, blobs)) }
|
||||
}
|
||||
|
||||
function materializeFigmaPayload(
|
||||
value: unknown,
|
||||
blobs: Uint8Array[],
|
||||
|
|
@ -122,7 +135,11 @@ function materializeFigmaPayload(
|
|||
const materialized: Record<string, unknown> = {}
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (FIGMA_PAYLOAD_PAINT_VARIABLE_FIELDS.has(key)) continue
|
||||
if (!options.includeVariableMaps && FIGMA_PAYLOAD_VARIABLE_MAP_FIELDS.has(key)) continue
|
||||
if (!options.includeVariableMaps && FIGMA_PAYLOAD_VARIABLE_MAP_FIELDS.has(key)) {
|
||||
const variableMap = materializeSafeVariableMap(child, blobs)
|
||||
if (variableMap !== undefined) materialized[key] = variableMap
|
||||
continue
|
||||
}
|
||||
materialized[key] = materializeFigmaPayload(child, blobs, options)
|
||||
}
|
||||
return materialized
|
||||
|
|
@ -213,8 +230,8 @@ function applyComponentMetadata(node: SceneNode, nc: KiwiNodeChange): void {
|
|||
if (overrideKey) nc.overrideKey = overrideKey
|
||||
if (node.sharedSymbolVersion) nc.sharedSymbolVersion = node.sharedSymbolVersion
|
||||
if (node.publishedVersion) nc.publishedVersion = node.publishedVersion
|
||||
if (node.isPublishable) nc.isPublishable = true
|
||||
if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET' || node.isSymbolPublishable) {
|
||||
if (node.type === 'COMPONENT_SET' || node.isPublishable) nc.isPublishable = node.isPublishable
|
||||
if (node.type === 'COMPONENT' || node.isSymbolPublishable) {
|
||||
nc.isSymbolPublishable = node.isSymbolPublishable
|
||||
}
|
||||
if (node.symbolDescription) nc.symbolDescription = node.symbolDescription
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function mapToFigmaType(type: SceneNode['type']): string {
|
|||
case 'COMPONENT':
|
||||
return 'SYMBOL'
|
||||
case 'COMPONENT_SET':
|
||||
return 'SYMBOL'
|
||||
return 'FRAME'
|
||||
case 'INSTANCE':
|
||||
return 'INSTANCE'
|
||||
case 'CONNECTOR':
|
||||
|
|
@ -250,21 +250,21 @@ function fillToKiwiPaint(f: SceneNode['fills'][number]): Paint {
|
|||
}
|
||||
|
||||
function serializeCornerRadii(node: SceneNode, nc: KiwiNodeChange): void {
|
||||
if (node.cornerRadius > 0 || node.independentCorners) {
|
||||
const hasCornerRadius = node.independentCorners
|
||||
? node.topLeftRadius > 0 ||
|
||||
node.topRightRadius > 0 ||
|
||||
node.bottomLeftRadius > 0 ||
|
||||
node.bottomRightRadius > 0
|
||||
: node.cornerRadius > 0
|
||||
if (hasCornerRadius) {
|
||||
nc.cornerRadius = node.cornerRadius
|
||||
nc.rectangleCornerRadiiIndependent = node.independentCorners
|
||||
nc.rectangleTopLeftCornerRadius = node.independentCorners
|
||||
? node.topLeftRadius
|
||||
: node.cornerRadius
|
||||
nc.rectangleTopRightCornerRadius = node.independentCorners
|
||||
? node.topRightRadius
|
||||
: node.cornerRadius
|
||||
nc.rectangleBottomLeftCornerRadius = node.independentCorners
|
||||
? node.bottomLeftRadius
|
||||
: node.cornerRadius
|
||||
nc.rectangleBottomRightCornerRadius = node.independentCorners
|
||||
? node.bottomRightRadius
|
||||
: node.cornerRadius
|
||||
if (node.independentCorners) {
|
||||
nc.rectangleCornerRadiiIndependent = true
|
||||
nc.rectangleTopLeftCornerRadius = node.topLeftRadius
|
||||
nc.rectangleTopRightCornerRadius = node.topRightRadius
|
||||
nc.rectangleBottomLeftCornerRadius = node.bottomLeftRadius
|
||||
nc.rectangleBottomRightCornerRadius = node.bottomRightRadius
|
||||
}
|
||||
}
|
||||
if (node.cornerSmoothing > 0) {
|
||||
nc.cornerSmoothing = node.cornerSmoothing
|
||||
|
|
|
|||
Loading…
Reference in a new issue