/* Command buffer opcodes — mirrors ops.h */ const OP_OPEN_ELEMENT = 0x02; const OP_TEXT = 0x03; const OP_CLOSE_ELEMENT = 0x04; const OP_SNAPSHOT = 0x05; /* Property group masks for OPEN_ELEMENT */ const PROP_LAYOUT = 0x01; const PROP_BG_COLOR = 0x02; const PROP_CORNER_RADIUS = 0x04; const PROP_BORDER = 0x08; const PROP_CLIP = 0x10; const PROP_FLOATING = 0x20; const encoder = new TextEncoder(); function packAxis(view: DataView, offset: number, axis: SizingAxis): number { let o = offset; switch (axis.type) { case "fit": view.setUint32(o, 0, true); o += 4; view.setFloat32(o, axis.min ?? 0, true); o += 4; view.setFloat32(o, axis.max ?? 0, true); o += 4; break; case "grow": view.setUint32(o, 1, true); o += 4; view.setFloat32(o, axis.min ?? 0, true); o += 4; view.setFloat32(o, axis.max ?? 0, true); o += 4; break; case "percent": view.setUint32(o, 2, true); o += 4; view.setFloat32(o, axis.value, true); o += 4; view.setFloat32(o, 0, true); o += 4; break; case "fixed": view.setUint32(o, 3, true); o += 4; view.setFloat32(o, axis.value, true); o += 4; view.setFloat32(o, 0, true); o += 4; break; } return o; } function packString( view: DataView, bytes: Uint8Array, o: number, end: number, context: string, ): number { let paddedLength = Math.ceil(bytes.length / 4) * 4; let next = o + 4 + paddedLength; if (next > end) { throw new RangeError( `clayterm transfer buffer capacity exceeded while packing ${context} ` + `(${next} byte offset, ${end} byte limit). ` + `Render a smaller visible slice or reduce frame content.`, ); } view.setUint32(o, bytes.length, true); o += 4; new Uint8Array(view.buffer).set(bytes, o); o += paddedLength; return o; } export function pack( ops: Op[], mem: ArrayBufferLike, offset: number, limit?: number, ): number { let view = new DataView(mem); let end = limit ?? mem.byteLength; let o = offset; for (let op of ops) { switch (op.directive) { case OP_CLOSE_ELEMENT: view.setUint32(o, op.directive, true); o += 4; break; case OP_OPEN_ELEMENT: { view.setUint32(o, OP_OPEN_ELEMENT, true); o += 4; let bytes = encoder.encode(op.id); o = packString(view, bytes, o, end, "element id"); let mask = 0; if (op.layout) mask |= PROP_LAYOUT; if (op.bg !== undefined) mask |= PROP_BG_COLOR; if (op.cornerRadius) mask |= PROP_CORNER_RADIUS; if (op.border) mask |= PROP_BORDER; if (op.clip) mask |= PROP_CLIP; if (op.floating) mask |= PROP_FLOATING; view.setUint32(o, mask, true); o += 4; if (op.layout) { let l = op.layout; o = packAxis(view, o, l.width ?? { type: "fit" }); o = packAxis(view, o, l.height ?? { type: "fit" }); let p = l.padding ?? {}; view.setUint32( o, (p.left ?? 0) | ((p.right ?? 0) << 8) | ((p.top ?? 0) << 16) | ((p.bottom ?? 0) << 24), true, ); o += 4; view.setUint32( o, (l.gap ?? 0) | ((l.direction === "ttb" ? 1 : 0) << 16), true, ); o += 4; view.setUint32(o, (l.alignX ?? 0) | ((l.alignY ?? 0) << 8), true); o += 4; } if (op.bg !== undefined) { view.setUint32(o, op.bg, true); o += 4; } if (op.cornerRadius) { let cr = op.cornerRadius; view.setUint32( o, (cr.tl ?? 0) | ((cr.tr ?? 0) << 8) | ((cr.bl ?? 0) << 16) | ((cr.br ?? 0) << 24), true, ); o += 4; } if (op.border) { let b = op.border; view.setUint32(o, b.color, true); o += 4; view.setUint32( o, (b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) | ((b.bottom ?? 0) << 24), true, ); o += 4; } if (op.clip) { view.setUint32( o, (op.clip.horizontal ? 1 : 0) | ((op.clip.vertical ? 1 : 0) << 8), true, ); o += 4; } if (op.floating) { let f = op.floating; view.setFloat32(o, f.x ?? 0, true); o += 4; view.setFloat32(o, f.y ?? 0, true); o += 4; view.setUint32(o, f.parent ?? 0, true); o += 4; view.setUint32( o, (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | ((f.zIndex ?? 0) << 16), true, ); o += 4; } break; } case OP_SNAPSHOT: { new Uint8Array(mem).set(op.data, o); o += op.data.length; break; } case OP_TEXT: { view.setUint32(o, OP_TEXT, true); o += 4; view.setUint32(o, op.color ?? 0xFFFFFFFF, true); o += 4; view.setUint32( o, (op.fontSize ?? 1) | ((op.fontId ?? 0) << 8) | ((op.wrap ?? 0) << 16) | ((op.attrs ?? 0) << 24), true, ); o += 4; let str = encoder.encode(op.content); o = packString(view, str, o, end, "text content"); break; } } if (o > end) { throw new RangeError( `ops exceed buffer capacity (${o - offset} bytes packed, ${ end - offset } available)`, ); } } return (o - offset) / 4; } export function rgba(r: number, g: number, b: number, a = 255): number { return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); } export type SizingAxis = | { type: "fit"; min?: number; max?: number } | { type: "grow"; min?: number; max?: number } | { type: "percent"; value: number } | { type: "fixed"; value: number }; export const fit = (min = 0, max = 0): SizingAxis => ({ type: "fit", min, max, }); export const grow = (min = 0, max = 0): SizingAxis => ({ type: "grow", min, max, }); export const percent = (value: number): SizingAxis => ({ type: "percent", value, }); export const fixed = (value: number): SizingAxis => ({ type: "fixed", value }); export interface CloseElement { directive: typeof OP_CLOSE_ELEMENT; } export interface OpenElement { directive: typeof OP_OPEN_ELEMENT; id: string; layout?: { width?: SizingAxis; height?: SizingAxis; padding?: { left?: number; right?: number; top?: number; bottom?: number }; gap?: number; direction?: "ltr" | "ttb"; alignX?: number; alignY?: number; }; bg?: number; cornerRadius?: { tl?: number; tr?: number; bl?: number; br?: number }; border?: { color: number; left?: number; right?: number; top?: number; bottom?: number; }; clip?: { horizontal?: boolean; vertical?: boolean }; floating?: { x?: number; y?: number; parent?: number; attachTo?: number; attachPoints?: number; zIndex?: number; }; } export interface Text { directive: typeof OP_TEXT; content: string; color?: number; fontSize?: number; fontId?: number; wrap?: number; attrs?: number; } interface Snapshot { directive: typeof OP_SNAPSHOT; data: Uint8Array; } export type Op = OpenElement | Text | CloseElement | Snapshot; export function open( id: string, props: Omit = {}, ): OpenElement { return { directive: OP_OPEN_ELEMENT, id, ...props }; } export function text( content: string, props: Omit = {}, ): Text { return { directive: OP_TEXT, content, ...props }; } export function close(): CloseElement { return { directive: OP_CLOSE_ELEMENT }; } function packSize(ops: Op[]): number { let n = 0; for (let op of ops) { switch (op.directive) { case OP_CLOSE_ELEMENT: n += 4; break; case OP_SNAPSHOT: n += op.data.length; break; case OP_OPEN_ELEMENT: { n += 4; // opcode n += 4 + Math.ceil(encoder.encode(op.id).length / 4) * 4; // id string n += 4; // mask if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align if (op.bg !== undefined) n += 4; if (op.cornerRadius) n += 4; if (op.border) n += 8; if (op.clip) n += 4; if (op.floating) n += 16; break; } case OP_TEXT: { n += 4 + 4 + 4; // opcode + color + cfg n += 4 + Math.ceil(encoder.encode(op.content).length / 4) * 4; // string break; } } } return n; } export function snapshot(ops: Op[]): Op { let size = packSize(ops); let buf = new ArrayBuffer(size); let words = pack(ops, buf, 0, size); return { directive: OP_SNAPSHOT, data: new Uint8Array(buf, 0, words * 4) }; }