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 }