diff --git a/packages/core/src/kiwi/fig/codec/index.ts b/packages/core/src/kiwi/fig/codec/index.ts index f74707de8..c89027d56 100644 --- a/packages/core/src/kiwi/fig/codec/index.ts +++ b/packages/core/src/kiwi/fig/codec/index.ts @@ -8,13 +8,19 @@ import { decompress as zstdDecompress } from 'fzstd' -import { figmaSchema, isZstdCompressed, getKiwiMessageType } from '@open-pencil/kiwi/fig' +import { + figmaSchema, + isZstdCompressed, + getKiwiMessageType, + encodeVarint, + encodePaintWithVariableBinding as encodePaintVariableBinding, + encodeNodeChangeWithVariables as encodeNodeChangeVariableBindings, + parseVariableId +} from '@open-pencil/kiwi/fig' import { compileSchema, encodeBinarySchema } from '@open-pencil/kiwi/schema-runtime' import { parseColor } from '#core/color' -import * as VariableBindings from './variable-bindings' - interface CompiledSchema { encodeMessage(message: unknown): Uint8Array decodeMessage(data: Uint8Array): unknown @@ -123,7 +129,7 @@ export function encodeMessage(message: FigmaMessage): Uint8Array { // Build nodeChanges array with our encoded nodes const ncBytes: number[] = [0x04] // field 4 - ncBytes.push(...VariableBindings.encodeVarint(nodeChangeBytes.length)) // array length + ncBytes.push(...encodeVarint(nodeChangeBytes.length)) // array length for (const ncArr of nodeChangeBytes) { ncBytes.push(...Array.from(ncArr)) } @@ -557,19 +563,14 @@ export function encodePaintWithVariableBinding( if (!compiledSchema) { throw new Error('Codec not initialized. Call initCodec() first.') } - return VariableBindings.encodePaintWithVariableBinding( - compiledSchema, - paint, - variableSessionID, - variableLocalID - ) + return encodePaintVariableBinding(compiledSchema, paint, variableSessionID, variableLocalID) } -export { parseVariableId } from './variable-bindings' +export { parseVariableId } export function encodeNodeChangeWithVariables(nodeChange: NodeChange): Uint8Array { if (!compiledSchema) { throw new Error('Codec not initialized. Call initCodec() first.') } - return VariableBindings.encodeNodeChangeWithVariables(compiledSchema, nodeChange) + return encodeNodeChangeVariableBindings(compiledSchema, nodeChange) } diff --git a/packages/kiwi/src/fig/index.ts b/packages/kiwi/src/fig/index.ts index 432bc292f..ed0e2e673 100644 --- a/packages/kiwi/src/fig/index.ts +++ b/packages/kiwi/src/fig/index.ts @@ -1,2 +1,3 @@ export { default as figmaSchema } from './schema' export * from './protocol' +export * from './variable-bindings' diff --git a/packages/core/src/kiwi/fig/codec/variable-bindings.ts b/packages/kiwi/src/fig/variable-bindings.ts similarity index 67% rename from packages/core/src/kiwi/fig/codec/variable-bindings.ts rename to packages/kiwi/src/fig/variable-bindings.ts index 08e6e3e93..d856db4d0 100644 --- a/packages/core/src/kiwi/fig/codec/variable-bindings.ts +++ b/packages/kiwi/src/fig/variable-bindings.ts @@ -1,9 +1,22 @@ -import { hexToBytes } from '#core/bytes/hex' -import type { GUID } from '#core/types' +export interface GUID { + sessionID: number + localID: number +} -import type { NodeChange, Paint } from './index' +export interface VariableBinding { + variableID: GUID +} -export interface VariableBindingCodec { +export interface PaintWithVariableBinding { + colorVariableBinding?: VariableBinding +} + +export interface NodeChangeWithVariableBindings { + fillPaints?: PaintWithVariableBinding[] + strokePaints?: PaintWithVariableBinding[] +} + +export interface VariableBindingCodec { encodePaint(paint: Paint): Uint8Array encodeNodeChange(nodeChange: NodeChange): Uint8Array } @@ -18,8 +31,8 @@ export function encodeVarint(value: number): number[] { return bytes } -export function encodePaintWithVariableBinding( - codec: VariableBindingCodec, +export function encodePaintWithVariableBinding( + codec: VariableBindingCodec, unknown>, paint: Paint, variableSessionID: number, variableLocalID: number @@ -52,12 +65,18 @@ export function parseVariableId(variableId: string): GUID | null { } } -export function encodeNodeChangeWithVariables( - codec: VariableBindingCodec, +export function encodeNodeChangeWithVariables( + codec: VariableBindingCodec< + unknown, + Omit & { + fillPaints?: Omit[] + strokePaints?: Omit[] + } + >, nodeChange: NodeChange ): Uint8Array { - const hasFillBinding = nodeChange.fillPaints?.some((p) => p.colorVariableBinding) - const hasStrokeBinding = nodeChange.strokePaints?.some((p) => p.colorVariableBinding) + const hasFillBinding = nodeChange.fillPaints?.some((paint) => paint.colorVariableBinding) + const hasStrokeBinding = nodeChange.strokePaints?.some((paint) => paint.colorVariableBinding) if (!hasFillBinding && !hasStrokeBinding) { return codec.encodeNodeChange(nodeChange) @@ -91,7 +110,7 @@ export function encodeNodeChangeWithVariables( return hexToBytes(hex) } -function injectVariableBinding(hex: string, marker: string, binding: { variableID: GUID }): string { +function injectVariableBinding(hex: string, marker: string, binding: VariableBinding): string { const markerIdx = hex.indexOf(marker) if (markerIdx === -1) return hex @@ -132,3 +151,13 @@ function injectVariableBinding(hex: string, marker: string, binding: { variableI return beforeVar + varHex + afterVar } + +function hexToBytes(hex: string): Uint8Array { + if (hex.length % 2 !== 0) throw new Error('Hex string must have an even length') + + const bytes = new Uint8Array(hex.length / 2) + for (let index = 0; index < bytes.length; index++) { + bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16) + } + return bytes +} diff --git a/packages/kiwi/tests/variable-bindings.test.ts b/packages/kiwi/tests/variable-bindings.test.ts new file mode 100644 index 000000000..04cdf8a93 --- /dev/null +++ b/packages/kiwi/tests/variable-bindings.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from 'bun:test' + +import { + encodeNodeChangeWithVariables, + encodePaintWithVariableBinding, + encodeVarint, + parseVariableId +} from '../src/fig/variable-bindings' + +describe('variable binding codec helpers', () => { + test('encodes varints', () => { + expect(encodeVarint(0)).toEqual([0]) + expect(encodeVarint(127)).toEqual([127]) + expect(encodeVarint(128)).toEqual([128, 1]) + expect(encodeVarint(16_384)).toEqual([128, 128, 1]) + }) + + test('parses Figma variable IDs', () => { + expect(parseVariableId('VariableID:123:456')).toEqual({ sessionID: 123, localID: 456 }) + expect(parseVariableId('not-a-variable')).toBeNull() + }) + + test('appends paint variable binding bytes', () => { + const codec = { + encodePaint: () => new Uint8Array([0x01, 0x02, 0x00]), + encodeNodeChange: () => new Uint8Array() + } + + const encoded = encodePaintWithVariableBinding( + codec, + { colorVariableBinding: { variableID: { sessionID: 5, localID: 130 } } }, + 5, + 130 + ) + + expect([...encoded]).toEqual([ + 0x01, 0x02, 0x15, 0x01, 0x04, 0x01, 0x05, 0x82, 0x01, 0x00, 0x00, 0x02, 0x03, 0x03, 0x04, + 0x00, 0x00 + ]) + }) + + test('injects node change variable bindings after paint markers', () => { + const codec = { + encodePaint: () => new Uint8Array(), + encodeNodeChange: () => new Uint8Array([0x26, 0x01, 0x04, 0x01, 0x00, 0xaa]) + } + + const encoded = encodeNodeChangeWithVariables(codec, { + fillPaints: [{ colorVariableBinding: { variableID: { sessionID: 2, localID: 3 } } }] + }) + + expect(Buffer.from(encoded).toString('hex')).toBe('260104011501040102030000020303040000aa') + }) +})