fix(kiwi): preserve edited font variation axes
This commit is contained in:
parent
8c85d2b953
commit
054199ac15
|
|
@ -1,7 +1,7 @@
|
|||
import type { NodeChange } from '#core/kiwi/fig/codec'
|
||||
import type { FontVariation } from '#core/scene-graph'
|
||||
|
||||
function figmaAxisTagToString(axisTag: number): string {
|
||||
export function figmaAxisTagToString(axisTag: number): string {
|
||||
return String.fromCharCode(
|
||||
(axisTag >> 24) & 0xff,
|
||||
(axisTag >> 16) & 0xff,
|
||||
|
|
@ -10,13 +10,25 @@ function figmaAxisTagToString(axisTag: number): string {
|
|||
)
|
||||
}
|
||||
|
||||
export function stringToFigmaAxisTag(axis: string): number | undefined {
|
||||
if (axis.length !== 4) return undefined
|
||||
return (
|
||||
((axis.charCodeAt(0) << 24) |
|
||||
(axis.charCodeAt(1) << 16) |
|
||||
(axis.charCodeAt(2) << 8) |
|
||||
axis.charCodeAt(3)) >>>
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function convertFontVariations(nc: NodeChange): FontVariation[] {
|
||||
const result: FontVariation[] = []
|
||||
for (const variation of nc.fontVariations ?? []) {
|
||||
if (typeof variation.value !== 'number') continue
|
||||
const axis =
|
||||
variation.axisName ||
|
||||
(typeof variation.axisTag === 'number' ? figmaAxisTagToString(variation.axisTag) : '')
|
||||
typeof variation.axisTag === 'number'
|
||||
? figmaAxisTagToString(variation.axisTag)
|
||||
: variation.axisName || ''
|
||||
if (axis) result.push({ axis, value: variation.value })
|
||||
}
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type { Color, GUID, Matrix } from '#core/types'
|
|||
|
||||
import { guidToString, stringToGuid, VARIABLE_BINDING_FIELDS } from './convert'
|
||||
import { sceneNodeToKiwiWithContext, type KiwiNodeChange } from './export-node'
|
||||
import { stringToFigmaAxisTag } from './font-variations'
|
||||
import {
|
||||
BOUND_VARIABLES_PLUGIN_KEY,
|
||||
LAYOUT_DIRECTION_PLUGIN_KEY,
|
||||
|
|
@ -177,7 +178,10 @@ function buildDerivedTextData(
|
|||
}
|
||||
|
||||
function fontVariationToKiwi(variation: SceneNode['fontVariations'][number]) {
|
||||
return { axisName: variation.axis, value: variation.value }
|
||||
const axisTag = stringToFigmaAxisTag(variation.axis)
|
||||
return axisTag === undefined
|
||||
? { axisName: variation.axis, value: variation.value }
|
||||
: { axisTag, axisName: variation.axis, value: variation.value }
|
||||
}
|
||||
|
||||
function exportTextData(node: SceneNode): NodeChange['textData'] {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const RAW_NODE_FIELD_KEYS = new Set([
|
|||
'letterSpacing',
|
||||
'maxLines',
|
||||
'styleRuns',
|
||||
'fontVariations',
|
||||
'textTruncation',
|
||||
'layoutMode',
|
||||
'itemSpacing',
|
||||
|
|
|
|||
|
|
@ -16,4 +16,23 @@ describe('Figma boolean operation export', () => {
|
|||
expect(changes[0].type).toBe('BOOLEAN_OPERATION')
|
||||
expect(changes[0].booleanOperation).toBe('INTERSECT')
|
||||
})
|
||||
|
||||
test('exports boolean operation children in order', () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
const node = graph.createNode('BOOLEAN_OPERATION', page.id, {
|
||||
booleanOperation: 'SUBTRACT'
|
||||
})
|
||||
graph.createNode('RECTANGLE', node.id, { name: 'Left operand' })
|
||||
graph.createNode('ELLIPSE', node.id, { name: 'Right operand' })
|
||||
|
||||
const changes = sceneNodeToKiwi(node, { sessionID: 1, localID: 1 }, 0, { value: 2 }, graph, [])
|
||||
|
||||
expect(changes.map((change) => change.type)).toEqual([
|
||||
'BOOLEAN_OPERATION',
|
||||
'RECTANGLE',
|
||||
'ELLIPSE'
|
||||
])
|
||||
expect(changes.map((change) => change.parentIndex?.position)).toEqual(['!', '!', '"'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ describe('Figma font variation export', () => {
|
|||
const changes = sceneNodeToKiwi(text, { sessionID: 1, localID: 1 }, 0, { value: 2 }, graph, [])
|
||||
const nodeChange = changes[0]
|
||||
|
||||
expect(nodeChange.fontVariations).toEqual([{ axisName: 'wght', value: 650 }])
|
||||
expect(nodeChange.fontVariations).toEqual([
|
||||
{ axisTag: 0x77676874, axisName: 'wght', value: 650 }
|
||||
])
|
||||
expect(nodeChange.textData?.styleOverrideTable?.[0]?.fontVariations).toEqual([
|
||||
{ axisName: 'wdth', value: 88 }
|
||||
{ axisTag: 0x77647468, axisName: 'wdth', value: 88 }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import type { NodeChange } from '#core/kiwi/fig/codec'
|
||||
import { importNodeChanges } from '#core/kiwi/fig/import'
|
||||
import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
|
||||
|
||||
describe('Figma boolean operation import', () => {
|
||||
|
|
@ -18,6 +19,56 @@ describe('Figma boolean operation import', () => {
|
|||
expect(props.booleanOperation).toBe('SUBTRACT')
|
||||
})
|
||||
|
||||
test('imports boolean operation nodes with children', () => {
|
||||
const graph = importNodeChanges([
|
||||
{
|
||||
guid: { sessionID: 0, localID: 0 },
|
||||
type: 'DOCUMENT',
|
||||
name: 'Document',
|
||||
phase: 'CREATED'
|
||||
},
|
||||
{
|
||||
guid: { sessionID: 0, localID: 1 },
|
||||
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
||||
type: 'CANVAS',
|
||||
name: 'Page',
|
||||
phase: 'CREATED'
|
||||
},
|
||||
{
|
||||
guid: { sessionID: 2, localID: 1 },
|
||||
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
||||
type: 'BOOLEAN_OPERATION',
|
||||
name: 'Imported boolean',
|
||||
booleanOperation: 'INTERSECT',
|
||||
phase: 'CREATED',
|
||||
size: { x: 120, y: 80 }
|
||||
},
|
||||
{
|
||||
guid: { sessionID: 2, localID: 2 },
|
||||
parentIndex: { guid: { sessionID: 2, localID: 1 }, position: '!' },
|
||||
type: 'RECTANGLE',
|
||||
name: 'Left operand',
|
||||
phase: 'CREATED',
|
||||
size: { x: 80, y: 80 }
|
||||
},
|
||||
{
|
||||
guid: { sessionID: 2, localID: 3 },
|
||||
parentIndex: { guid: { sessionID: 2, localID: 1 }, position: '"' },
|
||||
type: 'ELLIPSE',
|
||||
name: 'Right operand',
|
||||
phase: 'CREATED',
|
||||
size: { x: 80, y: 80 }
|
||||
}
|
||||
] as NodeChange[])
|
||||
|
||||
const booleanNode = graph.getChildren(graph.getPages()[0].id)[0]
|
||||
const children = graph.getChildren(booleanNode.id)
|
||||
|
||||
expect(booleanNode.type).toBe('BOOLEAN_OPERATION')
|
||||
expect(booleanNode.booleanOperation).toBe('INTERSECT')
|
||||
expect(children.map((child) => child.type)).toEqual(['RECTANGLE', 'ELLIPSE'])
|
||||
})
|
||||
|
||||
test('defaults missing boolean operations to union', () => {
|
||||
const props = nodeChangeToProps(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('Figma font variation import', () => {
|
|||
type: 'TEXT',
|
||||
textData: { characters: 'Axis' },
|
||||
fontVariations: [
|
||||
{ axisTag: 0x77676874, value: 650 },
|
||||
{ axisTag: 0x77676874, axisName: 'Weight', value: 650 },
|
||||
{ axisName: 'wdth', value: 88 }
|
||||
]
|
||||
} as NodeChange,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { beforeAll, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { exportFigFile, initCodec, parseFigFile, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { parseFigBuffer } from '@open-pencil/core/kiwi/fig/parse/core'
|
||||
import { guidToString } from '@open-pencil/core/kiwi/fig/node-change/convert'
|
||||
import { parseFigBuffer } from '@open-pencil/core/kiwi/fig/parse/core'
|
||||
|
||||
function decodeExport(bytes: Uint8Array) {
|
||||
return parseFigBuffer(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength))
|
||||
|
|
@ -126,6 +125,31 @@ describe('fig roundtrip source metadata', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('clears raw font variation payloads when normalized axes are edited', async () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
const text = graph.createNode('TEXT', page.id, {
|
||||
name: 'Variable font text',
|
||||
text: 'Axis',
|
||||
fontVariations: [{ axis: 'wght', value: 400 }]
|
||||
})
|
||||
text.source.format = 'fig'
|
||||
text.source.id = '4:501'
|
||||
text.source.fig.rawNodeFields.fontVariations = [{ axisName: 'wght', value: 900 }]
|
||||
|
||||
graph.updateNode(text.id, { fontVariations: [{ axis: 'wght', value: 650 }] })
|
||||
|
||||
const decoded = decodeExport(await exportFigFile(graph))
|
||||
const exported = decoded.nodeChanges.find(
|
||||
(nodeChange) => nodeChange.guid && guidToString(nodeChange.guid) === '4:501'
|
||||
)
|
||||
|
||||
expect(text.source.fig.rawNodeFields.fontVariations).toBeUndefined()
|
||||
expect(exported?.fontVariations).toEqual([
|
||||
{ axisTag: 0x77676874, axisName: 'wght', value: 650 }
|
||||
])
|
||||
})
|
||||
|
||||
test('exports imported raw vector payloads without regenerating vector data', async () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
|
|
|
|||
Loading…
Reference in a new issue