openpencil/packages/core/src/kiwi/binary/variable-bindings.ts
2026-05-17 13:02:00 +03:00

135 lines
3.6 KiB
TypeScript

import { hexToBytes } from '#core/bytes/hex'
import type { GUID } from '#core/types'
import type { NodeChange, Paint } from './codec'
export interface VariableBindingCodec {
encodePaint(paint: Paint): Uint8Array
encodeNodeChange(nodeChange: NodeChange): Uint8Array
}
export function encodeVarint(value: number): number[] {
const bytes: number[] = []
while (value > 0x7f) {
bytes.push((value & 0x7f) | 0x80)
value >>>= 7
}
bytes.push(value)
return bytes
}
export function encodePaintWithVariableBinding(
codec: VariableBindingCodec,
paint: Paint,
variableSessionID: number,
variableLocalID: number
): Uint8Array {
const { colorVariableBinding: _, ...basePaint } = paint
const baseBytes = codec.encodePaint(basePaint)
const baseArray = Array.from(baseBytes)
if (baseArray[baseArray.length - 1] === 0) {
baseArray.pop()
}
baseArray.push(0x15, 0x01)
baseArray.push(0x04, 0x01)
baseArray.push(...encodeVarint(variableSessionID))
baseArray.push(...encodeVarint(variableLocalID))
baseArray.push(0x00, 0x00, 0x02, 0x03, 0x03, 0x04)
baseArray.push(0x00, 0x00)
return new Uint8Array(baseArray)
}
export function parseVariableId(variableId: string): GUID | null {
const match = variableId.match(/VariableID:(\d+):(\d+)/)
if (!match) return null
return {
sessionID: parseInt(match[1] ?? '0', 10),
localID: parseInt(match[2] ?? '0', 10)
}
}
export function encodeNodeChangeWithVariables(
codec: VariableBindingCodec,
nodeChange: NodeChange
): Uint8Array {
const hasFillBinding = nodeChange.fillPaints?.some((p) => p.colorVariableBinding)
const hasStrokeBinding = nodeChange.strokePaints?.some((p) => p.colorVariableBinding)
if (!hasFillBinding && !hasStrokeBinding) {
return codec.encodeNodeChange(nodeChange)
}
const cleanNodeChange = { ...nodeChange }
if (cleanNodeChange.fillPaints) {
cleanNodeChange.fillPaints = cleanNodeChange.fillPaints.map(
({ colorVariableBinding: _, ...rest }) => rest
)
}
if (cleanNodeChange.strokePaints) {
cleanNodeChange.strokePaints = cleanNodeChange.strokePaints.map(
({ colorVariableBinding: _, ...rest }) => rest
)
}
const baseBytes = codec.encodeNodeChange(cleanNodeChange)
let hex = Buffer.from(baseBytes).toString('hex')
const fillBinding = nodeChange.fillPaints?.[0]?.colorVariableBinding
if (hasFillBinding && fillBinding) {
hex = injectVariableBinding(hex, '2601', fillBinding)
}
const strokeBinding = nodeChange.strokePaints?.[0]?.colorVariableBinding
if (hasStrokeBinding && strokeBinding) {
hex = injectVariableBinding(hex, '2701', strokeBinding)
}
return hexToBytes(hex)
}
function injectVariableBinding(hex: string, marker: string, binding: { variableID: GUID }): string {
const markerIdx = hex.indexOf(marker)
if (markerIdx === -1) return hex
const visiblePattern = '0401'
const patternIdx = hex.indexOf(visiblePattern, markerIdx)
if (patternIdx === -1) return hex
let insertPoint = patternIdx + visiblePattern.length
if (hex.slice(insertPoint, insertPoint + 4) === '0501') {
insertPoint += 4
}
const varBytes = [
0x15,
0x01,
0x04,
0x01,
...encodeVarint(binding.variableID.sessionID),
...encodeVarint(binding.variableID.localID),
0x00,
0x00,
0x02,
0x03,
0x03,
0x04,
0x00,
0x00
]
const varHex = Buffer.from(varBytes).toString('hex')
const beforeVar = hex.slice(0, insertPoint)
let afterIdx = insertPoint
if (hex.slice(afterIdx, afterIdx + 2) === '00') {
afterIdx += 2
}
const afterVar = hex.slice(afterIdx)
return beforeVar + varHex + afterVar
}