From 81fa52933260c23d1e1bddd082ca4d4587316111 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 20 May 2026 21:24:58 +0300 Subject: [PATCH] fix(fig): preserve raw Figma override metadata - Preserve raw paint variables, style ids, text line metadata, and variable maps - Keep raw node paint/geometry/effect fields for imported Figma nodes - Extend the Kiwi schema for current Figma override and variable fields --- packages/core/src/kiwi/binary/schema.ts | 65 ++++++++++++++++++- packages/core/src/kiwi/node-change/convert.ts | 35 ++++++++-- .../core/src/kiwi/node-change/export-node.ts | 11 +++- .../core/src/scene-graph/node-defaults.ts | 1 + packages/core/src/scene-graph/types.ts | 1 + 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/packages/core/src/kiwi/binary/schema.ts b/packages/core/src/kiwi/binary/schema.ts index 9e82bdb64..7095d4b21 100644 --- a/packages/core/src/kiwi/binary/schema.ts +++ b/packages/core/src/kiwi/binary/schema.ts @@ -846,7 +846,8 @@ message Paint { Video video = 18; uint originalImageWidth = 19; uint originalImageHeight = 20; - PaintVariableBinding variableBinding = 21; // Not in .fig files. Discovered via WS sniffing 2026-01 + VariableData colorVar = 21; + VariableData opacityVar = 38; } message FontMetaData { @@ -904,6 +905,7 @@ message Glyph { float fontSize = 4; uint firstCharacter = 5; float advance = 6; + float rotation = 7; } message Decoration { @@ -1705,6 +1707,12 @@ message TextLineData { int indentationLevel = 2; Directionality directionality = 3; DirectionalityIntent directionalityIntent = 4; + int downgradeStyleId = 5; + int consistencyStyleId = 6; + int listStartOffset = 7; + bool isFirstLineOfList = 8; + SourceDirectionality sourceDirectionality = 9; + int styleId = 10; } enum BulletType { @@ -1725,6 +1733,12 @@ enum Directionality { RTL = 1; } +enum SourceDirectionality { + AUTO = 0; + LTR = 1; + RTL = 2; +} + enum DirectionalityIntent { IMPLICIT = 0; EXPLICIT = 1; @@ -2053,6 +2067,7 @@ enum VariableDataType { COLOR = 4; SYMBOL_ID = 7; TEXT_DATA = 9; + PROP_REF = 13; } enum VariableResolvedDataType { @@ -2076,6 +2091,7 @@ message StyleId { message VariableID { GUID guid = 1; + AssetRef assetRef = 2; } message SymbolId { @@ -2140,6 +2156,8 @@ enum VariableScope { enum VariableField { MISSING = 0; CORNER_RADIUS = 1; + PARAGRAPH_SPACING = 2; + PARAGRAPH_INDENT = 3; STROKE_WEIGHT = 4; STACK_SPACING = 5; STACK_PADDING_LEFT = 6; @@ -2154,11 +2172,54 @@ enum VariableField { RECTANGLE_TOP_RIGHT_CORNER_RADIUS = 15; RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS = 16; RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS = 17; + BORDER_TOP_WEIGHT = 18; + BORDER_BOTTOM_WEIGHT = 19; + BORDER_LEFT_WEIGHT = 20; + BORDER_RIGHT_WEIGHT = 21; + VARIANT_PROPERTIES = 22; + STACK_COUNTER_SPACING = 23; + MIN_WIDTH = 24; + MAX_WIDTH = 25; + MIN_HEIGHT = 26; + MAX_HEIGHT = 27; + FONT_FAMILY = 28; + FONT_STYLE = 29; + FONT_VARIATIONS = 30; OPACITY = 31; FONT_SIZE = 32; LETTER_SPACING = 34; LINE_HEIGHT = 36; - STACK_COUNTER_SPACING = 23; + OVERRIDDEN_SYMBOL_ID = 37; + HYPERLINK = 38; + CMS_SERIALIZED_RICH_TEXT_DATA = 39; + SLOT_CONTENT_ID = 40; + GRID_ROW_GAP = 41; + GRID_COLUMN_GAP = 42; + X_POSITION = 43; + Y_POSITION = 44; + ROTATION = 45; + MOTION_TRANSLATION_X = 46; + MOTION_TRANSLATION_Y = 47; + MOTION_ROTATION = 48; + MOTION_SCALE_X = 49; + MOTION_SCALE_Y = 50; + MOTION_SHEAR = 51; + SCROLL_OFFSET_X = 52; + SCROLL_OFFSET_Y = 53; + PATH_TRIM_START = 54; + PATH_TRIM_END = 55; + DISSOLVE_PROGRESS = 56; + EASING_DATA = 57; + MEDIA_CURRENT_TIME = 58; + TRANSFORM_3D_PERSPECTIVE = 59; + TRANSFORM_3D_TRANSLATION_Z = 60; + TRANSFORM_3D_ROTATION_X = 61; + TRANSFORM_3D_ROTATION_Y = 62; + TRANSFORM_3D_ROTATION_Z = 63; + POLYGON_COUNT = 64; + ARC_DATA_STARTING_ANGLE = 65; + ARC_DATA_ENDING_ANGLE = 66; + ARC_DATA_INNER_RADIUS = 67; } message VariableDataMapEntry { diff --git a/packages/core/src/kiwi/node-change/convert.ts b/packages/core/src/kiwi/node-change/convert.ts index a64543c6d..ef4a33221 100644 --- a/packages/core/src/kiwi/node-change/convert.ts +++ b/packages/core/src/kiwi/node-change/convert.ts @@ -483,7 +483,7 @@ export function nodeChangeToProps( nodeType, name: nc.name ?? nodeType, figmaGuid: nc.guid ? guidToString(nc.guid) : null, - ...extractFigmaRawGeometry(nc), + ...extractFigmaRawGeometry(nc, blobs), ...extractFigmaSymbolMetadata(nc, blobs), ...convertTransformProps(nc), opacity: nc.opacity ?? 1, @@ -682,6 +682,7 @@ interface PreservedFigmaBlob { } function preserveFigmaPayloadBlobs(value: unknown, blobs: Uint8Array[]): unknown { + if (value instanceof Uint8Array) return value if (Array.isArray(value)) return value.map((item) => preserveFigmaPayloadBlobs(item, blobs)) if (!value || typeof value !== 'object') return value const result: Record = {} @@ -699,12 +700,38 @@ function preserveFigmaPayloadBlobs(value: unknown, blobs: Uint8Array[]): unknown return result } +const FIGMA_RAW_NODE_FIELD_KEYS = [ + 'styleIdForFill', + 'styleIdForStrokeFill', + 'styleIdForText', + 'styleIdForEffect', + 'styleIdForGrid', + 'variableConsumptionMap', + 'parameterConsumptionMap', + 'editInfo', + 'miterLimit', + 'strokeWeight', + 'strokeJoin', + 'fillPaints', + 'strokePaints', + 'effects', + 'fillGeometry', + 'strokeGeometry' +] + function extractFigmaRawGeometry( - nc: NodeChange -): Pick { + nc: NodeChange, + blobs: Uint8Array[] +): Pick { + const figmaRawNodeFields: Record = {} + for (const key of FIGMA_RAW_NODE_FIELD_KEYS) { + const value = (nc as Record)[key] + if (value !== undefined) figmaRawNodeFields[key] = preserveFigmaPayloadBlobs(value, blobs) + } return { figmaRawSize: nc.size ? { ...nc.size } : null, - figmaRawTransform: nc.transform ? { ...nc.transform } : null + figmaRawTransform: nc.transform ? { ...nc.transform } : null, + figmaRawNodeFields } } diff --git a/packages/core/src/kiwi/node-change/export-node.ts b/packages/core/src/kiwi/node-change/export-node.ts index 2b3294348..38f483897 100644 --- a/packages/core/src/kiwi/node-change/export-node.ts +++ b/packages/core/src/kiwi/node-change/export-node.ts @@ -100,6 +100,7 @@ function parseGuidOrNull(value: string) { } function materializeFigmaPayload(value: unknown, blobs: Uint8Array[]): unknown { + if (value instanceof Uint8Array) return value if (Array.isArray(value)) return value.map((item) => materializeFigmaPayload(item, blobs)) if (!value || typeof value !== 'object') return value if ('__openPencilFigmaBlob' in value) { @@ -113,7 +114,6 @@ function materializeFigmaPayload(value: unknown, blobs: Uint8Array[]): unknown { const materialized: Record = {} for (const [key, child] of Object.entries(value)) { - if (key === 'fontMetaData') continue materialized[key] = materializeFigmaPayload(child, blobs) } return materialized @@ -146,6 +146,14 @@ function getOrCreateNodeGuid( return guid } +function applyRawFigmaNodeFields( + context: SceneNodeToKiwiContext, + node: SceneNode, + nc: KiwiNodeChange +): void { + Object.assign(nc, materializeFigmaPayload(node.figmaRawNodeFields, context.blobs)) +} + function applyInstancePayload( context: SceneNodeToKiwiContext, node: SceneNode, @@ -330,6 +338,7 @@ export function sceneNodeToKiwiWithContext( context.serializeLayoutProps(node, nc) context.serializeGeometry(node, nc, context.blobs) context.serializeVariableBindings(node, nc, context.graph, context.varIdToGuid) + applyRawFigmaNodeFields(context, node, nc) const pluginData = mergePluginData(node.pluginData) if (pluginData.length > 0) nc.pluginData = pluginData diff --git a/packages/core/src/scene-graph/node-defaults.ts b/packages/core/src/scene-graph/node-defaults.ts index 8753c646e..bca734bdc 100644 --- a/packages/core/src/scene-graph/node-defaults.ts +++ b/packages/core/src/scene-graph/node-defaults.ts @@ -20,6 +20,7 @@ export function createDefaultNode( rotation: 0, figmaRawSize: null, figmaRawTransform: null, + figmaRawNodeFields: {}, figmaDerivedLayout: null, fills: type === 'TEXT' diff --git a/packages/core/src/scene-graph/types.ts b/packages/core/src/scene-graph/types.ts index 4f95d65ae..19bdff387 100644 --- a/packages/core/src/scene-graph/types.ts +++ b/packages/core/src/scene-graph/types.ts @@ -271,6 +271,7 @@ export interface SceneNode { rotation: number figmaRawSize: Vector | null figmaRawTransform: Matrix | null + figmaRawNodeFields: Record figmaDerivedLayout: Partial | null fills: Fill[]