fix(fig): preserve imported geometry and prop values

- Keep raw Figma size and transform values for imported nodes during export
- Add current Kiwi schema fields for component prop varValue and symbol id variable values
- Preserve component source geometry used by Figma to reconstruct instances
This commit is contained in:
Danila Poyarkov 2026-05-20 20:56:13 +03:00
parent 1c0122b485
commit f4ad80d789
6 changed files with 67 additions and 26 deletions

View file

@ -227,6 +227,7 @@ export interface VariableAnyValue {
floatValue?: number
colorValue?: Color
alias?: { guid?: GUID; assetRef?: { key: string; version?: string } }
symbolIdValue?: { guid?: GUID }
}
export interface VariableDataEntry {

View file

@ -1580,6 +1580,7 @@ enum ComponentPropNodeField {
message ComponentPropAssignment {
GUID defID = 1;
ComponentPropValue value = 2;
VariableData varValue = 3;
}
message ComponentPropDef {
@ -2044,6 +2045,8 @@ enum VariableDataType {
STRING = 2;
ALIAS = 3;
COLOR = 4;
SYMBOL_ID = 7;
TEXT_DATA = 9;
}
enum VariableResolvedDataType {
@ -2051,12 +2054,18 @@ enum VariableResolvedDataType {
FLOAT = 1;
STRING = 2;
COLOR = 4;
SYMBOL_ID = 6;
TEXT_DATA = 8;
}
message VariableID {
GUID guid = 1;
}
message SymbolId {
GUID guid = 1;
}
message VariableSetID {
GUID guid = 1;
}
@ -2067,6 +2076,7 @@ message VariableAnyValue {
float floatValue = 3;
VariableID alias = 4;
Color colorValue = 5;
SymbolId symbolIdValue = 8;
}
message VariableData {

View file

@ -483,6 +483,7 @@ export function nodeChangeToProps(
nodeType,
name: nc.name ?? nodeType,
figmaGuid: nc.guid ? guidToString(nc.guid) : null,
...extractFigmaRawGeometry(nc),
...extractFigmaSymbolMetadata(nc, blobs),
...convertTransformProps(nc),
opacity: nc.opacity ?? 1,
@ -698,6 +699,15 @@ function preserveFigmaPayloadBlobs(value: unknown, blobs: Uint8Array[]): unknown
return result
}
function extractFigmaRawGeometry(
nc: NodeChange
): Pick<SceneNode, 'figmaRawSize' | 'figmaRawTransform'> {
return {
figmaRawSize: nc.size ? { ...nc.size } : null,
figmaRawTransform: nc.transform ? { ...nc.transform } : null
}
}
function extractFigmaSymbolMetadata(
nc: NodeChange,
blobs: Uint8Array[]

View file

@ -1,6 +1,6 @@
import type { NodeChange, Paint } from '#core/kiwi/binary/codec'
import type { SceneGraph, SceneNode } from '#core/scene-graph'
import type { Color, GUID, Matrix } from '#core/types'
import type { Color, GUID, Matrix, Vector } from '#core/types'
import { stringToGuid } from './guid'
import {
@ -148,6 +148,36 @@ function getOrCreateNodeGuid(
return guid
}
function applyInstancePayload(
context: SceneNodeToKiwiContext,
node: SceneNode,
nc: KiwiNodeChange,
localIdCounter: { value: number }
): void {
if (node.type !== 'INSTANCE' || !node.componentId) return
const symbolID = getOrCreateNodeGuid(
context,
resolveInstanceComponentId(context, node.componentId),
localIdCounter
)
if (symbolID) {
const symbolData: Record<string, unknown> = { symbolID }
if (node.figmaSymbolOverrides.length > 0) {
symbolData.symbolOverrides = materializeFigmaPayload(node.figmaSymbolOverrides, context.blobs)
}
if (node.figmaUniformScaleFactor != null) {
symbolData.uniformScaleFactor = node.figmaUniformScaleFactor
}
nc.symbolData = symbolData as KiwiNodeChange['symbolData']
}
if (node.figmaDerivedSymbolData.length > 0) {
nc.derivedSymbolData = materializeFigmaPayload(node.figmaDerivedSymbolData, context.blobs)
}
if (node.figmaDerivedSymbolDataLayoutVersion != null) {
nc.derivedSymbolDataLayoutVersion = node.figmaDerivedSymbolDataLayoutVersion
}
}
function applyComponentMetadata(node: SceneNode, nc: KiwiNodeChange): void {
if (node.componentKey) nc.componentKey = node.componentKey
if (node.sourceLibraryKey) nc.sourceLibraryKey = node.sourceLibraryKey
@ -185,6 +215,14 @@ function applyComponentMetadata(node: SceneNode, nc: KiwiNodeChange): void {
if (variantPropSpecs.length > 0) nc.variantPropSpecs = variantPropSpecs
}
function exportNodeSize(node: SceneNode): Vector {
return node.figmaRawSize ? { ...node.figmaRawSize } : { x: node.width, y: node.height }
}
function exportNodeTransform(context: SceneNodeToKiwiContext, node: SceneNode): Matrix {
return node.figmaRawTransform ? { ...node.figmaRawTransform } : context.computeExportTransform(node)
}
function applyNodeVisualProps(
context: SceneNodeToKiwiContext,
node: SceneNode,
@ -273,37 +311,15 @@ export function sceneNodeToKiwiWithContext(
visible: node.visible,
opacity: node.opacity,
phase: 'CREATED',
size: { x: node.width, y: node.height },
transform: context.computeExportTransform(node),
size: exportNodeSize(node),
transform: exportNodeTransform(context, node),
strokeWeight: node.strokes[0]?.weight ?? DEFAULT_STROKE_WEIGHT,
strokeAlign: node.strokes[0]?.align ?? 'INSIDE'
}
applyNodeVisualProps(context, node, nc)
applyComponentMetadata(node, nc)
if (node.type === 'INSTANCE' && node.componentId) {
const symbolID = getOrCreateNodeGuid(
context,
resolveInstanceComponentId(context, node.componentId),
localIdCounter
)
if (symbolID) {
const symbolData: Record<string, unknown> = { symbolID }
if (node.figmaSymbolOverrides.length > 0) {
symbolData.symbolOverrides = materializeFigmaPayload(node.figmaSymbolOverrides, context.blobs)
}
if (node.figmaUniformScaleFactor != null) {
symbolData.uniformScaleFactor = node.figmaUniformScaleFactor
}
nc.symbolData = symbolData as KiwiNodeChange['symbolData']
}
if (node.figmaDerivedSymbolData.length > 0) {
nc.derivedSymbolData = materializeFigmaPayload(node.figmaDerivedSymbolData, context.blobs)
}
if (node.figmaDerivedSymbolDataLayoutVersion != null) {
nc.derivedSymbolDataLayoutVersion = node.figmaDerivedSymbolDataLayoutVersion
}
}
applyInstancePayload(context, node, nc, localIdCounter)
if (node.type === 'COMPONENT_SET') upsertPluginData(node, NODE_TYPE_PLUGIN_KEY, node.type)
if (strokePaints.length > 0) nc.strokePaints = strokePaints

View file

@ -18,6 +18,8 @@ export function createDefaultNode(
width: 100,
height: 100,
rotation: 0,
figmaRawSize: null,
figmaRawTransform: null,
figmaDerivedLayout: null,
fills:
type === 'TEXT'

View file

@ -269,6 +269,8 @@ export interface SceneNode {
width: number
height: number
rotation: number
figmaRawSize: Vector | null
figmaRawTransform: Matrix | null
figmaDerivedLayout: Partial<Rect> | null
fills: Fill[]