diff --git a/packages/core/src/io/formats/fig/export.ts b/packages/core/src/io/formats/fig/export.ts index 30618d67c..545f9a800 100644 --- a/packages/core/src/io/formats/fig/export.ts +++ b/packages/core/src/io/formats/fig/export.ts @@ -8,6 +8,7 @@ import { populateAllLazyFigImportRoots } from '#core/kiwi/fig/lazy-import' import { initCodec, getCompiledSchema, getSchemaBytes } from '#core/kiwi/fig/codec' import type { NodeChange } from '#core/kiwi/fig/codec' import { stringToGuid } from '#core/kiwi/fig/node-change/convert' +import { buildFigmaPaintVariableColorMap } from '#core/kiwi/fig/node-change/export-node' import { sceneNodeToKiwi, fractionalPosition, @@ -244,6 +245,7 @@ export async function exportFigFile( const modeIdToGuid = new Map() const fontDigestMap = await buildFontDigestMap(graph) const glyphBlobMap = new Map() + const paintVariableColorMap = buildFigmaPaintVariableColorMap(graph) assignVariableGuids(graph, localIdCounter, varIdToGuid, modeIdToGuid) @@ -275,7 +277,8 @@ export async function exportFigFile( nodeIdToGuid, fontDigestMap, varIdToGuid, - glyphBlobMap + glyphBlobMap, + paintVariableColorMap ) ) } diff --git a/packages/core/src/kiwi/fig/node-change/convert.ts b/packages/core/src/kiwi/fig/node-change/convert.ts index 7294d604b..f93fb62b9 100644 --- a/packages/core/src/kiwi/fig/node-change/convert.ts +++ b/packages/core/src/kiwi/fig/node-change/convert.ts @@ -721,6 +721,18 @@ const FIGMA_RAW_NODE_FIELD_KEYS = [ 'miterLimit', 'strokeWeight', 'strokeJoin', + 'textAutoResize', + 'textData', + 'lineHeight', + 'fontName', + 'fontSize', + 'letterSpacing', + 'textTracking', + 'fontVersion', + 'textUserLayoutVersion', + 'textExplicitLayoutVersion', + 'fontVariations', + 'derivedTextData', 'fillPaints', 'strokePaints', 'effects', 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 f996597be..b63f6a6ae 100644 --- a/packages/core/src/kiwi/fig/node-change/export-node.ts +++ b/packages/core/src/kiwi/fig/node-change/export-node.ts @@ -19,6 +19,7 @@ interface SceneNodeToKiwiContext { fontDigestMap?: Map glyphBlobMap?: Map varIdToGuid?: Map + paintVariableColorMap?: Map fractionalPosition: (index: number) => string mapToFigmaType: (type: SceneNode['type']) => string fillToKiwiPaint: (fill: SceneNode['fills'][number]) => Paint @@ -115,10 +116,23 @@ function materializeSafeVariableMap(value: unknown, blobs: Uint8Array[]): unknow return { entries: entries.map((entry) => materializeFigmaPayload(entry, blobs)) } } +function paintVariableKey(value: unknown): string | null { + if (!value || typeof value !== 'object') return null + const assetRef = (value as { value?: { alias?: { assetRef?: { key?: unknown; version?: unknown } } } }) + .value?.alias?.assetRef + return typeof assetRef?.key === 'string' + ? `${assetRef.key}:${typeof assetRef.version === 'string' ? assetRef.version : ''}` + : null +} + function materializeFigmaPayload( value: unknown, blobs: Uint8Array[], - options: { includeVariableMaps?: boolean } = {} + options: { + includePaintVariables?: boolean + includeVariableMaps?: boolean + paintVariableColorMap?: Map + } = {} ): unknown { if (value instanceof Uint8Array) return value if (Array.isArray(value)) return value.map((item) => materializeFigmaPayload(item, blobs, options)) @@ -133,8 +147,11 @@ function materializeFigmaPayload( } const materialized: Record = {} + const paintVariableColor = options.paintVariableColorMap?.get( + paintVariableKey((value as { colorVar?: unknown }).colorVar) ?? '' + ) for (const [key, child] of Object.entries(value)) { - if (FIGMA_PAYLOAD_PAINT_VARIABLE_FIELDS.has(key)) continue + if (FIGMA_PAYLOAD_PAINT_VARIABLE_FIELDS.has(key) && !options.includePaintVariables) continue if (!options.includeVariableMaps && FIGMA_PAYLOAD_VARIABLE_MAP_FIELDS.has(key)) { const variableMap = materializeSafeVariableMap(child, blobs) if (variableMap !== undefined) materialized[key] = variableMap @@ -142,9 +159,52 @@ function materializeFigmaPayload( } materialized[key] = materializeFigmaPayload(child, blobs, options) } + if (paintVariableColor) materialized.color = paintVariableColor return materialized } +function collectPaintVariableColorCounts( + value: unknown, + counts: Map> +): void { + if (!value || typeof value !== 'object') return + if (Array.isArray(value)) { + for (const item of value) collectPaintVariableColorCounts(item, counts) + return + } + + const paint = value as { color?: Color; colorVar?: unknown } + const key = paintVariableKey(paint.colorVar) + if (key && paint.color) { + const colorKey = [paint.color.r, paint.color.g, paint.color.b, paint.color.a] + .map((component) => Math.round(component * 255)) + .join(',') + const colorCounts = counts.get(key) ?? new Map() + const current = colorCounts.get(colorKey) + colorCounts.set(colorKey, { color: paint.color, count: (current?.count ?? 0) + 1 }) + counts.set(key, colorCounts) + } + + for (const child of Object.values(value)) collectPaintVariableColorCounts(child, counts) +} + +export function buildFigmaPaintVariableColorMap(graph: SceneGraph): Map { + const counts = new Map>() + for (const node of graph.nodes.values()) { + collectPaintVariableColorCounts(node.figmaRawNodeFields, counts) + collectPaintVariableColorCounts(node.figmaSymbolOverrides, counts) + collectPaintVariableColorCounts(node.figmaComponentPropAssignments, counts) + collectPaintVariableColorCounts(node.figmaDerivedSymbolData, counts) + } + + const colors = new Map() + for (const [key, colorCounts] of counts) { + const [mostCommon] = [...colorCounts.values()].sort((a, b) => b.count - a.count) + colors.set(key, mostCommon.color) + } + return colors +} + function resolveInstanceComponentId(context: SceneNodeToKiwiContext, componentId: string): string { const seen = new Set() let currentId = componentId @@ -196,7 +256,8 @@ function applyInstancePayload( const symbolData: Record = { symbolID } if (node.figmaSymbolOverrides.length > 0) { symbolData.symbolOverrides = materializeFigmaPayload(node.figmaSymbolOverrides, context.blobs, { - includeVariableMaps: true + includeVariableMaps: true, + paintVariableColorMap: context.paintVariableColorMap }) } if (node.figmaUniformScaleFactor != null) { @@ -208,12 +269,13 @@ function applyInstancePayload( nc.componentPropAssignments = materializeFigmaPayload( node.figmaComponentPropAssignments, context.blobs, - { includeVariableMaps: true } + { includeVariableMaps: true, paintVariableColorMap: context.paintVariableColorMap } ) } if (node.figmaDerivedSymbolData.length > 0) { nc.derivedSymbolData = materializeFigmaPayload(node.figmaDerivedSymbolData, context.blobs, { - includeVariableMaps: true + includeVariableMaps: true, + paintVariableColorMap: context.paintVariableColorMap }) } if (node.figmaDerivedSymbolDataLayoutVersion != null) { diff --git a/packages/core/src/kiwi/fig/node-change/serialize.ts b/packages/core/src/kiwi/fig/node-change/serialize.ts index aed699e47..c02239104 100644 --- a/packages/core/src/kiwi/fig/node-change/serialize.ts +++ b/packages/core/src/kiwi/fig/node-change/serialize.ts @@ -496,7 +496,8 @@ export function sceneNodeToKiwi( nodeIdToGuid?: Map, fontDigestMap?: Map, varIdToGuid?: Map, - glyphBlobMap = new Map() + glyphBlobMap = new Map(), + paintVariableColorMap?: Map ): KiwiNodeChange[] { return sceneNodeToKiwiWithContext(node, parentGuid, childIndex, localIdCounter, { graph, @@ -505,6 +506,7 @@ export function sceneNodeToKiwi( fontDigestMap, glyphBlobMap, varIdToGuid, + paintVariableColorMap, fractionalPosition, mapToFigmaType, fillToKiwiPaint,