From 5a3e68eadea9bf8a20c51d0c141c18daec3ebbe4 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 8 Mar 2026 22:22:26 +0300 Subject: [PATCH] Extract named types, deduplicate field maps, fix internal canvas GUID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define VariableAnyValue, VariableDataEntry, VariableConsumptionEntry, VariableDataValuesEntry in codec.ts — remove inline type monsters - Extract VARIABLE_BINDING_FIELDS / VARIABLE_BINDING_FIELDS_INVERSE to kiwi-convert.ts — single source of truth for export and import - Track internal canvas GUID during page loop instead of recalculating with incorrect idx+2 formula (broke when scene nodes existed) - Remove unused modeGuidToId map, unnecessary as-casts --- packages/core/src/fig-export.ts | 12 ++-- packages/core/src/kiwi-serialize.ts | 30 ++-------- packages/core/src/kiwi/codec.ts | 31 +++++++++- packages/core/src/kiwi/fig-import.ts | 83 +++++++++----------------- packages/core/src/kiwi/kiwi-convert.ts | 26 ++++++++ 5 files changed, 90 insertions(+), 92 deletions(-) diff --git a/packages/core/src/fig-export.ts b/packages/core/src/fig-export.ts index 2527f1a65..6b505c256 100644 --- a/packages/core/src/fig-export.ts +++ b/packages/core/src/fig-export.ts @@ -82,12 +82,15 @@ export async function exportFigFile( const blobs: Uint8Array[] = [] const pages = graph.getPages(true) const nodeIdToGuid = new Map() + let internalCanvasGuid: { sessionID: number; localID: number } | null = null for (let p = 0; p < pages.length; p++) { const page = pages[p] const canvasLocalID = localIdCounter.value++ const canvasGuid = { sessionID: 0, localID: canvasLocalID } + if (page.internalOnly) internalCanvasGuid = canvasGuid + const canvasNc: KiwiNodeChange = { guid: canvasGuid, parentIndex: { guid: docGuid, position: fractionalPosition(p) }, @@ -116,14 +119,7 @@ export async function exportFigFile( } if (graph.variableCollections.size > 0) { - const hasInternalPage = pages.some((p) => p.internalOnly) - let internalCanvasGuid: { sessionID: number; localID: number } - - if (hasInternalPage) { - const internalPage = pages.find((p) => p.internalOnly)! - const idx = pages.indexOf(internalPage) - internalCanvasGuid = { sessionID: 0, localID: idx + 2 } - } else { + if (!internalCanvasGuid) { const internalLocalID = localIdCounter.value++ internalCanvasGuid = { sessionID: 0, localID: internalLocalID } nodeChanges.push({ diff --git a/packages/core/src/kiwi-serialize.ts b/packages/core/src/kiwi-serialize.ts index 75e93028a..73ca0bec3 100644 --- a/packages/core/src/kiwi-serialize.ts +++ b/packages/core/src/kiwi-serialize.ts @@ -4,9 +4,9 @@ import { deflateSync, inflateSync } from 'fflate' import { weightToStyle } from './fonts' import { encodeVectorNetworkBlob } from './vector' -import { stringToGuid } from './kiwi/kiwi-convert' +import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi/kiwi-convert' -import type { NodeChange, Paint } from './kiwi/codec' +import type { NodeChange, Paint, VariableConsumptionEntry } from './kiwi/codec' import type { SceneGraph, SceneNode, CharacterStyleOverride } from './scene-graph' type KiwiNodeChange = NodeChange & Record @@ -161,28 +161,6 @@ function exportTextData(node: SceneNode): NodeChange['textData'] { } } -const BOUND_VARIABLE_FIELD_MAP: Record = { - cornerRadius: 'CORNER_RADIUS', - topLeftRadius: 'RECTANGLE_TOP_LEFT_CORNER_RADIUS', - topRightRadius: 'RECTANGLE_TOP_RIGHT_CORNER_RADIUS', - bottomLeftRadius: 'RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS', - bottomRightRadius: 'RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS', - strokeWeight: 'STROKE_WEIGHT', - itemSpacing: 'STACK_SPACING', - paddingLeft: 'STACK_PADDING_LEFT', - paddingTop: 'STACK_PADDING_TOP', - paddingRight: 'STACK_PADDING_RIGHT', - paddingBottom: 'STACK_PADDING_BOTTOM', - counterAxisSpacing: 'STACK_COUNTER_SPACING', - visible: 'VISIBLE', - opacity: 'OPACITY', - width: 'WIDTH', - height: 'HEIGHT', - fontSize: 'FONT_SIZE', - letterSpacing: 'LETTER_SPACING', - lineHeight: 'LINE_HEIGHT' -} - export function sceneNodeToKiwi( node: SceneNode, parentGuid: { sessionID: number; localID: number }, @@ -347,9 +325,9 @@ export function sceneNodeToKiwi( } if (Object.keys(node.boundVariables).length > 0) { - const entries: Array<{ variableData: { value: { alias: { guid: { sessionID: number; localID: number } } }; dataType: string; resolvedDataType: string }; variableField: string }> = [] + const entries: VariableConsumptionEntry[] = [] for (const [field, varId] of Object.entries(node.boundVariables)) { - const kiwiField = BOUND_VARIABLE_FIELD_MAP[field] + const kiwiField = VARIABLE_BINDING_FIELDS[field] if (!kiwiField) continue const variable = graph.variables.get(varId) if (!variable) continue diff --git a/packages/core/src/kiwi/codec.ts b/packages/core/src/kiwi/codec.ts index 647d74d30..55a9c8c90 100644 --- a/packages/core/src/kiwi/codec.ts +++ b/packages/core/src/kiwi/codec.ts @@ -209,6 +209,31 @@ export interface Effect { blendMode?: string } +export interface VariableAnyValue { + boolValue?: boolean + textValue?: string + floatValue?: number + colorValue?: Color + alias?: { guid: GUID } +} + +export interface VariableDataEntry { + value?: VariableAnyValue + dataType?: string + resolvedDataType?: string +} + +export interface VariableConsumptionEntry { + nodeField?: number + variableData?: VariableDataEntry + variableField?: string +} + +export interface VariableDataValuesEntry { + modeID: GUID + variableData: VariableDataEntry +} + export interface NodeChange { [key: string]: unknown guid: GUID @@ -290,12 +315,12 @@ export interface NodeChange { horizontalConstraint?: string verticalConstraint?: string // Variables - variableData?: { value?: { boolValue?: boolean; textValue?: string; floatValue?: number; colorValue?: { r: number; g: number; b: number; a: number }; alias?: { guid: GUID } }; dataType?: string; resolvedDataType?: string } - variableConsumptionMap?: { entries?: Array<{ nodeField?: number; variableData?: { value?: { alias?: { guid: GUID }; colorValue?: { r: number; g: number; b: number; a: number }; boolValue?: boolean; textValue?: string; floatValue?: number }; dataType?: string; resolvedDataType?: string }; variableField?: string }> } + variableData?: VariableDataEntry + variableConsumptionMap?: { entries?: VariableConsumptionEntry[] } variableSetModes?: Array<{ id: GUID; name: string; sortPosition?: string }> variableSetID?: { guid: GUID } variableResolvedType?: string - variableDataValues?: { entries?: Array<{ modeID: GUID; variableData: { value?: { boolValue?: boolean; textValue?: string; floatValue?: number; colorValue?: { r: number; g: number; b: number; a: number }; alias?: { guid: GUID } }; dataType?: string; resolvedDataType?: string } }> } + variableDataValues?: { entries?: VariableDataValuesEntry[] } variableScopes?: string[] } diff --git a/packages/core/src/kiwi/fig-import.ts b/packages/core/src/kiwi/fig-import.ts index 9f9133cfa..5055f4891 100644 --- a/packages/core/src/kiwi/fig-import.ts +++ b/packages/core/src/kiwi/fig-import.ts @@ -1,7 +1,12 @@ import type { VariableType, VariableValue } from '../scene-graph' import { SceneGraph } from '../scene-graph' -import { guidToString, nodeChangeToProps, sortChildren } from './kiwi-convert' +import { + guidToString, + nodeChangeToProps, + sortChildren, + VARIABLE_BINDING_FIELDS_INVERSE +} from './kiwi-convert' import { populateAndApplyOverrides } from './instance-overrides' import type { InstanceNodeChange } from './instance-overrides' @@ -78,18 +83,13 @@ export function importNodeChanges( } function importVariables() { - const modeGuidToId = new Map() - for (const [id, nc] of changeMap) { if (nc.type !== 'VARIABLE_SET') continue - const modes = (nc.variableSetModes ?? []).map( - (m: { id?: { sessionID: number; localID: number }; name?: string }) => { - const modeId = m.id ? guidToString(m.id) : 'default' - if (m.id) modeGuidToId.set(modeId, modeId) - return { modeId, name: m.name ?? 'Mode' } - } - ) + const modes = (nc.variableSetModes ?? []).map((m) => { + const modeId = m.id ? guidToString(m.id) : 'default' + return { modeId, name: m.name ?? 'Mode' } + }) if (modes.length === 0) modes.push({ modeId: 'default', name: 'Default' }) graph.addCollection({ @@ -104,8 +104,7 @@ export function importNodeChanges( for (const [id, nc] of changeMap) { if (nc.type !== 'VARIABLE') continue - const setIdObj = nc.variableSetID as { guid?: { sessionID: number; localID: number } } | undefined - const collectionId = setIdObj?.guid ? guidToString(setIdObj.guid) : (parentMap.get(id) ?? '') + const collectionId = nc.variableSetID?.guid ? guidToString(nc.variableSetID.guid) : (parentMap.get(id) ?? '') if (!graph.variableCollections.has(collectionId)) { const parentNc = changeMap.get(collectionId) @@ -118,34 +117,32 @@ export function importNodeChanges( }) } - const resolvedType = nc.variableResolvedType as string | undefined let type: VariableType = 'FLOAT' + const resolvedType = nc.variableResolvedType if (resolvedType === 'COLOR') type = 'COLOR' else if (resolvedType === 'BOOLEAN') type = 'BOOLEAN' else if (resolvedType === 'STRING') type = 'STRING' const valuesByMode: Record = {} - const dataValues = nc.variableDataValues as { entries?: Array<{ modeID?: { sessionID: number; localID: number }; variableData?: { value?: Record; dataType?: string; resolvedDataType?: string } }> } | undefined - if (dataValues?.entries) { - for (const entry of dataValues.entries) { - const modeId = entry.modeID ? guidToString(entry.modeID) : 'default' + if (nc.variableDataValues?.entries) { + for (const entry of nc.variableDataValues.entries) { + const modeId = guidToString(entry.modeID) const vd = entry.variableData - if (!vd?.value) continue + if (!vd.value) continue const dt = vd.dataType ?? vd.resolvedDataType if (dt === 'COLOR' && vd.value.colorValue) { - const c = vd.value.colorValue as { r: number; g: number; b: number; a: number } + const c = vd.value.colorValue valuesByMode[modeId] = { r: c.r, g: c.g, b: c.b, a: c.a } } else if (dt === 'BOOLEAN') { - valuesByMode[modeId] = (vd.value.boolValue as boolean) ?? false + valuesByMode[modeId] = vd.value.boolValue ?? false } else if (dt === 'STRING') { - valuesByMode[modeId] = (vd.value.textValue as string) ?? '' - } else if (dt === 'ALIAS' && vd.value.alias) { - const alias = vd.value.alias as { guid?: { sessionID: number; localID: number } } - if (alias.guid) valuesByMode[modeId] = { aliasId: guidToString(alias.guid) } + valuesByMode[modeId] = vd.value.textValue ?? '' + } else if (dt === 'ALIAS' && vd.value.alias?.guid) { + valuesByMode[modeId] = { aliasId: guidToString(vd.value.alias.guid) } } else { - valuesByMode[modeId] = (vd.value.floatValue as number) ?? 0 + valuesByMode[modeId] = vd.value.floatValue ?? 0 } } } @@ -169,41 +166,17 @@ export function importNodeChanges( } function importVariableBindings() { - const fieldMap: Record = { - CORNER_RADIUS: 'cornerRadius', - RECTANGLE_TOP_LEFT_CORNER_RADIUS: 'topLeftRadius', - RECTANGLE_TOP_RIGHT_CORNER_RADIUS: 'topRightRadius', - RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS: 'bottomLeftRadius', - RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS: 'bottomRightRadius', - STROKE_WEIGHT: 'strokeWeight', - STACK_SPACING: 'itemSpacing', - STACK_PADDING_LEFT: 'paddingLeft', - STACK_PADDING_TOP: 'paddingTop', - STACK_PADDING_RIGHT: 'paddingRight', - STACK_PADDING_BOTTOM: 'paddingBottom', - STACK_COUNTER_SPACING: 'counterAxisSpacing', - VISIBLE: 'visible', - OPACITY: 'opacity', - WIDTH: 'width', - HEIGHT: 'height', - FONT_SIZE: 'fontSize', - LETTER_SPACING: 'letterSpacing', - LINE_HEIGHT: 'lineHeight' - } - for (const [ncId, nc] of changeMap) { - const consumption = nc.variableConsumptionMap as { entries?: Array<{ variableData?: { value?: { alias?: { guid?: { sessionID: number; localID: number } } } }; variableField?: string }> } | undefined - if (!consumption?.entries?.length) continue + if (!nc.variableConsumptionMap?.entries?.length) continue const nodeId = guidToNodeId.get(ncId) if (!nodeId) continue - for (const entry of consumption.entries) { - const alias = entry.variableData?.value?.alias - if (!alias?.guid) continue - const variableId = guidToString(alias.guid) - const field = fieldMap[entry.variableField ?? ''] - if (field) graph.bindVariable(nodeId, field, variableId) + for (const entry of nc.variableConsumptionMap.entries) { + const varGuid = entry.variableData?.value?.alias?.guid + if (!varGuid) continue + const field = VARIABLE_BINDING_FIELDS_INVERSE[entry.variableField ?? ''] + if (field) graph.bindVariable(nodeId, field, guidToString(varGuid)) } } } diff --git a/packages/core/src/kiwi/kiwi-convert.ts b/packages/core/src/kiwi/kiwi-convert.ts index f50a68941..bb312c9c8 100644 --- a/packages/core/src/kiwi/kiwi-convert.ts +++ b/packages/core/src/kiwi/kiwi-convert.ts @@ -44,6 +44,32 @@ export function stringToGuid(str: string): GUID { return { sessionID: parseInt(session, 10), localID: parseInt(local, 10) } } +export const VARIABLE_BINDING_FIELDS: Record = { + cornerRadius: 'CORNER_RADIUS', + topLeftRadius: 'RECTANGLE_TOP_LEFT_CORNER_RADIUS', + topRightRadius: 'RECTANGLE_TOP_RIGHT_CORNER_RADIUS', + bottomLeftRadius: 'RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS', + bottomRightRadius: 'RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS', + strokeWeight: 'STROKE_WEIGHT', + itemSpacing: 'STACK_SPACING', + paddingLeft: 'STACK_PADDING_LEFT', + paddingTop: 'STACK_PADDING_TOP', + paddingRight: 'STACK_PADDING_RIGHT', + paddingBottom: 'STACK_PADDING_BOTTOM', + counterAxisSpacing: 'STACK_COUNTER_SPACING', + visible: 'VISIBLE', + opacity: 'OPACITY', + width: 'WIDTH', + height: 'HEIGHT', + fontSize: 'FONT_SIZE', + letterSpacing: 'LETTER_SPACING', + lineHeight: 'LINE_HEIGHT' +} + +export const VARIABLE_BINDING_FIELDS_INVERSE: Record = Object.fromEntries( + Object.entries(VARIABLE_BINDING_FIELDS).map(([k, v]) => [v, k]) +) + const convertColor = normalizeColor function imageHashToString(hash: Record): string {