fix(kiwi): export EXCLUDE booleans as XOR

This commit is contained in:
Danila Poyarkov 2026-06-06 16:37:04 +03:00
parent f6a8bd8580
commit 1ea6631ffd
4 changed files with 31 additions and 7 deletions

View file

@ -1,9 +1,10 @@
import { guidToString } from '@open-pencil/kiwi/fig/guid'
/* eslint-disable max-lines -- kiwi↔scene conversion helpers are tightly coupled */
import { DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from '#core/constants'
import { parseVariantName } from '#core/scene-graph/variant-name'
import { styleToWeight } from '#core/text/fonts'
import { guidToString } from '@open-pencil/kiwi/fig/guid'
import { convertEffects, convertFills, convertStrokes } from './paint'
import { importStyleRuns } from './style-runs'
export { importStyleRuns } from './style-runs'
@ -133,10 +134,11 @@ function mapNodeType(type?: string): NodeType | 'DOCUMENT' | 'VARIABLE' {
function mapBooleanOperation(nc: NodeChange): SceneNode['booleanOperation'] {
if (nc.type !== 'BOOLEAN_OPERATION') return undefined
switch (nc.booleanOperation) {
const operation = nc.booleanOperation as NodeChange['booleanOperation'] | 'EXCLUDE' | undefined
switch (operation) {
case 'SUBTRACT':
case 'INTERSECT':
return nc.booleanOperation
return operation
case 'EXCLUDE':
case 'XOR':
return 'EXCLUDE'

View file

@ -1,11 +1,11 @@
import type { NodeChange, Paint } from '@open-pencil/kiwi/fig/codec'
import { stringToGuid } from '@open-pencil/kiwi/fig/guid'
/* eslint-disable max-lines */
import { bytesToHex } from '#core/bytes/hex'
import type { SceneGraph, SceneNode } from '#core/scene-graph'
import type { Color, GUID, Matrix, Vector } from '#core/types'
import { stringToGuid } from '@open-pencil/kiwi/fig/guid'
import {
applyExportSettingsPluginData,
mergePluginData,
@ -16,6 +16,12 @@ import {
export type KiwiNodeChange = NodeChange & Record<string, unknown>
type KiwiBooleanOperation = NonNullable<NodeChange['booleanOperation']>
function toKiwiBooleanOperation(operation: SceneNode['booleanOperation']): KiwiBooleanOperation {
return operation === 'EXCLUDE' ? 'XOR' : (operation ?? 'UNION')
}
/**
* Build a mapping from assetRef key strings ("key@version" or "key") to
* variable GUIDs. This is used to convert colorVar.assetRef references in raw
@ -704,8 +710,7 @@ export function sceneNodeToKiwiWithContext(
if (node.type === 'COMPONENT_SET') upsertPluginData(node, NODE_TYPE_PLUGIN_KEY, node.type)
if (nc.type === 'CANVAS') nc.pageType = 'DESIGN'
if (node.type === 'BOOLEAN_OPERATION')
nc.booleanOperation =
node.booleanOperation === 'EXCLUDE' ? 'XOR' : (node.booleanOperation ?? 'UNION')
nc.booleanOperation = toKiwiBooleanOperation(node.booleanOperation)
if (strokePaints.length > 0) nc.strokePaints = strokePaints
context.serializeLayoutProps(node, nc)

View file

@ -334,7 +334,7 @@ export interface NodeChange {
frameMaskDisabled?: boolean
resizeToFit?: boolean
// Vector
booleanOperation?: 'UNION' | 'SUBTRACT' | 'INTERSECT' | 'EXCLUDE' | 'XOR'
booleanOperation?: 'UNION' | 'SUBTRACT' | 'INTERSECT' | 'XOR'
vectorData?: unknown
fillGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>

View file

@ -1,5 +1,7 @@
import { describe, expect, test } from 'bun:test'
import { createNodeChangesMessage, encodeMessage, initCodec } from '@open-pencil/kiwi/fig/codec'
import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize'
import { SceneGraph } from '#core/scene-graph'
@ -17,6 +19,21 @@ describe('Figma boolean operation export', () => {
expect(changes[0].booleanOperation).toBe('INTERSECT')
})
test('exports exclude as Kiwi XOR', async () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const node = graph.createNode('BOOLEAN_OPERATION', page.id, {
booleanOperation: 'EXCLUDE'
})
const changes = sceneNodeToKiwi(node, { sessionID: 1, localID: 1 }, 0, { value: 2 }, graph, [])
expect(changes[0].booleanOperation).toBe('XOR')
await initCodec()
expect(() => encodeMessage(createNodeChangesMessage(1, 1, changes))).not.toThrow()
})
test('exports boolean operation children in order', () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]