|
|
|
|
@ -2,9 +2,6 @@ |
|
|
|
|
// library.test.ts — bun test suite for MonoDisplay library
|
|
|
|
|
//
|
|
|
|
|
// run: bun test
|
|
|
|
|
//
|
|
|
|
|
// Tests cover BinaryReader primitives and MonoDisplayParser with the real
|
|
|
|
|
// binary format: magic 0xAF7E2B63, section-based layout.
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
import { test, expect, describe } from "bun:test"; |
|
|
|
|
@ -13,12 +10,12 @@ import { |
|
|
|
|
MonoDisplayParser, |
|
|
|
|
MonoDisplayFile, |
|
|
|
|
ElementType, |
|
|
|
|
SectionType, |
|
|
|
|
buildBinBuffer, |
|
|
|
|
type StaticFrame, |
|
|
|
|
type Animation, |
|
|
|
|
type TextDisplay, |
|
|
|
|
type ScrollText, |
|
|
|
|
} from "../src/library"; |
|
|
|
|
type Image2DElement, |
|
|
|
|
type MonoFormatElementsAlways, |
|
|
|
|
type MonoFormatImage2D, |
|
|
|
|
} from "../src/index"; |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Low-level buffer helpers
|
|
|
|
|
@ -28,7 +25,9 @@ import { |
|
|
|
|
const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]); |
|
|
|
|
|
|
|
|
|
function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer { |
|
|
|
|
const arrays = parts.map(p => p instanceof ArrayBuffer ? new Uint8Array(p) : p); |
|
|
|
|
const arrays = parts.map((p: Uint8Array | ArrayBuffer) => |
|
|
|
|
p instanceof ArrayBuffer ? new Uint8Array(p) : p |
|
|
|
|
); |
|
|
|
|
const total = arrays.reduce((s, a) => s + a.byteLength, 0); |
|
|
|
|
const out = new Uint8Array(total); |
|
|
|
|
let off = 0; |
|
|
|
|
@ -41,25 +40,6 @@ function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer). |
|
|
|
|
function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); } |
|
|
|
|
function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; } |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Build a well-formed file buffer with a single section. |
|
|
|
|
* sectionType: 0x01=static 0x02=animation 0x03=text 0x04=scrolltext |
|
|
|
|
* sectionData: raw section payload (after section header) |
|
|
|
|
*/ |
|
|
|
|
function makeFile(sectionType: number, sectionData: Uint8Array): ArrayBuffer { |
|
|
|
|
const fileHeader = concat( |
|
|
|
|
MAGIC_BYTES, // magic
|
|
|
|
|
u32le(1), // version = 1
|
|
|
|
|
u16le(1), // numSections = 1
|
|
|
|
|
u16le(0), // reserved = 0
|
|
|
|
|
); |
|
|
|
|
const sectionHeader = concat( |
|
|
|
|
u8(sectionType), |
|
|
|
|
u24le(sectionData.byteLength), |
|
|
|
|
); |
|
|
|
|
return concat(fileHeader, sectionHeader, sectionData); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// BinaryReader
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
@ -159,273 +139,227 @@ describe("MonoDisplayParser — header", () => { |
|
|
|
|
const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0)); |
|
|
|
|
expect(() => parser.parse(buf)).toThrow(/version/i); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("rejects 0 sections", () => { |
|
|
|
|
const buf = concat(MAGIC_BYTES, u32le(1), u16le(0), u16le(0)); // 0 sections
|
|
|
|
|
expect(() => parser.parse(buf)).toThrow(/0 section/i); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("skips unknown section type, throws when no recognised section follows", () => { |
|
|
|
|
// Section type 0xFF is unknown
|
|
|
|
|
const unknownData = new Uint8Array(4); |
|
|
|
|
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(1), u16le(0)); |
|
|
|
|
const secHeader = concat(u8(0xFF), u24le(unknownData.byteLength)); |
|
|
|
|
const buf = concat(fileHeader, secHeader, unknownData); |
|
|
|
|
expect(() => parser.parse(buf)).toThrow(/no recognised/i); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayParser — static section
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayParser — static", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("parses 2×2 static frame", () => { |
|
|
|
|
const data = concat(u16le(2), u16le(2), new Uint8Array([1, 0, 0, 1])); |
|
|
|
|
const buf = makeFile(0x01, new Uint8Array(data)); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as StaticFrame; |
|
|
|
|
expect(result.type).toBe("static"); |
|
|
|
|
expect(result.width).toBe(2); |
|
|
|
|
expect(result.height).toBe(2); |
|
|
|
|
expect(result.pixels[0]).toBe(1); |
|
|
|
|
expect(result.pixels[3]).toBe(1); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayParser — animation section
|
|
|
|
|
// MonoDisplayParser — Image2D round-trip
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayParser — animation", () => { |
|
|
|
|
describe("MonoDisplayParser — Image2D round-trip", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("parses 2-frame animation", () => { |
|
|
|
|
const pixels = new Uint8Array(4); // 2×2
|
|
|
|
|
const data = concat( |
|
|
|
|
u16le(2), u16le(2), // width, height
|
|
|
|
|
u16le(2), // frameCount
|
|
|
|
|
u32le(100), pixels, // frame 1: 100ms, all-off
|
|
|
|
|
u32le(200), new Uint8Array([1,1,1,1]), // frame 2: 200ms, all-on
|
|
|
|
|
); |
|
|
|
|
const buf = makeFile(0x02, new Uint8Array(data)); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as Animation; |
|
|
|
|
expect(result.type).toBe("animation"); |
|
|
|
|
expect(result.frames.length).toBe(2); |
|
|
|
|
expect(result.frames[0].durationMs).toBe(100); |
|
|
|
|
expect(result.frames[1].durationMs).toBe(200); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("empty animation (0 frames) is valid", () => { |
|
|
|
|
const data = concat(u16le(4), u16le(4), u16le(0)); |
|
|
|
|
const buf = makeFile(0x02, new Uint8Array(data)); |
|
|
|
|
const result = parser.parse(buf) as Animation; |
|
|
|
|
expect(result.frames.length).toBe(0); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayParser — text section
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayParser — text", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("parses text with align=center", () => { |
|
|
|
|
const enc = new TextEncoder().encode("HELLO"); |
|
|
|
|
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(1)); // fontId=0 align=1
|
|
|
|
|
const buf = makeFile(0x03, new Uint8Array(data)); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as TextDisplay; |
|
|
|
|
expect(result.type).toBe("text"); |
|
|
|
|
expect(result.text).toBe("HELLO"); |
|
|
|
|
expect(result.align).toBe(1); |
|
|
|
|
expect(result.fontId).toBe(0); |
|
|
|
|
}); |
|
|
|
|
/** |
|
|
|
|
* Build a minimal valid file buffer containing one ElementsAlways section |
|
|
|
|
* with one Image2D element. |
|
|
|
|
* |
|
|
|
|
* File header (12 bytes): |
|
|
|
|
* magic u32le + version u32le + numSections u16le + reserved u16le |
|
|
|
|
* |
|
|
|
|
* Section header (4 bytes): |
|
|
|
|
* type u8 + size u24le (INCLUDES 4-byte header) |
|
|
|
|
* |
|
|
|
|
* ElementsAlways section data: |
|
|
|
|
* flags u16le + numElements u16le |
|
|
|
|
* |
|
|
|
|
* Image2D element (12 fixed bytes + packed pixel data): |
|
|
|
|
* type u16le + xOffset u16le + yOffset u16le + width u16le + height u16le + reserved u16le |
|
|
|
|
* + packed 1bpp pixel data + padding to 4-byte boundary |
|
|
|
|
*/ |
|
|
|
|
function makeImage2DFile( |
|
|
|
|
width: number, |
|
|
|
|
height: number, |
|
|
|
|
pixels: Uint8Array, |
|
|
|
|
xOffset: number = 0, |
|
|
|
|
yOffset: number = 0, |
|
|
|
|
): ArrayBuffer { |
|
|
|
|
// Pack pixels to 1bpp
|
|
|
|
|
const total = width * height; |
|
|
|
|
const packedLen = (total + 7) >> 3; |
|
|
|
|
const packed = new Uint8Array(packedLen); |
|
|
|
|
for (let i = 0; i < total; i++) { |
|
|
|
|
if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7)); |
|
|
|
|
} |
|
|
|
|
// Padding to 4-byte boundary for element (12 fixed bytes + packed data)
|
|
|
|
|
const elementDataLen = 12 + packedLen; |
|
|
|
|
const padLen = (4 - (elementDataLen & 3)) & 3; |
|
|
|
|
const padding = new Uint8Array(padLen); |
|
|
|
|
|
|
|
|
|
// Image2D element bytes
|
|
|
|
|
const element = new Uint8Array(concat( |
|
|
|
|
u16le(1), // type = Image2D
|
|
|
|
|
u16le(xOffset), |
|
|
|
|
u16le(yOffset), |
|
|
|
|
u16le(width), |
|
|
|
|
u16le(height), |
|
|
|
|
u16le(0), // reserved
|
|
|
|
|
packed, |
|
|
|
|
padding, |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// Section data: flags + numElements + element
|
|
|
|
|
const sectionData = new Uint8Array(concat( |
|
|
|
|
u16le(0), // flags = 0
|
|
|
|
|
u16le(1), // numElements = 1
|
|
|
|
|
element, |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// Section header: type u8 + size u24le (4 header bytes + data)
|
|
|
|
|
const sectionSize = 4 + sectionData.byteLength; |
|
|
|
|
const sectionHeader = new Uint8Array(concat( |
|
|
|
|
u8(1), // SectionType.ElementsAlways = 1
|
|
|
|
|
u24le(sectionSize), |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// File header
|
|
|
|
|
const fileHeader = new Uint8Array(concat( |
|
|
|
|
MAGIC_BYTES, |
|
|
|
|
u32le(1), // version = 1
|
|
|
|
|
u16le(1), // numSections = 1
|
|
|
|
|
u16le(0), // reserved
|
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
test("handles multi-byte UTF-8 text", () => { |
|
|
|
|
const str = "日本語"; |
|
|
|
|
const enc = new TextEncoder().encode(str); |
|
|
|
|
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(0)); |
|
|
|
|
const buf = makeFile(0x03, new Uint8Array(data)); |
|
|
|
|
return concat(fileHeader, sectionHeader, sectionData); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as TextDisplay; |
|
|
|
|
expect(result.text).toBe(str); |
|
|
|
|
test("sections[0].sectionType === SectionType.ElementsAlways", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 0, 0, 1]); |
|
|
|
|
const buf = makeImage2DFile(2, 2, pixels); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
expect(result.sections.length).toBe(1); |
|
|
|
|
expect(result.sections[0]!.sectionType).toBe(SectionType.ElementsAlways); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayParser — scrolltext section
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayParser — scrolltext", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("parses scrolltext left-direction", () => { |
|
|
|
|
const enc = new TextEncoder().encode("SCROLL ME"); |
|
|
|
|
const data = concat(u16le(enc.byteLength), enc, u16le(60), u8(0)); |
|
|
|
|
const buf = makeFile(0x04, new Uint8Array(data)); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as ScrollText; |
|
|
|
|
expect(result.type).toBe("scrolltext"); |
|
|
|
|
expect(result.text).toBe("SCROLL ME"); |
|
|
|
|
expect(result.speedPps).toBe(60); |
|
|
|
|
expect(result.direction).toBe(0); |
|
|
|
|
test("elements[0].type === ElementType.Image2D", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 0, 0, 1]); |
|
|
|
|
const buf = makeImage2DFile(2, 2, pixels); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
expect(section.elements.length).toBe(1); |
|
|
|
|
expect(section.elements[0]!.type).toBe(ElementType.Image2D); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("handles multi-codepoint text incl. emoji", () => { |
|
|
|
|
const str = "TICKER 🚀 日本語"; |
|
|
|
|
const enc = new TextEncoder().encode(str); |
|
|
|
|
const data = concat(u16le(enc.byteLength), enc, u16le(80), u8(1)); |
|
|
|
|
const buf = makeFile(0x04, new Uint8Array(data)); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as ScrollText; |
|
|
|
|
expect(result.text).toBe(str); |
|
|
|
|
expect(result.direction).toBe(1); |
|
|
|
|
test("correct dimensions after parse", () => { |
|
|
|
|
const pixels = new Uint8Array([0, 1, 1, 0]); |
|
|
|
|
const buf = makeImage2DFile(2, 2, pixels); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const el = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(el.image.width).toBe(2); |
|
|
|
|
expect(el.image.height).toBe(2); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("correct pixel data after round-trip", () => { |
|
|
|
|
// pixels: [1, 0, 0, 1] → top-left and bottom-right are on
|
|
|
|
|
const pixels = new Uint8Array([1, 0, 0, 1]); |
|
|
|
|
const buf = makeImage2DFile(2, 2, pixels); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const el = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(Array.from(el.image.pixels)).toEqual([1, 0, 0, 1]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("xOffset and yOffset are preserved", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 1, 1, 1]); |
|
|
|
|
const buf = makeImage2DFile(2, 2, pixels, 5, 3); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const el = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(el.xOffset).toBe(5); |
|
|
|
|
expect(el.yOffset).toBe(3); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayParser — unknown section skip
|
|
|
|
|
// MonoDisplayFile
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayParser — section skip", () => { |
|
|
|
|
describe("MonoDisplayFile", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("skips unknown section, parses second known section", () => { |
|
|
|
|
// File with 2 sections: 0xFF (unknown) then 0x01 (static 2×2)
|
|
|
|
|
const staticData = new Uint8Array(concat(u16le(2), u16le(2), new Uint8Array([0,1,1,0]))); |
|
|
|
|
const unknownData = new Uint8Array(4).fill(0xAA); |
|
|
|
|
|
|
|
|
|
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(2), u16le(0)); |
|
|
|
|
const sec1 = concat(u8(0xFF), u24le(unknownData.byteLength), unknownData); |
|
|
|
|
const sec2 = concat(u8(0x01), u24le(staticData.byteLength), staticData); |
|
|
|
|
const buf = concat(fileHeader, sec1, sec2); |
|
|
|
|
|
|
|
|
|
const result = parser.parse(buf) as StaticFrame; |
|
|
|
|
expect(result.type).toBe("static"); |
|
|
|
|
expect(result.pixels[1]).toBe(1); |
|
|
|
|
test("toBuffer() produces parseable output for Image2D element", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 0, 1, 0]); |
|
|
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 }; |
|
|
|
|
const file = new MonoDisplayFile({ elements_always: [el] }); |
|
|
|
|
const buf = file.toBuffer(); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
expect(result.sections.length).toBeGreaterThan(0); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
expect(section.sectionType).toBe(SectionType.ElementsAlways); |
|
|
|
|
expect(section.elements.length).toBe(1); |
|
|
|
|
const parsed = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(parsed.type).toBe(ElementType.Image2D); |
|
|
|
|
expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 1, 0]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("xOffset/yOffset are round-tripped", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 1, 1, 1]); |
|
|
|
|
const el: Image2DElement = { |
|
|
|
|
type: ElementType.Image2D, |
|
|
|
|
pixels, |
|
|
|
|
width: 2, |
|
|
|
|
height: 2, |
|
|
|
|
xOffset: 10, |
|
|
|
|
yOffset: 7, |
|
|
|
|
}; |
|
|
|
|
const file = new MonoDisplayFile({ elements_always: [el] }); |
|
|
|
|
const buf = file.toBuffer(); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const parsed = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(parsed.xOffset).toBe(10); |
|
|
|
|
expect(parsed.yOffset).toBe(7); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("flags.drawFront default is applied", () => { |
|
|
|
|
const pixels = new Uint8Array([0]); |
|
|
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 1, height: 1 }; |
|
|
|
|
const file = new MonoDisplayFile({ elements_always: [el] }); |
|
|
|
|
const buf = file.toBuffer(); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
// Default flags should have drawFront = true (flags byte bit 0 = 1)
|
|
|
|
|
expect(section.flags.drawFront).toBe(true); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("empty elements_always array is valid and produces parseable output", () => { |
|
|
|
|
const file = new MonoDisplayFile({ elements_always: [] }); |
|
|
|
|
const buf = file.toBuffer(); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
expect(result.sections.length).toBeGreaterThan(0); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
expect(section.sectionType).toBe(SectionType.ElementsAlways); |
|
|
|
|
expect(section.elements.length).toBe(0); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// buildBinBuffer round-trip
|
|
|
|
|
// buildBinBuffer
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("buildBinBuffer", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("static round-trip", () => { |
|
|
|
|
test("builds a parseable Uint8Array from Image2DElement", () => { |
|
|
|
|
const pixels = new Uint8Array([0, 1, 1, 0]); |
|
|
|
|
const original: StaticFrame = { type: "static", width: 2, height: 2, pixels }; |
|
|
|
|
const buf = buildBinBuffer(original); |
|
|
|
|
const parsed = parser.parse(buf) as StaticFrame; |
|
|
|
|
expect(parsed.type).toBe("static"); |
|
|
|
|
expect(parsed.width).toBe(2); |
|
|
|
|
expect(parsed.height).toBe(2); |
|
|
|
|
expect(Array.from(parsed.pixels)).toEqual([0, 1, 1, 0]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("non-static throws (not implemented yet)", () => { |
|
|
|
|
const anim: Animation = { type: "animation", width: 1, height: 1, frames: [] }; |
|
|
|
|
expect(() => buildBinBuffer(anim)).toThrow(/not yet implemented/); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// MonoDisplayFile — JSON descriptor builder
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
describe("MonoDisplayFile", () => { |
|
|
|
|
const parser = new MonoDisplayParser(); |
|
|
|
|
|
|
|
|
|
test("single Image2D fills full display", () => { |
|
|
|
|
const pixels = new Uint8Array(4 * 2).fill(1); // 4×2 all-on
|
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
width: 4, height: 2, |
|
|
|
|
elements_always: [{ type: ElementType.Image2D, pixels, width: 4, height: 2 }], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
expect(result.type).toBe("static"); |
|
|
|
|
expect(result.width).toBe(4); |
|
|
|
|
expect(result.height).toBe(2); |
|
|
|
|
expect(result.pixels.every(p => p === 1)).toBe(true); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("xoffset / yoffset positions element", () => { |
|
|
|
|
// 6×2 display, 2×2 all-on block placed at xoffset=2
|
|
|
|
|
const block = new Uint8Array(4).fill(1); |
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
width: 6, height: 2, |
|
|
|
|
elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 2 }], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
// cols 0,1 off — cols 2,3 on — cols 4,5 off
|
|
|
|
|
expect(result.pixels[0]).toBe(0); // (0,0) off
|
|
|
|
|
expect(result.pixels[2]).toBe(1); // (2,0) on
|
|
|
|
|
expect(result.pixels[3]).toBe(1); // (3,0) on
|
|
|
|
|
expect(result.pixels[4]).toBe(0); // (4,0) off
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("two overlapping elements OR-blend", () => { |
|
|
|
|
// 4×1 display: element A sets pixels [0,1,0,0], element B sets [0,0,0,1]
|
|
|
|
|
const a = new Uint8Array([0, 1, 0, 0]); |
|
|
|
|
const b = new Uint8Array([0, 0, 0, 1]); |
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
width: 4, height: 1, |
|
|
|
|
elements_always: [ |
|
|
|
|
{ type: ElementType.Image2D, pixels: a, width: 4, height: 1 }, |
|
|
|
|
{ type: ElementType.Image2D, pixels: b, width: 4, height: 1 }, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
expect(Array.from(result.pixels)).toEqual([0, 1, 0, 1]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("element clipped at right/bottom edge", () => { |
|
|
|
|
// 4×2 display, 2×2 block placed at xoffset=3 — only 1 col fits
|
|
|
|
|
const block = new Uint8Array(4).fill(1); |
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
width: 4, height: 2, |
|
|
|
|
elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 3 }], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
// col 3 on, col 4+ clipped
|
|
|
|
|
expect(result.pixels[3]).toBe(1); // (3,0) on
|
|
|
|
|
expect(result.width).toBe(4); // canvas didn't grow
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("size inferred from element bounding box when omitted", () => { |
|
|
|
|
const pixels = new Uint8Array(3 * 2).fill(1); |
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
elements_always: [{ type: ElementType.Image2D, pixels, width: 3, height: 2, xoffset: 1, yoffset: 1 }], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
expect(result.width).toBe(4); // 1 + 3
|
|
|
|
|
expect(result.height).toBe(3); // 1 + 2
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("empty descriptor produces 160×80 blank static frame", () => { |
|
|
|
|
const file = new MonoDisplayFile({}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
expect(result.width).toBe(160); |
|
|
|
|
expect(result.height).toBe(80); |
|
|
|
|
expect(result.pixels.every(p => p === 0)).toBe(true); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("toBuffer round-trips through parser", () => { |
|
|
|
|
const pixels = new Uint8Array(120 * 80); |
|
|
|
|
for (let i = 0; i < pixels.length; i++) pixels[i] = i % 2; |
|
|
|
|
const file = new MonoDisplayFile({ |
|
|
|
|
width: 160, height: 80, |
|
|
|
|
elements_always: [{ type: ElementType.Image2D, pixels, width: 120, height: 80, xoffset: 20 }], |
|
|
|
|
}); |
|
|
|
|
const result = parser.parse(file.toBuffer()) as StaticFrame; |
|
|
|
|
expect(result.width).toBe(160); |
|
|
|
|
expect(result.pixels[20]).toBe(0); // first pixel of element (i=0 → 0)
|
|
|
|
|
expect(result.pixels[21]).toBe(1); // second pixel (i=1 → 1)
|
|
|
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 }; |
|
|
|
|
const buf = buildBinBuffer(el); |
|
|
|
|
expect(buf).toBeInstanceOf(Uint8Array); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
expect(result.sections.length).toBeGreaterThan(0); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const parsed = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(Array.from(parsed.image.pixels)).toEqual([0, 1, 1, 0]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
test("pixel data round-trips correctly", () => { |
|
|
|
|
const pixels = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 1]); // 8 pixels, corners on
|
|
|
|
|
const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 8, height: 1 }; |
|
|
|
|
const buf = buildBinBuffer(el); |
|
|
|
|
const result = parser.parse(buf); |
|
|
|
|
const section = result.sections[0] as MonoFormatElementsAlways; |
|
|
|
|
const parsed = section.elements[0] as MonoFormatImage2D; |
|
|
|
|
expect(parsed.image.width).toBe(8); |
|
|
|
|
expect(parsed.image.height).toBe(1); |
|
|
|
|
expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 0, 0, 0, 0, 0, 1]); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|