diff --git a/packages/core/src/io/formats/fig/export.ts b/packages/core/src/io/formats/fig/export.ts index f5edc4511..30618d67c 100644 --- a/packages/core/src/io/formats/fig/export.ts +++ b/packages/core/src/io/formats/fig/export.ts @@ -29,6 +29,13 @@ const THUMBNAIL_1X1 = Uint8Array.from( ) type KiwiNodeChange = NodeChange & Record +type FigExportPage = ReturnType[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 +): { 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() const fontDigestMap = await buildFontDigestMap(graph) const glyphBlobMap = new Map() - 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) } diff --git a/packages/core/src/kiwi/fig/codec/schema/fig.kiwi b/packages/core/src/kiwi/fig/codec/schema/fig.kiwi index 6751bfbdc..ff299ab86 100644 --- a/packages/core/src/kiwi/fig/codec/schema/fig.kiwi +++ b/packages/core/src/kiwi/fig/codec/schema/fig.kiwi @@ -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 { diff --git a/packages/core/src/kiwi/fig/node-change/convert.ts b/packages/core/src/kiwi/fig/node-change/convert.ts index e8d12f5ef..7294d604b 100644 --- a/packages/core/src/kiwi/fig/node-change/convert.ts +++ b/packages/core/src/kiwi/fig/node-change/convert.ts @@ -710,6 +710,8 @@ const FIGMA_RAW_NODE_FIELD_KEYS = [ 'componentPropRefs', 'variantPropSpecs', 'stateGroupPropertyValueOrders', + 'isStateGroup', + 'version', 'sourceLibraryKey', 'userFacingVersion', 'sortPosition', diff --git a/packages/core/src/kiwi/fig/node-change/export-node.ts b/packages/core/src/kiwi/fig/node-change/export-node.ts index ce2f1a419..f996597be 100644 --- a/packages/core/src/kiwi/fig/node-change/export-node.ts +++ b/packages/core/src/kiwi/fig/node-change/export-node.ts @@ -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 = {} 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 diff --git a/packages/core/src/kiwi/fig/node-change/serialize.ts b/packages/core/src/kiwi/fig/node-change/serialize.ts index 8a1192f9a..aed699e47 100644 --- a/packages/core/src/kiwi/fig/node-change/serialize.ts +++ b/packages/core/src/kiwi/fig/node-change/serialize.ts @@ -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