Extract named types, deduplicate field maps, fix internal canvas GUID
- 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
This commit is contained in:
parent
d9fbc6fb92
commit
5a3e68eade
|
|
@ -82,12 +82,15 @@ export async function exportFigFile(
|
|||
const blobs: Uint8Array[] = []
|
||||
const pages = graph.getPages(true)
|
||||
const nodeIdToGuid = new Map<string, { sessionID: number; localID: number }>()
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>
|
||||
|
|
@ -161,28 +161,6 @@ function exportTextData(node: SceneNode): NodeChange['textData'] {
|
|||
}
|
||||
}
|
||||
|
||||
const BOUND_VARIABLE_FIELD_MAP: Record<string, string> = {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string>()
|
||||
|
||||
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<string, VariableValue> = {}
|
||||
const dataValues = nc.variableDataValues as { entries?: Array<{ modeID?: { sessionID: number; localID: number }; variableData?: { value?: Record<string, unknown>; 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<string, string> = {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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<string, string> = Object.fromEntries(
|
||||
Object.entries(VARIABLE_BINDING_FIELDS).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
const convertColor = normalizeColor
|
||||
|
||||
function imageHashToString(hash: Record<string, number>): string {
|
||||
|
|
|
|||
Loading…
Reference in a new issue