fix(kiwi): preserve more Figma metadata

This commit is contained in:
Danila Poyarkov 2026-05-26 11:57:13 +03:00
parent e31234a0ed
commit 4cd187c766
7 changed files with 76 additions and 9 deletions

View file

@ -293,12 +293,18 @@ export interface NodeChange {
cornerRadius?: number
fillPaints?: Paint[]
strokePaints?: Paint[]
backgroundPaints?: Paint[]
strokeWeight?: number
strokeAlign?: string
strokeCap?: string
strokeJoin?: string
dashPattern?: number[]
effects?: Effect[]
mask?: boolean
maskType?: string
maskIsOutline?: boolean
exportSettings?: unknown[]
layoutGrids?: unknown[]
// Layout
stackMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL'
stackSpacing?: number
@ -380,6 +386,12 @@ export interface NodeChange {
textBidiVersion?: number
textDecoration?: string
textDecorationSkipInk?: boolean
textDecorationFillPaints?: Paint[]
textUnderlineOffset?: { value?: number; units?: string }
textDecorationThickness?: { value?: number; units?: string }
textDecorationStyle?: string
toggledOnOTFeatures?: string[]
toggledOffOTFeatures?: string[]
fontVariations?: Array<{ axisTag?: number; axisName?: string; value?: number }>
fontVariantCommonLigatures?: boolean
fontVariantContextualLigatures?: boolean
@ -404,6 +416,10 @@ export interface NodeChange {
// Constraints
horizontalConstraint?: string
verticalConstraint?: string
// Prototype
prototypeStartNodeID?: GUID
prototypeInteractions?: unknown[]
transitionInfo?: unknown
// Variables
variableData?: VariableDataEntry
variableConsumptionMap?: { entries?: VariableConsumptionEntry[] }

View file

@ -760,6 +760,9 @@ export const FIGMA_RAW_NODE_FIELD_KEYS = [
'styleIdForText',
'styleIdForEffect',
'styleIdForGrid',
'backgroundPaints',
'layoutGrids',
'exportSettings',
'componentPropDefs',
'componentPropRefs',
'variantPropSpecs',
@ -784,6 +787,9 @@ export const FIGMA_RAW_NODE_FIELD_KEYS = [
'handoffStatusMap',
'annotationCategories',
'miterLimit',
'mask',
'maskType',
'maskIsOutline',
'strokeWeight',
'strokeJoin',
'borderStrokeWeightsIndependent',
@ -815,7 +821,12 @@ export const FIGMA_RAW_NODE_FIELD_KEYS = [
'fontVariations',
'fontVariantCommonLigatures',
'fontVariantContextualLigatures',
'toggledOnOTFeatures',
'toggledOffOTFeatures',
'leadingTrim',
'textDecorationFillPaints',
'textUnderlineOffset',
'textDecorationThickness',
'textDecorationStyle',
'semanticWeight',
'semanticItalic',
@ -826,6 +837,9 @@ export const FIGMA_RAW_NODE_FIELD_KEYS = [
'strokePaints',
'effects',
'sectionStatusInfo',
'prototypeStartNodeID',
'prototypeInteractions',
'transitionInfo',
'codeSyntax',
'lockMode',
'slideThemeMap',

View file

@ -64,6 +64,8 @@ const RAW_NODE_FIELD_KEYS = new Set([
'vectorNetwork',
'fillGeometry',
'strokeGeometry',
'isMask',
'maskType',
'clipsContent'
])

View file

@ -195,9 +195,9 @@ OpenPencil deliberately preserves many Figma/Kiwi fields even when they are not
| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. |
| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. |
| Page fields: background, page type, guides | ↩ | ◐ | — | Background/page type/guides mostly round-trip. Guides are not rendered/editable. |
| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, and semantic font metadata are preserved for round-trip when safe. |
| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe. |
| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. |
| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details. |
| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. |
## Highest-priority visual gaps

View file

@ -11,6 +11,7 @@ const RAW_FIELD_COVERAGE = {
'borderStrokeWeightsIndependent',
'borderTopWeight',
'derivedTextData',
'backgroundPaints',
'effects',
'fillGeometry',
'fillPaints',
@ -19,26 +20,36 @@ const RAW_FIELD_COVERAGE = {
'fontVariantCommonLigatures',
'fontVariantContextualLigatures',
'fontVariations',
'layoutGrids',
'leadingTrim',
'letterSpacing',
'lineHeight',
'mask',
'maskIsOutline',
'maskType',
'maxLines',
'miterLimit',
'semanticItalic',
'semanticWeight',
'strokeGeometry',
'textDecorationFillPaints',
'strokeJoin',
'strokePaints',
'strokeWeight',
'textAutoResize',
'textData',
'textDecorationStyle',
'textUnderlineOffset',
'textDecorationThickness',
'textPathStart',
'textTracking',
'toggledOffOTFeatures',
'toggledOnOTFeatures',
'vectorData'
],
uiEditable: [
'componentPropDefs',
'exportSettings',
'fontSize',
'letterSpacing',
'lineHeight',
@ -88,6 +99,8 @@ const RAW_FIELD_COVERAGE = {
'minSize',
'pageType',
'parameterConsumptionMap',
'prototypeInteractions',
'prototypeStartNodeID',
'scatterStrokeSettings',
'sectionStatusInfo',
'slideThemeMap',
@ -102,6 +115,7 @@ const RAW_FIELD_COVERAGE = {
'textExplicitLayoutVersion',
'textUserLayoutVersion',
'targetAspectRatio',
'transitionInfo',
'userFacingVersion',
'variableConsumptionMap',
'variableModeBySetMap',

View file

@ -373,17 +373,17 @@ describe('Figma Kiwi schema coverage', () => {
expect(
Object.fromEntries([...buckets].map(([bucket, items]) => [bucket, items.length]))
).toEqual({
modeled: 89,
modeled: 104,
schemaTag: 60,
internalBookkeeping: 18,
rawPreserved: 53,
internalBookkeeping: 17,
rawPreserved: 52,
styleLibraryMetadata: 39,
componentInstanceMetadata: 42,
textMetadata: 28,
textMetadata: 23,
slideFigjamMetadata: 39,
visualGeometryMetadata: 42,
layoutMetadata: 30,
prototypeConnectorMetadata: 35,
visualGeometryMetadata: 38,
layoutMetadata: 29,
prototypeConnectorMetadata: 32,
variableDevMetadata: 14,
widgetMetadata: 11,
codeCmsAiMetadata: 67,
@ -405,6 +405,15 @@ describe('Figma Kiwi schema coverage', () => {
expect(covered('textDecorationStyle')).toBe(true)
expect(covered('semanticWeight')).toBe(true)
expect(covered('semanticItalic')).toBe(true)
expect(covered('toggledOnOTFeatures')).toBe(true)
expect(covered('toggledOffOTFeatures')).toBe(true)
expect(covered('textDecorationFillPaints')).toBe(true)
expect(covered('textUnderlineOffset')).toBe(true)
expect(covered('textDecorationThickness')).toBe(true)
expect(covered('mask')).toBe(true)
expect(covered('maskType')).toBe(true)
expect(covered('maskIsOutline')).toBe(true)
expect(covered('layoutGrids')).toBe(true)
expect(covered('gridChildVerticalAlign')).toBe(true)
expect(covered('gridChildHorizontalAlign')).toBe(true)
expect(covered('slideThemeMap')).toBe(true)

View file

@ -137,6 +137,13 @@ describe('fig roundtrip source metadata', () => {
text.source.id = '4:502'
text.source.fig.rawNodeFields.leadingTrim = 'CAP_HEIGHT'
text.source.fig.rawNodeFields.textDecorationStyle = 'WAVY'
text.source.fig.rawNodeFields.textDecorationFillPaints = [
{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1 }
]
text.source.fig.rawNodeFields.textUnderlineOffset = { value: 2, units: 'PIXELS' }
text.source.fig.rawNodeFields.textDecorationThickness = { value: 1.5, units: 'PIXELS' }
text.source.fig.rawNodeFields.toggledOnOTFeatures = ['DLIG']
text.source.fig.rawNodeFields.toggledOffOTFeatures = ['LIGA']
text.source.fig.rawNodeFields.semanticWeight = 'BOLD'
text.source.fig.rawNodeFields.semanticItalic = 'ITALIC'
text.source.fig.rawNodeFields.derivedTextData = {
@ -151,6 +158,11 @@ describe('fig roundtrip source metadata', () => {
expect(exported?.leadingTrim).toBe('CAP_HEIGHT')
expect(exported?.textDecorationStyle).toBe('WAVY')
expect(exported?.textDecorationFillPaints?.[0]?.type).toBe('SOLID')
expect(exported?.textUnderlineOffset).toEqual({ value: 2, units: 'PIXELS' })
expect(exported?.textDecorationThickness).toEqual({ value: 1.5, units: 'PIXELS' })
expect(exported?.toggledOnOTFeatures).toEqual(['DLIG'])
expect(exported?.toggledOffOTFeatures).toEqual(['LIGA'])
expect(exported?.semanticWeight).toBe('BOLD')
expect(exported?.semanticItalic).toBe('ITALIC')
expect(exported?.derivedTextData?.layoutSize).toEqual({ x: 80, y: 20 })