brilliantsole 0.0.48 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/brilliantsole.cjs +36 -5
- package/build/brilliantsole.cjs.map +1 -1
- package/build/brilliantsole.node.module.js +36 -5
- package/build/brilliantsole.node.module.js.map +1 -1
- package/build/dts/connection/bluetooth/NobleConnectionManager.d.ts +1 -0
- package/examples/basic/script.js +3 -3
- package/package.json +1 -1
- package/src/connection/bluetooth/NobleConnectionManager.ts +11 -1
- package/src/scanner/BaseScanner.ts +2 -0
- package/src/scanner/NobleScanner.ts +23 -2
- package/build/dts/BS-output.d.ts +0 -10
- package/build/dts/connection/WebSocketClientConnectionManager.d.ts +0 -23
- package/build/dts/connection/bluetooth/BluetoothUUID.d.ts +0 -12
- package/build/dts/connection/webSocket/ClientConnectionManager.d.ts +0 -23
- package/build/dts/connection/webSocket/WebSocketClientConnectionManager.d.ts +0 -23
- package/build/dts/utils/BitmapUtils.d.ts +0 -17
- package/build/dts/utils/SpriteSheetUtils.d.ts +0 -20
- package/build/dts/utils/SvgUtils copy 2.d.ts +0 -8
- package/build/dts/utils/SvgUtils copy.d.ts +0 -9
- /package/build/dts/connection/{webSocket → websocket}/WebSocketConnectionManager.d.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brilliantsole.node.module.js","sources":["../brilliantsole/utils/environment.ts","../brilliantsole/utils/Console.ts","../brilliantsole/utils/EventDispatcher.ts","../brilliantsole/utils/Timer.ts","../brilliantsole/utils/checksum.ts","../brilliantsole/utils/Text.ts","../brilliantsole/utils/ArrayBufferUtils.ts","../brilliantsole/FileTransferManager.ts","../brilliantsole/utils/MathUtils.ts","../brilliantsole/utils/RangeHelper.ts","../brilliantsole/utils/CenterOfPressureHelper.ts","../brilliantsole/utils/ArrayUtils.ts","../brilliantsole/sensor/PressureSensorDataManager.ts","../brilliantsole/sensor/MotionSensorDataManager.ts","../brilliantsole/sensor/BarometerSensorDataManager.ts","../brilliantsole/utils/ParseUtils.ts","../brilliantsole/CameraManager.ts","../brilliantsole/utils/AudioUtils.ts","../brilliantsole/MicrophoneManager.ts","../brilliantsole/sensor/SensorDataManager.ts","../node_modules/auto-bind/index.js","../brilliantsole/sensor/SensorConfigurationManager.ts","../brilliantsole/TfliteManager.ts","../brilliantsole/DeviceInformationManager.ts","../brilliantsole/InformationManager.ts","../brilliantsole/vibration/VibrationWaveformEffects.ts","../brilliantsole/vibration/VibrationManager.ts","../brilliantsole/WifiManager.ts","../brilliantsole/utils/ColorUtils.ts","../brilliantsole/utils/DisplayContextState.ts","../brilliantsole/utils/ObjectUtils.ts","../brilliantsole/utils/DisplayContextStateHelper.ts","../brilliantsole/utils/DisplayUtils.ts","../brilliantsole/utils/DisplayContextCommand.ts","../brilliantsole/utils/PathUtils.ts","../brilliantsole/utils/SvgUtils.ts","../brilliantsole/utils/stringUtils.ts","../brilliantsole/utils/DisplaySpriteSheetUtils.ts","../brilliantsole/utils/DisplayBitmapUtils.ts","../brilliantsole/utils/DisplayManagerInterface.ts","../brilliantsole/DisplayManager.ts","../brilliantsole/connection/BaseConnectionManager.ts","../brilliantsole/utils/EventUtils.ts","../brilliantsole/connection/bluetooth/bluetoothUUIDs.ts","../brilliantsole/connection/bluetooth/BluetoothConnectionManager.ts","../brilliantsole/connection/bluetooth/WebBluetoothConnectionManager.ts","../brilliantsole/utils/cbor.js","../brilliantsole/utils/mcumgr.js","../brilliantsole/FirmwareManager.ts","../brilliantsole/DeviceManager.ts","../brilliantsole/server/ServerUtils.ts","../brilliantsole/server/websocket/WebSocketUtils.ts","../brilliantsole/connection/websocket/WebSocketConnectionManager.ts","../brilliantsole/connection/udp/UDPConnectionManager.ts","../brilliantsole/Device.ts","../brilliantsole/devicePair/DevicePairPressureSensorDataManager.ts","../brilliantsole/devicePair/DevicePairSensorDataManager.ts","../brilliantsole/devicePair/DevicePair.ts","../brilliantsole/utils/ThrottleUtils.ts","../brilliantsole/scanner/BaseScanner.ts","../brilliantsole/connection/bluetooth/NobleConnectionManager.ts","../brilliantsole/scanner/NobleScanner.ts","../brilliantsole/scanner/Scanner.ts","../brilliantsole/server/BaseServer.ts","../brilliantsole/server/websocket/WebSocketServer.ts","../brilliantsole/server/udp/UDPUtils.ts","../brilliantsole/server/udp/UDPServer.ts","../brilliantsole/BS.ts"],"sourcesContent":["type ENVIRONMENT_FLAG = \"__BRILLIANTSOLE__DEV__\" | \"__BRILLIANTSOLE__PROD__\";\nconst __BRILLIANTSOLE__ENVIRONMENT__: ENVIRONMENT_FLAG =\n \"__BRILLIANTSOLE__DEV__\";\n\n//@ts-expect-error\nconst isInProduction =\n __BRILLIANTSOLE__ENVIRONMENT__ == \"__BRILLIANTSOLE__PROD__\";\nconst isInDev = __BRILLIANTSOLE__ENVIRONMENT__ == \"__BRILLIANTSOLE__DEV__\";\n\n// https://github.com/flexdinesh/browser-or-node/blob/master/src/index.ts\nconst isInBrowser =\n typeof window !== \"undefined\" && typeof window?.document !== \"undefined\";\nconst isInNode =\n typeof process !== \"undefined\" && process?.versions?.node != null;\n\nconst userAgent = (isInBrowser && navigator.userAgent) || \"\";\n\nlet isBluetoothSupported = false;\nif (isInBrowser) {\n isBluetoothSupported = Boolean(navigator.bluetooth);\n} else if (isInNode) {\n isBluetoothSupported = true;\n}\n\nconst isInBluefy = isInBrowser && /Bluefy/i.test(userAgent);\nconst isInWebBLE = isInBrowser && /WebBLE/i.test(userAgent);\n\nconst isAndroid = isInBrowser && /Android/i.test(userAgent);\nconst isSafari =\n isInBrowser && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);\n\nconst isIOS = isInBrowser && /iPad|iPhone|iPod/i.test(userAgent);\nconst isMac = isInBrowser && /Macintosh/i.test(userAgent);\n\n// @ts-expect-error\nconst isInLensStudio =\n !isInBrowser &&\n !isInNode &&\n typeof global !== \"undefined\" &&\n typeof Studio !== \"undefined\";\n\nexport {\n isInDev,\n isInProduction,\n isInBrowser,\n isInNode,\n isAndroid,\n isInBluefy,\n isInWebBLE,\n isSafari,\n isInLensStudio,\n isIOS,\n isMac,\n isBluetoothSupported,\n};\n","import { isInDev, isInLensStudio, isInNode } from \"./environment.ts\";\n\ndeclare var Studio: any | undefined;\n\nexport type LogFunction = (...data: any[]) => void;\nexport type AssertLogFunction = (condition: boolean, ...data: any[]) => void;\n\nexport interface ConsoleLevelFlags {\n log?: boolean;\n warn?: boolean;\n error?: boolean;\n assert?: boolean;\n table?: boolean;\n}\n\ninterface ConsoleLike {\n log?: LogFunction;\n warn?: LogFunction;\n error?: LogFunction;\n assert?: AssertLogFunction;\n table?: LogFunction;\n}\n\nvar __console: ConsoleLike;\nif (isInLensStudio) {\n const log = function (...args: any[]) {\n Studio.log(args.map((value) => new String(value)).join(\",\"));\n };\n __console = {};\n __console.log = log;\n __console.warn = log.bind(__console, \"WARNING\");\n __console.error = log.bind(__console, \"ERROR\");\n} else {\n __console = console;\n}\n\nfunction getCallerFunctionPath(): string {\n const stack = new Error().stack;\n if (!stack) return \"\";\n\n const lines = stack.split(\"\\n\");\n const callerLine = lines[3] || lines[2];\n\n const match = callerLine.match(/at (.*?) \\(/) || callerLine.match(/at (.*)/);\n if (!match) return \"\";\n\n const fullFn = match[1].trim();\n return `[${fullFn}]`;\n}\n\nfunction wrapWithLocation(fn: LogFunction): LogFunction {\n return (...args: any[]) => {\n if (isInNode) {\n const functionPath = getCallerFunctionPath();\n fn(functionPath, ...args);\n } else {\n fn(...args);\n }\n };\n}\n\n// console.assert not supported in WebBLE\nif (!__console.assert) {\n const assert: AssertLogFunction = (condition, ...data) => {\n if (!condition) {\n __console.warn!(...data);\n }\n };\n __console.assert = assert;\n}\n\n// console.table not supported in WebBLE\nif (!__console.table) {\n const table: LogFunction = (...data) => {\n __console.log!(...data);\n };\n __console.table = table;\n}\n\nfunction emptyFunction() {}\n\nconst log: LogFunction = isInNode\n ? wrapWithLocation(__console.log!.bind(__console))\n : __console.log!.bind(__console);\nconst warn: LogFunction = isInNode\n ? wrapWithLocation(__console.warn!.bind(__console))\n : __console.warn!.bind(__console);\nconst error: LogFunction = isInNode\n ? wrapWithLocation(__console.error!.bind(__console))\n : __console.error!.bind(__console);\nconst table: LogFunction = isInNode\n ? wrapWithLocation(__console.table!.bind(__console))\n : __console.table!.bind(__console);\nconst assert: AssertLogFunction = __console.assert.bind(__console);\n\nclass Console {\n static #consoles: { [type: string]: Console } = {};\n\n constructor(type: string) {\n if (Console.#consoles[type]) {\n throw new Error(`\"${type}\" console already exists`);\n }\n Console.#consoles[type] = this;\n }\n\n #levelFlags: ConsoleLevelFlags = {\n log: isInDev,\n warn: isInDev,\n assert: true,\n error: true,\n table: true,\n };\n\n setLevelFlags(levelFlags: ConsoleLevelFlags) {\n Object.assign(this.#levelFlags, levelFlags);\n }\n\n /** @throws {Error} if no console with type \"type\" is found */\n static setLevelFlagsForType(type: string, levelFlags: ConsoleLevelFlags) {\n if (!this.#consoles[type]) {\n throw new Error(`no console found with type \"${type}\"`);\n }\n this.#consoles[type].setLevelFlags(levelFlags);\n }\n\n static setAllLevelFlags(levelFlags: ConsoleLevelFlags) {\n for (const type in this.#consoles) {\n this.#consoles[type].setLevelFlags(levelFlags);\n }\n }\n\n static create(type: string, levelFlags?: ConsoleLevelFlags): Console {\n const console = this.#consoles[type] || new Console(type);\n if (isInDev && levelFlags) {\n console.setLevelFlags(levelFlags);\n }\n return console;\n }\n\n get log() {\n return this.#levelFlags.log ? log : emptyFunction;\n }\n\n get warn() {\n return this.#levelFlags.warn ? warn : emptyFunction;\n }\n\n get error() {\n return this.#levelFlags.error ? error : emptyFunction;\n }\n\n get assert() {\n return this.#levelFlags.assert ? assert : emptyFunction;\n }\n\n get table() {\n return this.#levelFlags.table ? table : emptyFunction;\n }\n\n /** @throws {Error} if condition is not met */\n assertWithError(condition: any, message: string) {\n if (!Boolean(condition)) {\n throw new Error(message);\n }\n }\n\n /** @throws {Error} if value's type doesn't match */\n assertTypeWithError(value: any, type: string) {\n this.assertWithError(\n typeof value == type,\n `value ${value} of type \"${typeof value}\" not of type \"${type}\"`\n );\n }\n\n /** @throws {Error} if value's type doesn't match */\n assertEnumWithError(value: string, enumeration: readonly string[]) {\n this.assertWithError(\n enumeration.includes(value),\n `invalid enum \"${value}\"`\n );\n }\n\n /** @throws {Error} if value is not within some range */\n assertRangeWithError(name: string, value: number, min: number, max: number) {\n this.assertWithError(\n value >= min && value <= max,\n `${name} ${value} must be within ${min}-${max}`\n );\n }\n}\n\nexport function createConsole(\n type: string,\n levelFlags?: ConsoleLevelFlags\n): Console {\n return Console.create(type, levelFlags);\n}\n\n/** @throws {Error} if no console with type is found */\nexport function setConsoleLevelFlagsForType(\n type: string,\n levelFlags: ConsoleLevelFlags\n) {\n Console.setLevelFlagsForType(type, levelFlags);\n}\n\nexport function setAllConsoleLevelFlags(levelFlags: ConsoleLevelFlags) {\n Console.setAllLevelFlags(levelFlags);\n}\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"EventDispatcher\", { log: false });\n\nexport type EventMap<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [T in keyof EventMessages]: {\n type: T;\n target: Target;\n message: EventMessages[T];\n };\n};\nexport type EventListenerMap<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [T in keyof EventMessages]: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void;\n};\n\nexport type Event<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = EventMap<Target, EventType, EventMessages>[keyof EventMessages];\n\ntype SpecificEvent<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>,\n SpecificEventType extends EventType\n> = {\n type: SpecificEventType;\n target: Target;\n message: EventMessages[SpecificEventType];\n};\n\nexport type BoundEventListeners<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [SpecificEventType in keyof EventMessages]?: (\n // @ts-expect-error\n event: SpecificEvent<Target, EventType, EventMessages, SpecificEventType>\n ) => void;\n};\n\nclass EventDispatcher<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> {\n private listeners: {\n [T in EventType]?: {\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void;\n once?: boolean;\n shouldRemove?: boolean;\n }[];\n } = {};\n\n constructor(\n private target: Target,\n private validEventTypes: readonly EventType[]\n ) {\n this.addEventListener = this.addEventListener.bind(this);\n this.removeEventListener = this.removeEventListener.bind(this);\n this.removeEventListeners = this.removeEventListeners.bind(this);\n this.removeAllEventListeners = this.removeAllEventListeners.bind(this);\n this.dispatchEvent = this.dispatchEvent.bind(this);\n this.waitForEvent = this.waitForEvent.bind(this);\n }\n\n private isValidEventType(type: any): type is EventType {\n return this.validEventTypes.includes(type);\n }\n\n private updateEventListeners(type: EventType) {\n if (!this.listeners[type]) return;\n this.listeners[type] = this.listeners[type]!.filter((listenerObj) => {\n if (listenerObj.shouldRemove) {\n _console.log(`removing \"${type}\" eventListener`, listenerObj);\n }\n return !listenerObj.shouldRemove;\n });\n }\n\n addEventListener<T extends EventType>(\n type: T,\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void,\n options: { once?: boolean } = { once: false }\n ): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) {\n this.listeners[type] = [];\n _console.log(`creating \"${type}\" listeners array`, this.listeners[type]!);\n }\n const alreadyAdded = this.listeners[type].find((listenerObject) => {\n return (\n listenerObject.listener == listener &&\n listenerObject.once == options.once\n );\n });\n if (alreadyAdded) {\n _console.log(\"already added listener\");\n return;\n }\n _console.log(`adding \"${type}\" listener`, listener, options);\n this.listeners[type]!.push({ listener, once: options.once });\n\n _console.log(\n `currently have ${this.listeners[type]!.length} \"${type}\" listeners`\n );\n }\n\n removeEventListener<T extends EventType>(\n type: T,\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void\n ): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n _console.log(`removing \"${type}\" listener...`, listener);\n this.listeners[type]!.forEach((listenerObj) => {\n const isListenerToRemove = listenerObj.listener === listener;\n if (isListenerToRemove) {\n _console.log(`flagging \"${type}\" listener`, listener);\n listenerObj.shouldRemove = true;\n }\n });\n\n this.updateEventListeners(type);\n }\n\n removeEventListeners<T extends EventType>(type: T): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n _console.log(`removing \"${type}\" listeners...`);\n this.listeners[type] = [];\n }\n\n removeAllEventListeners(): void {\n _console.log(`removing listeners...`);\n this.listeners = {};\n }\n\n dispatchEvent<T extends EventType>(type: T, message: EventMessages[T]): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n // Take a snapshot of listeners at this moment\n const listenersSnapshot = [...this.listeners[type]!];\n\n listenersSnapshot.forEach((listenerObj) => {\n if (listenerObj.shouldRemove) {\n return;\n }\n\n _console.log(`dispatching \"${type}\" listener`, listenerObj);\n try {\n listenerObj.listener({ type, target: this.target, message });\n } catch (error) {\n console.error(error);\n }\n\n if (listenerObj.once) {\n _console.log(`flagging \"${type}\" listener`, listenerObj);\n listenerObj.shouldRemove = true;\n }\n });\n\n this.updateEventListeners(type);\n }\n\n waitForEvent<T extends EventType>(\n type: T\n ): Promise<{ type: T; target: Target; message: EventMessages[T] }> {\n return new Promise((resolve) => {\n const onceListener = (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => {\n resolve(event);\n };\n\n this.addEventListener(type, onceListener, { once: true });\n });\n }\n}\n\nexport default EventDispatcher;\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"Timer\", { log: false });\n\nexport async function wait(delay: number) {\n _console.log(`waiting for ${delay}ms`);\n return new Promise((resolve: Function) => {\n setTimeout(() => resolve(), delay);\n });\n}\n\nexport class Timer {\n #callback!: Function;\n get callback() {\n return this.#callback;\n }\n set callback(newCallback) {\n _console.assertTypeWithError(newCallback, \"function\");\n _console.log({ newCallback });\n this.#callback = newCallback;\n if (this.isRunning) {\n this.restart();\n }\n }\n\n #interval!: number;\n get interval() {\n return this.#interval;\n }\n set interval(newInterval) {\n _console.assertTypeWithError(newInterval, \"number\");\n _console.assertWithError(newInterval > 0, \"interval must be above 0\");\n _console.log({ newInterval });\n this.#interval = newInterval;\n if (this.isRunning) {\n this.restart();\n }\n }\n\n constructor(callback: Function, interval: number) {\n this.interval = interval;\n this.callback = callback;\n }\n\n #intervalId: number | undefined;\n get isRunning() {\n return this.#intervalId != undefined;\n }\n\n start(immediately = false) {\n if (this.isRunning) {\n _console.log(\"interval already running\");\n return;\n }\n _console.log(`starting interval every ${this.#interval}ms`);\n this.#intervalId = setInterval(this.#callback, this.#interval);\n if (immediately) {\n this.#callback();\n }\n }\n stop() {\n if (!this.isRunning) {\n _console.log(\"interval already not running\");\n return;\n }\n _console.log(\"stopping interval\");\n clearInterval(this.#intervalId);\n this.#intervalId = undefined;\n }\n restart(startImmediately = false) {\n this.stop();\n this.start(startImmediately);\n }\n}\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"checksum\", { log: false });\n\n// https://github.com/googlecreativelab/tiny-motion-trainer/blob/5fceb49f018ae0c403bf9f0ccc437309c2acb507/frontend/src/tf4micro-motion-kit/modules/bleFileTransfer#L195\n\n// See http://home.thep.lu.se/~bjorn/crc/ for more information on simple CRC32 calculations.\nexport function crc32ForByte(r: number) {\n for (let j = 0; j < 8; ++j) {\n r = (r & 1 ? 0 : 0xedb88320) ^ (r >>> 1);\n }\n return r ^ 0xff000000;\n}\n\nconst tableSize = 256;\nconst crc32Table = new Uint32Array(tableSize);\nfor (let i = 0; i < tableSize; ++i) {\n crc32Table[i] = crc32ForByte(i);\n}\n\nexport function crc32(dataIterable: ArrayBuffer | number[]) {\n let dataBytes = new Uint8Array(dataIterable);\n let crc = 0;\n for (let i = 0; i < dataBytes.byteLength; ++i) {\n const crcLowByte = crc & 0x000000ff;\n const dataByte = dataBytes[i];\n const tableIndex = crcLowByte ^ dataByte;\n // The last >>> is to convert this into an unsigned 32-bit integer.\n crc = (crc32Table[tableIndex] ^ (crc >>> 8)) >>> 0;\n }\n return crc;\n}\n\n// This is a small test function for the CRC32 implementation, not normally called but left in\n// for debugging purposes. We know the expected CRC32 of [97, 98, 99, 100, 101] is 2240272485,\n// or 0x8587d865, so if anything else is output we know there's an error in the implementation.\nexport function testCrc32() {\n const testArray = [97, 98, 99, 100, 101];\n const testArrayCrc32 = crc32(testArray);\n _console.log(\"CRC32 for [97, 98, 99, 100, 101] is 0x\" + testArrayCrc32.toString(16) + \" (\" + testArrayCrc32 + \")\");\n}\n","var _TextEncoder;\nif (typeof TextEncoder == \"undefined\") {\n _TextEncoder = class {\n encode(string: string) {\n const encoding = Array.from(string).map((char) => char.charCodeAt(0));\n return Uint8Array.from(encoding);\n }\n };\n} else {\n _TextEncoder = TextEncoder;\n}\n\nvar _TextDecoder;\nif (typeof TextDecoder == \"undefined\") {\n _TextDecoder = class {\n decode(data: ArrayBuffer) {\n const byteArray = Array.from(new Uint8Array(data));\n return byteArray\n .map((value) => {\n return String.fromCharCode(value);\n })\n .join(\"\");\n }\n };\n} else {\n _TextDecoder = TextDecoder;\n}\n\nexport const textEncoder = new _TextEncoder();\nexport const textDecoder = new _TextDecoder();\n","import { createConsole } from \"./Console.ts\";\nimport { textEncoder } from \"./Text.ts\";\n\nconst _console = createConsole(\"ArrayBufferUtils\", { log: false });\n\nexport function concatenateArrayBuffers(...arrayBuffers: any[]): ArrayBuffer {\n arrayBuffers = arrayBuffers.filter(\n (arrayBuffer) => arrayBuffer != undefined || arrayBuffer != null\n );\n arrayBuffers = arrayBuffers.map((arrayBuffer) => {\n if (typeof arrayBuffer == \"number\") {\n const number = arrayBuffer;\n return Uint8Array.from([Math.floor(number)]);\n } else if (typeof arrayBuffer == \"boolean\") {\n const boolean = arrayBuffer;\n return Uint8Array.from([boolean ? 1 : 0]);\n } else if (typeof arrayBuffer == \"string\") {\n const string = arrayBuffer;\n return stringToArrayBuffer(string);\n } else if (arrayBuffer instanceof Array) {\n const array = arrayBuffer;\n return concatenateArrayBuffers(...array);\n } else if (arrayBuffer instanceof ArrayBuffer) {\n return arrayBuffer;\n } else if (\n \"buffer\" in arrayBuffer &&\n arrayBuffer.buffer instanceof ArrayBuffer\n ) {\n const bufferContainer = arrayBuffer;\n return bufferContainer.buffer;\n } else if (arrayBuffer instanceof DataView) {\n const dataView = arrayBuffer;\n return dataView.buffer;\n } else if (typeof arrayBuffer == \"object\") {\n const object = arrayBuffer;\n return objectToArrayBuffer(object);\n } else {\n return arrayBuffer;\n }\n });\n arrayBuffers = arrayBuffers.filter(\n (arrayBuffer) => arrayBuffer && \"byteLength\" in arrayBuffer\n );\n const length = arrayBuffers.reduce(\n (length, arrayBuffer) => length + arrayBuffer.byteLength,\n 0\n );\n const uint8Array = new Uint8Array(length);\n let byteOffset = 0;\n arrayBuffers.forEach((arrayBuffer) => {\n uint8Array.set(new Uint8Array(arrayBuffer), byteOffset);\n byteOffset += arrayBuffer.byteLength;\n });\n return uint8Array.buffer;\n}\n\nexport function dataToArrayBuffer(data: Buffer) {\n return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);\n}\n\nexport function stringToArrayBuffer(string: string) {\n const encoding = textEncoder.encode(string);\n return concatenateArrayBuffers(encoding.byteLength, encoding);\n}\n\nexport function objectToArrayBuffer(object: object) {\n return stringToArrayBuffer(JSON.stringify(object));\n}\n\nexport function sliceDataView(\n dataView: DataView,\n begin: number,\n length?: number\n) {\n let end;\n if (length != undefined) {\n end = dataView.byteOffset + begin + length;\n }\n _console.log({ dataView, begin, end, length });\n return new DataView(dataView.buffer.slice(dataView.byteOffset + begin, end));\n}\n\nexport type FileLike = number[] | ArrayBuffer | DataView | URL | string | File;\n\nexport async function getFileBuffer(file: FileLike) {\n let fileBuffer;\n if (file instanceof Array) {\n fileBuffer = Uint8Array.from(file);\n } else if (file instanceof DataView) {\n fileBuffer = file.buffer;\n } else if (typeof file == \"string\" || file instanceof URL) {\n const response = await fetch(file);\n fileBuffer = await response.arrayBuffer();\n } else if (file instanceof File) {\n fileBuffer = await file.arrayBuffer();\n } else if (file instanceof ArrayBuffer) {\n fileBuffer = file;\n } else {\n throw { error: \"invalid file type\", file };\n }\n return fileBuffer;\n}\n\nexport function UInt8ByteBuffer(value: number) {\n return Uint8Array.from([value]).buffer;\n}\n","import { createConsole } from \"./utils/Console.ts\";\nimport { crc32 } from \"./utils/checksum.ts\";\nimport { getFileBuffer, UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"FileTransferManager\", { log: false });\n\nexport const FileTransferMessageTypes = [\n \"getFileTypes\",\n \"maxFileLength\",\n \"getFileType\",\n \"setFileType\",\n \"getFileLength\",\n \"setFileLength\",\n \"getFileChecksum\",\n \"setFileChecksum\",\n \"setFileTransferCommand\",\n \"fileTransferStatus\",\n \"getFileBlock\",\n \"setFileBlock\",\n \"fileBytesTransferred\",\n] as const;\nexport type FileTransferMessageType = (typeof FileTransferMessageTypes)[number];\n\nexport const FileTypes = [\n \"tflite\",\n \"wifiServerCert\",\n \"wifiServerKey\",\n \"spriteSheet\",\n] as const;\nexport type FileType = (typeof FileTypes)[number];\n\nexport const FileTransferStatuses = [\"idle\", \"sending\", \"receiving\"] as const;\nexport type FileTransferStatus = (typeof FileTransferStatuses)[number];\n\nexport const FileTransferCommands = [\n \"startSend\",\n \"startReceive\",\n \"cancel\",\n] as const;\nexport type FileTransferCommand = (typeof FileTransferCommands)[number];\n\nexport const FileTransferDirections = [\"sending\", \"receiving\"] as const;\nexport type FileTransferDirection = (typeof FileTransferDirections)[number];\n\nexport const FileTransferEventTypes = [\n ...FileTransferMessageTypes,\n \"fileTransferProgress\",\n \"fileTransferComplete\",\n \"fileReceived\",\n] as const;\nexport type FileTransferEventType = (typeof FileTransferEventTypes)[number];\n\nexport const RequiredFileTransferMessageTypes: FileTransferMessageType[] = [\n \"maxFileLength\",\n \"getFileLength\",\n \"getFileChecksum\",\n \"getFileType\",\n \"fileTransferStatus\",\n];\n\nexport interface FileConfiguration {\n file: FileLike;\n type: FileType;\n}\n\nexport interface FileTransferEventMessages {\n getFileTypes: { fileTypes: FileType[] };\n maxFileLength: { maxFileLength: number };\n getFileType: { fileType: FileType };\n getFileLength: { fileLength: number };\n getFileChecksum: { fileChecksum: number };\n fileTransferStatus: {\n fileType: FileType;\n fileTransferStatus: FileTransferStatus;\n };\n getFileBlock: { fileTransferBlock: DataView };\n fileTransferProgress: { fileType: FileType; progress: number };\n fileTransferComplete: {\n fileType: FileType;\n direction: FileTransferDirection;\n };\n fileReceived: { fileType: FileType; file: File | Blob };\n}\n\nexport type FileTransferEventDispatcher = EventDispatcher<\n Device,\n FileTransferEventType,\n FileTransferEventMessages\n>;\nexport type SendFileTransferMessageCallback =\n SendMessageCallback<FileTransferMessageType>;\n\nexport type SendFileCallback = (\n type: FileType,\n file: FileLike,\n override?: boolean\n) => Promise<boolean>;\n\nclass FileTransferManager {\n constructor() {\n autoBind(this);\n }\n sendMessage!: SendFileTransferMessageCallback;\n\n eventDispatcher!: FileTransferEventDispatcher;\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #assertValidType(type: FileType) {\n _console.assertEnumWithError(type, FileTypes);\n }\n #isValidType(type: FileType) {\n return FileTypes.includes(type);\n }\n #assertValidTypeEnum(typeEnum: number) {\n _console.assertWithError(\n typeEnum in FileTypes,\n `invalid typeEnum ${typeEnum}`\n );\n }\n\n #assertValidStatusEnum(statusEnum: number) {\n _console.assertWithError(\n statusEnum in FileTransferStatuses,\n `invalid statusEnum ${statusEnum}`\n );\n }\n #assertValidCommand(command: FileTransferCommand) {\n _console.assertEnumWithError(command, FileTransferCommands);\n }\n\n #fileTypes: FileType[] = [];\n get fileTypes() {\n return this.#fileTypes;\n }\n #parseFileTypes(dataView: DataView) {\n const fileTypes = Array.from(new Uint8Array(dataView.buffer))\n .map((index) => FileTypes[index])\n .filter(Boolean);\n this.#fileTypes = fileTypes;\n _console.log(\"fileTypes\", fileTypes);\n this.#dispatchEvent(\"getFileTypes\", {\n fileTypes: this.#fileTypes,\n });\n }\n\n static #MaxLength = 0; // kB\n static get MaxLength() {\n return this.#MaxLength;\n }\n #maxLength = FileTransferManager.MaxLength;\n /** kB */\n get maxLength() {\n return this.#maxLength;\n }\n #parseMaxLength(dataView: DataView) {\n _console.log(\"parseFileMaxLength\", dataView);\n const maxLength = dataView.getUint32(0, true);\n _console.log(`maxLength: ${maxLength / 1024}kB`);\n this.#updateMaxLength(maxLength);\n }\n #updateMaxLength(maxLength: number) {\n _console.log({ maxLength });\n this.#maxLength = maxLength;\n this.#dispatchEvent(\"maxFileLength\", { maxFileLength: maxLength });\n }\n #assertValidLength(length: number) {\n _console.assertWithError(\n length <= this.maxLength,\n `file length ${length}kB too large - must be ${this.maxLength}kB or less`\n );\n }\n\n #type: FileType | undefined;\n get type() {\n return this.#type;\n }\n #parseType(dataView: DataView) {\n _console.log(\"parseFileType\", dataView);\n const typeEnum = dataView.getUint8(0);\n this.#assertValidTypeEnum(typeEnum);\n const type = FileTypes[typeEnum];\n this.#updateType(type);\n }\n #updateType(type: FileType) {\n _console.log({ fileTransferType: type });\n this.#type = type;\n this.#dispatchEvent(\"getFileType\", { fileType: type });\n }\n async #setType(newType: FileType, sendImmediately?: boolean) {\n this.#assertValidType(newType);\n if (this.type == newType) {\n _console.log(`redundant type assignment ${newType}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileType\");\n\n const typeEnum = FileTypes.indexOf(newType);\n\n this.sendMessage(\n [{ type: \"setFileType\", data: UInt8ByteBuffer(typeEnum) }],\n sendImmediately\n );\n\n await promise;\n }\n\n #length = 0;\n get length() {\n return this.#length;\n }\n #parseLength(dataView: DataView) {\n _console.log(\"parseFileLength\", dataView);\n const length = dataView.getUint32(0, true);\n\n this.#updateLength(length);\n }\n #updateLength(length: number) {\n _console.log(`length: ${length / 1024}kB`);\n this.#length = length;\n this.#dispatchEvent(\"getFileLength\", { fileLength: length });\n }\n async #setLength(newLength: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newLength, \"number\");\n this.#assertValidLength(newLength);\n if (this.length == newLength) {\n _console.log(`redundant length assignment ${newLength}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileLength\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, newLength, true);\n this.sendMessage(\n [{ type: \"setFileLength\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #checksum = 0;\n get checksum() {\n return this.#checksum;\n }\n #parseChecksum(dataView: DataView) {\n _console.log(\"checksum\", dataView);\n const checksum = dataView.getUint32(0, true);\n this.#updateChecksum(checksum);\n }\n #updateChecksum(checksum: number) {\n _console.log({ checksum });\n this.#checksum = checksum;\n this.#dispatchEvent(\"getFileChecksum\", { fileChecksum: checksum });\n }\n async #setChecksum(newChecksum: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newChecksum, \"number\");\n if (this.checksum == newChecksum) {\n _console.log(`redundant checksum assignment ${newChecksum}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileChecksum\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, newChecksum, true);\n this.sendMessage(\n [{ type: \"setFileChecksum\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n async #setCommand(command: FileTransferCommand, sendImmediately?: boolean) {\n this.#assertValidCommand(command);\n\n const promise = this.waitForEvent(\"fileTransferStatus\");\n _console.log(`setting command ${command}`);\n const commandEnum = FileTransferCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"setFileTransferCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n\n #status: FileTransferStatus = \"idle\";\n get status() {\n return this.#status;\n }\n #parseStatus(dataView: DataView) {\n _console.log(\"parseFileStatus\", dataView);\n const statusEnum = dataView.getUint8(0);\n this.#assertValidStatusEnum(statusEnum);\n const status = FileTransferStatuses[statusEnum];\n this.#updateStatus(status);\n }\n #updateStatus(status: FileTransferStatus) {\n _console.log({ status });\n this.#status = status;\n this.#receivedBlocks.length = 0;\n this.#isCancelling = false;\n this.#buffer = undefined;\n this.#bytesTransferred = 0;\n this.#dispatchEvent(\"fileTransferStatus\", {\n fileTransferStatus: status,\n fileType: this.type!,\n });\n }\n #assertIsIdle() {\n _console.assertWithError(this.#status == \"idle\", \"status is not idle\");\n }\n #assertIsNotIdle() {\n _console.assertWithError(this.#status != \"idle\", \"status is idle\");\n }\n\n // BLOCK\n\n #receivedBlocks: ArrayBuffer[] = [];\n\n async #parseBlock(dataView: DataView) {\n _console.log(\"parseFileBlock\", dataView);\n this.#receivedBlocks.push(dataView.buffer);\n\n const bytesReceived = this.#receivedBlocks.reduce(\n (sum, arrayBuffer) => (sum += arrayBuffer.byteLength),\n 0\n );\n const progress = bytesReceived / this.#length;\n\n _console.log(\n `received ${bytesReceived} of ${this.#length} bytes (${progress * 100}%)`\n );\n\n this.#dispatchEvent(\"fileTransferProgress\", {\n progress,\n fileType: this.type!,\n });\n\n if (bytesReceived != this.#length) {\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, bytesReceived, true);\n\n if (this.isServerSide) {\n return;\n }\n await this.sendMessage([\n { type: \"fileBytesTransferred\", data: dataView.buffer },\n ]);\n return;\n }\n\n _console.log(\"file transfer complete\");\n\n let fileName = new Date().toLocaleString();\n switch (this.type) {\n case \"tflite\":\n fileName += \".tflite\";\n break;\n case \"wifiServerCert\":\n fileName += \"_server.crt\";\n break;\n case \"wifiServerKey\":\n fileName += \"_server.key\";\n break;\n }\n\n let file: File | Blob;\n if (typeof File !== \"undefined\") {\n file = new File(this.#receivedBlocks, fileName);\n } else {\n file = new Blob(this.#receivedBlocks);\n }\n\n const arrayBuffer = await file.arrayBuffer();\n const checksum = crc32(arrayBuffer);\n _console.log({ checksum });\n\n if (checksum != this.#checksum) {\n _console.error(\n `wrong checksum - expected ${this.#checksum}, got ${checksum}`\n );\n return;\n }\n\n _console.log(\"received file\", file);\n\n this.#dispatchEvent(\"getFileBlock\", { fileTransferBlock: dataView });\n this.#dispatchEvent(\"fileTransferComplete\", {\n direction: \"receiving\",\n fileType: this.type!,\n });\n this.#dispatchEvent(\"fileReceived\", { file, fileType: this.type! });\n }\n\n parseMessage(messageType: FileTransferMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getFileTypes\":\n this.#parseFileTypes(dataView);\n break;\n case \"maxFileLength\":\n this.#parseMaxLength(dataView);\n break;\n case \"getFileType\":\n case \"setFileType\":\n this.#parseType(dataView);\n break;\n case \"getFileLength\":\n case \"setFileLength\":\n this.#parseLength(dataView);\n break;\n case \"getFileChecksum\":\n case \"setFileChecksum\":\n this.#parseChecksum(dataView);\n break;\n case \"fileTransferStatus\":\n this.#parseStatus(dataView);\n break;\n case \"getFileBlock\":\n this.#parseBlock(dataView);\n break;\n case \"fileBytesTransferred\":\n this.#parseBytesTransferred(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n async send(type: FileType, file: FileLike, override?: boolean) {\n if (true) {\n this.#assertIsIdle();\n this.#assertValidType(type);\n } else {\n if (this.status != \"idle\") {\n _console.warn(`cannot send file - status is ${this.status}`);\n return false;\n }\n if (!this.#isValidType(type)) {\n _console.warn(`invalid fileType ${type}`);\n return false;\n }\n }\n\n const fileBuffer = await getFileBuffer(file);\n const fileLength = fileBuffer.byteLength;\n const checksum = crc32(fileBuffer);\n this.#assertValidLength(fileLength);\n\n if (!override) {\n if (type != this.type) {\n _console.log(\"different fileTypes - sending\");\n } else if (fileLength != this.length) {\n _console.log(\"different fileLengths - sending\");\n } else if (checksum != this.checksum) {\n _console.log(\"different fileChecksums - sending\");\n } else {\n _console.log(\"already sent file\");\n return false;\n }\n }\n\n const promises: Promise<any>[] = [];\n\n promises.push(this.#setType(type, false));\n promises.push(this.#setLength(fileLength, false));\n promises.push(this.#setChecksum(checksum, false));\n promises.push(this.#setCommand(\"startSend\", false));\n\n this.sendMessage();\n\n await Promise.all(promises);\n\n if (this.#buffer) {\n return false;\n }\n if (this.#length != fileLength) {\n return false;\n }\n if (this.#checksum != checksum) {\n return false;\n }\n\n await this.#send(fileBuffer);\n\n return true;\n }\n\n #buffer?: ArrayBuffer;\n #bytesTransferred = 0;\n async #send(buffer: ArrayBuffer) {\n this.#buffer = buffer;\n return this.#sendBlock();\n }\n\n mtu!: number;\n async #sendBlock(): Promise<void> {\n if (this.status != \"sending\") {\n return;\n }\n if (this.#isCancelling) {\n _console.error(\"not sending block - busy cancelling\");\n return;\n }\n if (!this.#buffer) {\n if (!this.isServerSide) {\n _console.error(\"no buffer defined\");\n }\n return;\n }\n\n const buffer = this.#buffer;\n let offset = this.#bytesTransferred;\n\n _console.log(\"sending block\", { buffer, offset, mtu: this.mtu });\n\n const slicedBuffer = buffer.slice(offset, offset + (this.mtu - 3 - 3));\n _console.log(\"slicedBuffer\", slicedBuffer);\n const bytesLeft = buffer.byteLength - offset;\n\n const progress = 1 - bytesLeft / buffer.byteLength;\n _console.log(\n `sending bytes ${offset}-${offset + slicedBuffer.byteLength} of ${\n buffer.byteLength\n } bytes (${progress * 100}%)`\n );\n this.#dispatchEvent(\"fileTransferProgress\", {\n progress,\n fileType: this.type!,\n });\n if (slicedBuffer.byteLength == 0) {\n _console.log(\"finished sending buffer\");\n this.#dispatchEvent(\"fileTransferComplete\", {\n direction: \"sending\",\n fileType: this.type!,\n });\n } else {\n await this.sendMessage([{ type: \"setFileBlock\", data: slicedBuffer }]);\n this.#bytesTransferred = offset + slicedBuffer.byteLength;\n //return this.#sendBlock(buffer, offset + slicedBuffer.byteLength);\n }\n }\n\n async #parseBytesTransferred(dataView: DataView) {\n _console.log(\"parseBytesTransferred\", dataView);\n const bytesTransferred = dataView.getUint32(0, true);\n _console.log({ bytesTransferred });\n if (this.status != \"sending\") {\n _console.error(`not currently sending file`);\n return;\n }\n if (!this.isServerSide && this.#bytesTransferred != bytesTransferred) {\n _console.error(\n `bytesTransferred are not equal - got ${bytesTransferred}, expected ${\n this.#bytesTransferred\n }`\n );\n this.cancel();\n return;\n }\n this.#sendBlock();\n }\n\n async receive(type: FileType) {\n this.#assertIsIdle();\n\n this.#assertValidType(type);\n\n await this.#setType(type);\n await this.#setCommand(\"startReceive\");\n }\n\n #isCancelling = false;\n async cancel() {\n this.#assertIsNotIdle();\n _console.log(\"cancelling file transfer...\");\n this.#isCancelling = true;\n await this.#setCommand(\"cancel\");\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n _console.log(\"redundant isServerSide assignment\");\n return;\n }\n _console.log({ newIsServerSide });\n this.#isServerSide = newIsServerSide;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required fileTransfer information\");\n const messages = RequiredFileTransferMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n clear() {\n this.#receivedBlocks.length = 0;\n this.#isCancelling = false;\n this.#buffer = undefined;\n this.#bytesTransferred = 0;\n this.#isServerSide = false;\n this.#checksum = 0;\n this.#fileTypes.length = 0;\n this.#type = undefined;\n this.#length = 0;\n this.#checksum = 0;\n this.#status = \"idle\";\n // @ts-expect-error\n this.mtu = undefined;\n }\n}\n\nexport default FileTransferManager;\n","import { PressureSensorPosition } from \"../sensor/PressureSensorDataManager.ts\";\nimport { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"MathUtils\", { log: false });\n\nexport function getInterpolation(\n value: number,\n min: number,\n max: number,\n span: number\n) {\n if (span == undefined) {\n span = max - min;\n }\n return (value - min) / span;\n}\n\nexport const Uint16Max = 2 ** 16;\nexport const Int16Max = 2 ** 15;\nexport const Int16Min = -(2 ** 15) - 1;\n\nfunction removeLower2Bytes(number: number) {\n const lower2Bytes = number % Uint16Max;\n return number - lower2Bytes;\n}\n\nconst timestampThreshold = 60_000;\n\nexport function parseTimestamp(dataView: DataView, byteOffset: number) {\n const now = Date.now();\n const nowWithoutLower2Bytes = removeLower2Bytes(now);\n const lower2Bytes = dataView.getUint16(byteOffset, true);\n let timestamp = nowWithoutLower2Bytes + lower2Bytes;\n if (Math.abs(now - timestamp) > timestampThreshold) {\n _console.log(\"correcting timestamp delta\");\n timestamp += Uint16Max * Math.sign(now - timestamp);\n }\n return timestamp;\n}\n\nexport interface Vector2 {\n x: number;\n y: number;\n}\n\nexport function getVector2Length(vector: Vector2) {\n const { x, y } = vector;\n return Math.sqrt(x ** 2 + y ** 2);\n}\n\nexport function getVector2Distance(a: Vector2, b: Vector2) {\n return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);\n}\n\nexport function getVector2DistanceSquared(a: Vector2, b: Vector2) {\n return (b.x - a.x) ** 2 + (b.y - a.y) ** 2;\n}\n\nexport function getVector2Angle(vector: Vector2) {\n const { x, y } = vector;\n return Math.atan2(y, x);\n}\n\nexport function getVector2Midpoint(a: Vector2, b: Vector2): Vector2 {\n return {\n x: (a.x + b.x) / 2,\n y: (a.y + b.y) / 2,\n };\n}\n\nexport function multiplyVector2ByScalar(\n vector: Vector2,\n scalar: number\n): Vector2 {\n let { x, y } = vector;\n x *= scalar;\n y *= scalar;\n return { x, y };\n}\nexport function normalizedVector2(vector: Vector2): Vector2 {\n return multiplyVector2ByScalar(vector, 1 / getVector2Length(vector));\n}\n\nexport interface Vector3 extends Vector2 {\n z: number;\n}\n\nexport interface Quaternion {\n x: number;\n y: number;\n z: number;\n w: number;\n}\n\nexport interface Euler {\n heading: number;\n pitch: number;\n roll: number;\n}\n\nexport function computeVoronoiWeights(\n points: PressureSensorPosition[],\n sampleCount = 100000\n) {\n const n = points.length;\n const counts = new Array(n).fill(0);\n\n for (let i = 0; i < sampleCount; i++) {\n const x = Math.random();\n const y = Math.random();\n\n // Find the closest input point\n let minDist = Infinity;\n let closestIndex = -1;\n\n for (let j = 0; j < n; j++) {\n const { x: px, y: py } = points[j];\n const dist = (px - x) ** 2 + (py - y) ** 2; // Squared Euclidean distance\n if (dist < minDist) {\n minDist = dist;\n closestIndex = j;\n }\n }\n\n // Increment count for the closest point\n counts[closestIndex]++;\n }\n\n // Convert counts to weights (sum to 1)\n return counts.map((c) => c / sampleCount);\n}\n\nexport function getVector3Length(vector: Vector3) {\n const { x, y, z } = vector;\n return Math.sqrt(x ** 2 + y ** 2 + z ** 2);\n}\n\nexport function clamp(value: number, min: number = 0, max: number = 1) {\n return Math.min(Math.max(value, min), max);\n}\n\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\nexport const twoPi = Math.PI * 2;\nexport function normalizeRadians(rad: number): number {\n return ((rad % twoPi) + twoPi) % twoPi;\n}\n\nexport function pointInPolygon(pt: Vector2, polygon: Vector2[]): boolean {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const xi = polygon[i].x,\n yi = polygon[i].y;\n const xj = polygon[j].x,\n yj = polygon[j].y;\n\n const intersect =\n yi > pt.y !== yj > pt.y &&\n pt.x < ((xj - xi) * (pt.y - yi)) / (yj - yi) + xi;\n if (intersect) inside = !inside;\n }\n return inside;\n}\n","import { getInterpolation } from \"./MathUtils.ts\";\n\nexport interface Range {\n min: number;\n max: number;\n span: number;\n}\n\nconst initialRange: Range = { min: Infinity, max: -Infinity, span: 0 };\n\nclass RangeHelper {\n #range: Range = structuredClone(initialRange);\n get min() {\n return this.#range.min;\n }\n get max() {\n return this.#range.max;\n }\n get span() {\n return this.#range.span;\n }\n\n get range() {\n return structuredClone(this.#range);\n }\n\n set min(newMin) {\n this.#range.min = newMin;\n this.#range.max = Math.max(newMin, this.#range.max);\n this.#updateSpan();\n }\n set max(newMax) {\n this.#range.max = newMax;\n this.#range.min = Math.min(newMax, this.#range.min);\n this.#updateSpan();\n }\n\n #updateSpan() {\n this.#range.span = this.#range.max - this.#range.min;\n }\n\n reset() {\n Object.assign(this.#range, initialRange);\n }\n\n update(value: number) {\n this.#range.min = Math.min(value, this.#range.min);\n this.#range.max = Math.max(value, this.#range.max);\n this.#updateSpan();\n }\n\n getNormalization(value: number, weightByRange: boolean) {\n let normalization = getInterpolation(\n value,\n this.#range.min,\n this.#range.max,\n this.#range.span\n );\n if (weightByRange) {\n normalization *= this.#range.span;\n }\n return normalization || 0;\n }\n\n updateAndGetNormalization(value: number, weightByRange: boolean) {\n this.update(value);\n return this.getNormalization(value, weightByRange);\n }\n}\n\nexport default RangeHelper;\n","import RangeHelper from \"./RangeHelper.ts\";\n\nimport { Vector2 } from \"./MathUtils.ts\";\n\nexport type CenterOfPressure = Vector2;\n\nexport interface CenterOfPressureRange {\n x: RangeHelper;\n y: RangeHelper;\n}\n\nclass CenterOfPressureHelper {\n #range: CenterOfPressureRange = {\n x: new RangeHelper(),\n y: new RangeHelper(),\n };\n reset() {\n this.#range.x.reset();\n this.#range.y.reset();\n }\n\n update(centerOfPressure: CenterOfPressure) {\n this.#range.x.update(centerOfPressure.x);\n this.#range.y.update(centerOfPressure.y);\n }\n getNormalization(centerOfPressure: CenterOfPressure, weightByRange: boolean): CenterOfPressure {\n return {\n x: this.#range.x.getNormalization(centerOfPressure.x, weightByRange),\n y: this.#range.y.getNormalization(centerOfPressure.y, weightByRange),\n };\n }\n\n updateAndGetNormalization(centerOfPressure: CenterOfPressure, weightByRange: boolean) {\n this.update(centerOfPressure);\n return this.getNormalization(centerOfPressure, weightByRange);\n }\n}\n\nexport default CenterOfPressureHelper;\n","export function createArray(arrayLength: number, objectOrCallback: ((index: number) => any) | object) {\n return new Array(arrayLength).fill(1).map((_, index) => {\n if (typeof objectOrCallback == \"function\") {\n const callback = objectOrCallback;\n return callback(index);\n } else {\n const object = objectOrCallback;\n return Object.assign({}, object);\n }\n });\n}\n\nexport function arrayWithoutDuplicates(array: any[]) {\n return array.filter((value, index) => array.indexOf(value) == index);\n}\n","import { createConsole } from \"../utils/Console.ts\";\nimport CenterOfPressureHelper from \"../utils/CenterOfPressureHelper.ts\";\nimport RangeHelper from \"../utils/RangeHelper.ts\";\nimport { createArray } from \"../utils/ArrayUtils.ts\";\n\nconst _console = createConsole(\"PressureDataManager\", { log: false });\n\nexport const PressureSensorTypes = [\"pressure\"] as const;\nexport type PressureSensorType = (typeof PressureSensorTypes)[number];\n\nexport const ContinuousPressureSensorTypes = PressureSensorTypes;\nexport type ContinuousPressureSensorType =\n (typeof ContinuousPressureSensorTypes)[number];\n\nimport { computeVoronoiWeights, Vector2 } from \"../utils/MathUtils.ts\";\nexport type PressureSensorPosition = Vector2;\n\nimport { CenterOfPressure } from \"../utils/CenterOfPressureHelper.ts\";\n\nexport interface PressureSensorValue {\n position: PressureSensorPosition;\n rawValue: number;\n scaledValue: number;\n normalizedValue: number;\n weightedValue: number;\n}\n\nexport interface PressureData {\n sensors: PressureSensorValue[];\n scaledSum: number;\n normalizedSum: number;\n center?: CenterOfPressure;\n normalizedCenter?: CenterOfPressure;\n}\n\nexport interface PressureDataEventMessages {\n pressure: { pressure: PressureData };\n}\n\nexport const DefaultNumberOfPressureSensors = 8;\n\nclass PressureSensorDataManager {\n #positions: PressureSensorPosition[] = [];\n get positions() {\n return this.#positions;\n }\n\n get numberOfSensors() {\n return this.positions.length;\n }\n\n parsePositions(dataView: DataView) {\n const positions: PressureSensorPosition[] = [];\n\n for (\n let pressureSensorIndex = 0, byteOffset = 0;\n byteOffset < dataView.byteLength;\n pressureSensorIndex++, byteOffset += 2\n ) {\n positions.push({\n x: dataView.getUint8(byteOffset) / 2 ** 8,\n y: dataView.getUint8(byteOffset + 1) / 2 ** 8,\n });\n }\n\n _console.log({ positions });\n\n this.#positions = positions;\n\n this.#sensorRangeHelpers = createArray(\n this.numberOfSensors,\n () => new RangeHelper()\n );\n\n this.resetRange();\n }\n\n #sensorRangeHelpers!: RangeHelper[];\n #normalizedSumRangeHelper = new RangeHelper();\n\n #centerOfPressureHelper = new CenterOfPressureHelper();\n\n resetRange() {\n this.#sensorRangeHelpers?.forEach((rangeHelper) => rangeHelper.reset());\n this.#centerOfPressureHelper.reset();\n this.#normalizedSumRangeHelper.reset();\n }\n\n parseData(dataView: DataView, scalar: number) {\n const pressure: PressureData = {\n sensors: [],\n scaledSum: 0,\n normalizedSum: 0,\n };\n for (\n let index = 0, byteOffset = 0;\n byteOffset < dataView.byteLength;\n index++, byteOffset += 2\n ) {\n const rawValue = dataView.getUint16(byteOffset, true);\n let scaledValue = (rawValue * scalar) / this.numberOfSensors;\n const rangeHelper = this.#sensorRangeHelpers[index];\n const normalizedValue = rangeHelper.updateAndGetNormalization(\n scaledValue,\n false\n );\n //scaledValue -= rangeHelper.min;\n\n const position = this.positions[index];\n pressure.sensors[index] = {\n rawValue,\n scaledValue,\n normalizedValue,\n position,\n weightedValue: 0,\n };\n\n pressure.scaledSum += scaledValue;\n //pressure.normalizedSum += normalizedValue;\n }\n pressure.normalizedSum =\n this.#normalizedSumRangeHelper.updateAndGetNormalization(\n pressure.scaledSum,\n false\n );\n\n if (pressure.scaledSum > 0) {\n pressure.center = { x: 0, y: 0 };\n pressure.sensors.forEach((sensor) => {\n sensor.weightedValue = sensor.scaledValue / pressure.scaledSum;\n pressure.center!.x += sensor.position.x * sensor.weightedValue;\n pressure.center!.y += sensor.position.y * sensor.weightedValue;\n });\n pressure.normalizedCenter =\n this.#centerOfPressureHelper.updateAndGetNormalization(\n pressure.center,\n false\n );\n }\n\n _console.log({ pressure });\n return pressure;\n }\n}\n\nexport default PressureSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\n\nconst _console = createConsole(\"MotionSensorDataManager\", { log: false });\n\nexport const MotionSensorTypes = [\n \"acceleration\",\n \"gravity\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n \"gameRotation\",\n \"rotation\",\n \"orientation\",\n \"activity\",\n \"stepCounter\",\n \"stepDetector\",\n \"deviceOrientation\",\n \"tapDetector\",\n] as const;\nexport type MotionSensorType = (typeof MotionSensorTypes)[number];\n\nexport const ContinuousMotionTypes = [\n \"acceleration\",\n \"gravity\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n \"gameRotation\",\n \"rotation\",\n \"orientation\",\n] as const;\nexport type ContinuousMotionType = (typeof ContinuousMotionTypes)[number];\n\nimport { Vector3, Quaternion, Euler } from \"../utils/MathUtils.ts\";\nimport { ValueOf } from \"../utils/TypeScriptUtils.ts\";\n\nexport const Vector2Size = 2 * 2;\nexport const Vector3Size = 3 * 2;\nexport const QuaternionSize = 4 * 2;\n\nexport const ActivityTypes = [\n \"still\",\n \"walking\",\n \"running\",\n \"bicycle\",\n \"vehicle\",\n \"tilting\",\n] as const;\nexport type ActivityType = (typeof ActivityTypes)[number];\n\nexport interface Activity {\n still: boolean;\n walking: boolean;\n running: boolean;\n bicycle: boolean;\n vehicle: boolean;\n tilting: boolean;\n}\n\nexport const DeviceOrientations = [\n \"portraitUpright\",\n \"landscapeLeft\",\n \"portraitUpsideDown\",\n \"landscapeRight\",\n \"unknown\",\n] as const;\nexport type DeviceOrientation = (typeof DeviceOrientations)[number];\n\nexport interface MotionSensorDataEventMessages {\n acceleration: { acceleration: Vector3 };\n gravity: { gravity: Vector3 };\n linearAcceleration: { linearAcceleration: Vector3 };\n gyroscope: { gyroscope: Vector3 };\n magnetometer: { magnetometer: Vector3 };\n gameRotation: { gameRotation: Quaternion };\n rotation: { rotation: Quaternion };\n orientation: { orientation: Euler };\n stepDetector: { stepDetector: Object };\n stepCounter: { stepCounter: number };\n activity: { activity: Activity };\n deviceOrientation: { deviceOrientation: DeviceOrientation };\n tapDetector: { tapDetector: Object };\n}\n\nexport type MotionSensorDataEventMessage =\n ValueOf<MotionSensorDataEventMessages>;\n\nclass MotionSensorDataManager {\n parseVector3(dataView: DataView, scalar: number): Vector3 {\n let [x, y, z] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n ].map((value) => value * scalar);\n\n const vector: Vector3 = { x, y, z };\n\n _console.log({ vector });\n return vector;\n }\n\n parseQuaternion(dataView: DataView, scalar: number): Quaternion {\n let [x, y, z, w] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n dataView.getInt16(6, true),\n ].map((value) => value * scalar);\n\n const quaternion: Quaternion = { x, y, z, w };\n\n _console.log({ quaternion });\n return quaternion;\n }\n\n parseEuler(dataView: DataView, scalar: number): Euler {\n let [heading, pitch, roll] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n ].map((value) => value * scalar);\n\n pitch *= -1;\n heading *= -1;\n if (heading < 0) {\n heading += 360;\n }\n\n const euler: Euler = { heading, pitch, roll };\n\n _console.log({ euler });\n return euler;\n }\n\n parseStepCounter(dataView: DataView) {\n _console.log(\"parseStepCounter\", dataView);\n const stepCount = dataView.getUint32(0, true);\n _console.log({ stepCount });\n return stepCount;\n }\n\n parseActivity(dataView: DataView) {\n _console.log(\"parseActivity\", dataView);\n const activity: Partial<Activity> = {};\n\n const activityBitfield = dataView.getUint8(0);\n _console.log(\"activityBitfield\", activityBitfield.toString(2));\n ActivityTypes.forEach((activityType, index) => {\n activity[activityType] = Boolean(activityBitfield & (1 << index));\n });\n\n _console.log(\"activity\", activity);\n\n return activity as Activity;\n }\n\n parseDeviceOrientation(dataView: DataView) {\n _console.log(\"parseDeviceOrientation\", dataView);\n const index = dataView.getUint8(0);\n const deviceOrientation = DeviceOrientations[index];\n _console.assertWithError(deviceOrientation, \"undefined deviceOrientation\");\n _console.log({ deviceOrientation });\n return deviceOrientation;\n }\n}\n\nexport default MotionSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\n\nexport const BarometerSensorTypes = [\"barometer\"] as const;\nexport type BarometerSensorType = (typeof BarometerSensorTypes)[number];\n\nexport const ContinuousBarometerSensorTypes = BarometerSensorTypes;\nexport type ContinuousBarometerSensorType = (typeof ContinuousBarometerSensorTypes)[number];\n\nexport interface BarometerSensorDataEventMessages {\n barometer: {\n barometer: number;\n //altitude: number;\n };\n}\n\nconst _console = createConsole(\"BarometerSensorDataManager\", { log: false });\n\nclass BarometerSensorDataManager {\n #calculcateAltitude(pressure: number) {\n const P0 = 101325; // Standard atmospheric pressure at sea level in Pascals\n const T0 = 288.15; // Standard temperature at sea level in Kelvin\n const L = 0.0065; // Temperature lapse rate in K/m\n const R = 8.3144598; // Universal gas constant in J/(mol·K)\n const g = 9.80665; // Acceleration due to gravity in m/s²\n const M = 0.0289644; // Molar mass of Earth's air in kg/mol\n\n const exponent = (R * L) / (g * M);\n const h = (T0 / L) * (1 - Math.pow(pressure / P0, exponent));\n\n return h;\n }\n\n parseData(dataView: DataView, scalar: number) {\n const pressure = dataView.getUint32(0, true) * scalar;\n const altitude = this.#calculcateAltitude(pressure);\n _console.log({ pressure, altitude });\n return { pressure };\n }\n}\n\nexport default BarometerSensorDataManager;\n","import { sliceDataView } from \"./ArrayBufferUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { textDecoder } from \"./Text.ts\";\n\nconst _console = createConsole(\"ParseUtils\", { log: false });\n\nexport function parseStringFromDataView(\n dataView: DataView,\n byteOffset: number = 0\n) {\n const stringLength = dataView.getUint8(byteOffset++);\n const string = textDecoder.decode(\n dataView.buffer.slice(\n dataView.byteOffset + byteOffset,\n dataView.byteOffset + byteOffset + stringLength\n )\n );\n byteOffset += stringLength;\n return { string, byteOffset };\n}\n\nexport function parseMessage<MessageType extends string>(\n dataView: DataView,\n messageTypes: readonly MessageType[],\n callback: (\n messageType: MessageType,\n dataView: DataView,\n context?: any\n ) => void,\n context?: any,\n parseMessageLengthAsUint16: boolean = false\n) {\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const messageTypeEnum = dataView.getUint8(byteOffset++);\n _console.assertWithError(\n messageTypeEnum in messageTypes,\n `invalid messageTypeEnum ${messageTypeEnum}`\n );\n const messageType = messageTypes[messageTypeEnum];\n\n let messageLength: number;\n if (parseMessageLengthAsUint16) {\n messageLength = dataView.getUint16(byteOffset, true);\n byteOffset += 2;\n } else {\n messageLength = dataView.getUint8(byteOffset++);\n }\n\n _console.log({\n messageTypeEnum,\n messageType,\n messageLength,\n dataView,\n byteOffset,\n });\n\n const _dataView = sliceDataView(dataView, byteOffset, messageLength);\n _console.log({ _dataView });\n\n callback(messageType, _dataView, context);\n\n byteOffset += messageLength;\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInNode } from \"./utils/environment.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport { parseMessage } from \"./utils/ParseUtils.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\n\nconst _console = createConsole(\"CameraManager\", { log: false });\n\nexport const CameraSensorTypes = [\"camera\"] as const;\nexport type CameraSensorType = (typeof CameraSensorTypes)[number];\n\nexport const CameraCommands = [\n \"focus\",\n \"takePicture\",\n \"stop\",\n \"sleep\",\n \"wake\",\n] as const;\nexport type CameraCommand = (typeof CameraCommands)[number];\n\nexport const CameraStatuses = [\n \"idle\",\n \"focusing\",\n \"takingPicture\",\n \"asleep\",\n] as const;\nexport type CameraStatus = (typeof CameraStatuses)[number];\n\nexport const CameraDataTypes = [\n \"headerSize\",\n \"header\",\n \"imageSize\",\n \"image\",\n \"footerSize\",\n \"footer\",\n] as const;\nexport type CameraDataType = (typeof CameraDataTypes)[number];\n\nexport const CameraConfigurationTypes = [\n \"resolution\",\n \"qualityFactor\",\n \"shutter\",\n \"gain\",\n \"redGain\",\n \"greenGain\",\n \"blueGain\",\n] as const;\nexport type CameraConfigurationType = (typeof CameraConfigurationTypes)[number];\n\nexport const CameraMessageTypes = [\n \"cameraStatus\",\n \"cameraCommand\",\n \"getCameraConfiguration\",\n \"setCameraConfiguration\",\n \"cameraData\",\n] as const;\nexport type CameraMessageType = (typeof CameraMessageTypes)[number];\n\nexport type CameraConfiguration = {\n [cameraConfigurationType in CameraConfigurationType]?: number;\n};\nexport type CameraConfigurationRanges = {\n [cameraConfigurationType in CameraConfigurationType]: {\n min: number;\n max: number;\n };\n};\n\nexport const RequiredCameraMessageTypes: CameraMessageType[] = [\n \"getCameraConfiguration\",\n \"cameraStatus\",\n] as const;\n\nexport const CameraEventTypes = [\n ...CameraMessageTypes,\n \"cameraImageProgress\",\n \"cameraImage\",\n] as const;\nexport type CameraEventType = (typeof CameraEventTypes)[number];\n\nexport interface CameraEventMessages {\n cameraStatus: {\n cameraStatus: CameraStatus;\n previousCameraStatus: CameraStatus;\n };\n getCameraConfiguration: { cameraConfiguration: CameraConfiguration };\n cameraImageProgress: { progress: number; type: CameraDataType };\n cameraImage: { blob: Blob; url: string };\n}\n\nexport type CameraEventDispatcher = EventDispatcher<\n Device,\n CameraEventType,\n CameraEventMessages\n>;\nexport type SendCameraMessageCallback = SendMessageCallback<CameraMessageType>;\n\nclass CameraManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendCameraMessageCallback;\n\n eventDispatcher!: CameraEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required camera information\");\n const messages = RequiredCameraMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // CAMERA STATUS\n #cameraStatus!: CameraStatus;\n get cameraStatus() {\n return this.#cameraStatus;\n }\n #parseCameraStatus(dataView: DataView) {\n const cameraStatusIndex = dataView.getUint8(0);\n const newCameraStatus = CameraStatuses[cameraStatusIndex];\n this.#updateCameraStatus(newCameraStatus);\n }\n #updateCameraStatus(newCameraStatus: CameraStatus) {\n _console.assertEnumWithError(newCameraStatus, CameraStatuses);\n if (newCameraStatus == this.#cameraStatus) {\n _console.log(`redundant cameraStatus ${newCameraStatus}`);\n return;\n }\n const previousCameraStatus = this.#cameraStatus;\n this.#cameraStatus = newCameraStatus;\n _console.log(`updated cameraStatus to \"${this.cameraStatus}\"`);\n this.#dispatchEvent(\"cameraStatus\", {\n cameraStatus: this.cameraStatus,\n previousCameraStatus,\n });\n\n if (\n this.#cameraStatus != \"takingPicture\" &&\n this.#imageProgress > 0 &&\n !this.#didBuildImage\n ) {\n this.#buildImage();\n }\n }\n\n // CAMERA COMMAND\n async #sendCameraCommand(command: CameraCommand, sendImmediately?: boolean) {\n _console.assertEnumWithError(command, CameraCommands);\n _console.log(`sending camera command \"${command}\"`);\n\n const promise = this.waitForEvent(\"cameraStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = CameraCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"cameraCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsAsleep() {\n _console.assertWithError(\n this.#cameraStatus == \"asleep\",\n `camera is not asleep - currently ${this.#cameraStatus}`\n );\n }\n #assertIsAwake() {\n _console.assertWithError(\n this.#cameraStatus != \"asleep\",\n `camera is not awake - currently ${this.#cameraStatus}`\n );\n }\n async focus() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"focus\");\n }\n async takePicture() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"takePicture\");\n }\n async stop() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"stop\");\n }\n async sleep() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"sleep\");\n }\n async wake() {\n this.#assertIsAsleep();\n await this.#sendCameraCommand(\"wake\");\n }\n\n // CAMERA DATA\n #parseCameraData(dataView: DataView) {\n _console.log(\"parsing camera data\", dataView);\n parseMessage(\n dataView,\n CameraDataTypes,\n this.#onCameraData.bind(this),\n null,\n true\n );\n }\n #onCameraData(cameraDataType: CameraDataType, dataView: DataView) {\n _console.log({ cameraDataType, dataView });\n switch (cameraDataType) {\n case \"headerSize\":\n this.#headerSize = dataView.getUint16(0, true);\n _console.log({ headerSize: this.#headerSize });\n this.#headerData = undefined;\n this.#headerProgress == 0;\n break;\n case \"header\":\n this.#headerData = concatenateArrayBuffers(this.#headerData, dataView);\n _console.log({ headerData: this.#headerData });\n this.#headerProgress = this.#headerData?.byteLength / this.#headerSize;\n _console.log({ headerProgress: this.#headerProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#headerProgress,\n type: \"header\",\n });\n if (this.#headerProgress == 1) {\n _console.log(\"finished getting header data\");\n }\n break;\n case \"imageSize\":\n this.#imageSize = dataView.getUint16(0, true);\n _console.log({ imageSize: this.#imageSize });\n this.#imageData = undefined;\n this.#imageProgress == 0;\n this.#didBuildImage = false;\n break;\n case \"image\":\n this.#imageData = concatenateArrayBuffers(this.#imageData, dataView);\n _console.log({ imageData: this.#imageData });\n this.#imageProgress = this.#imageData?.byteLength / this.#imageSize;\n _console.log({ imageProgress: this.#imageProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#imageProgress,\n type: \"image\",\n });\n if (this.#imageProgress == 1) {\n _console.log(\"finished getting image data\");\n if (this.#headerProgress == 1 && this.#footerProgress == 1) {\n this.#buildImage();\n }\n }\n break;\n case \"footerSize\":\n this.#footerSize = dataView.getUint16(0, true);\n _console.log({ footerSize: this.#footerSize });\n this.#footerData = undefined;\n this.#footerProgress == 0;\n break;\n case \"footer\":\n this.#footerData = concatenateArrayBuffers(this.#footerData, dataView);\n _console.log({ footerData: this.#footerData });\n this.#footerProgress = this.#footerData?.byteLength / this.#footerSize;\n _console.log({ footerProgress: this.#footerProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#footerProgress,\n type: \"footer\",\n });\n if (this.#footerProgress == 1) {\n _console.log(\"finished getting footer data\");\n if (this.#imageProgress == 1) {\n this.#buildImage();\n }\n }\n break;\n }\n }\n\n #headerSize: number = 0;\n #headerData?: ArrayBuffer;\n #headerProgress: number = 0;\n\n #imageSize: number = 0;\n #imageData?: ArrayBuffer;\n #imageProgress: number = 0;\n\n #footerSize: number = 0;\n #footerData?: ArrayBuffer;\n #footerProgress: number = 0;\n\n #didBuildImage: boolean = false;\n #buildImage() {\n _console.log(\"building image...\");\n const imageData = concatenateArrayBuffers(\n this.#headerData,\n this.#imageData,\n this.#footerData\n );\n _console.log({ imageData });\n\n let blob = new Blob([imageData], { type: \"image/jpeg\" });\n _console.log(\"created blob\", blob);\n\n const url = URL.createObjectURL(blob);\n _console.log(\"created url\", url);\n\n this.#dispatchEvent(\"cameraImage\", { url, blob });\n\n this.#didBuildImage = true;\n }\n\n // CONFIG\n #cameraConfiguration: CameraConfiguration = {};\n get cameraConfiguration() {\n return this.#cameraConfiguration;\n }\n #availableCameraConfigurationTypes!: CameraConfigurationType[];\n get availableCameraConfigurationTypes() {\n return this.#availableCameraConfigurationTypes;\n }\n\n #cameraConfigurationRanges: CameraConfigurationRanges = {\n resolution: { min: 100, max: 720 },\n qualityFactor: { min: 15, max: 60 },\n shutter: { min: 4, max: 16383 },\n gain: { min: 1, max: 248 },\n redGain: { min: 0, max: 1023 },\n greenGain: { min: 0, max: 1023 },\n blueGain: { min: 0, max: 1023 },\n };\n get cameraConfigurationRanges() {\n return this.#cameraConfigurationRanges;\n }\n\n #parseCameraConfiguration(dataView: DataView) {\n const parsedCameraConfiguration: CameraConfiguration = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const cameraConfigurationTypeIndex = dataView.getUint8(byteOffset++);\n const cameraConfigurationType =\n CameraConfigurationTypes[cameraConfigurationTypeIndex];\n _console.assertWithError(\n cameraConfigurationType,\n `invalid cameraConfigurationTypeIndex ${cameraConfigurationTypeIndex}`\n );\n parsedCameraConfiguration[cameraConfigurationType] = dataView.getUint16(\n byteOffset,\n true\n );\n byteOffset += 2;\n }\n\n _console.log({ parsedCameraConfiguration });\n this.#availableCameraConfigurationTypes = Object.keys(\n parsedCameraConfiguration\n ) as CameraConfigurationType[];\n this.#cameraConfiguration = parsedCameraConfiguration;\n this.#dispatchEvent(\"getCameraConfiguration\", {\n cameraConfiguration: this.#cameraConfiguration,\n });\n }\n\n #isCameraConfigurationRedundant(cameraConfiguration: CameraConfiguration) {\n let cameraConfigurationTypes = Object.keys(\n cameraConfiguration\n ) as CameraConfigurationType[];\n return cameraConfigurationTypes.every((cameraConfigurationType) => {\n return (\n this.cameraConfiguration[cameraConfigurationType] ==\n cameraConfiguration[cameraConfigurationType]\n );\n });\n }\n async setCameraConfiguration(newCameraConfiguration: CameraConfiguration) {\n _console.log({ newCameraConfiguration });\n if (this.#isCameraConfigurationRedundant(newCameraConfiguration)) {\n _console.log(\"redundant camera configuration\");\n return;\n }\n const setCameraConfigurationData = this.#createData(newCameraConfiguration);\n _console.log({ setCameraConfigurationData });\n\n const promise = this.waitForEvent(\"getCameraConfiguration\");\n this.sendMessage([\n {\n type: \"setCameraConfiguration\",\n data: setCameraConfigurationData.buffer,\n },\n ]);\n await promise;\n }\n\n #assertAvailableCameraConfigurationType(\n cameraConfigurationType: CameraConfigurationType\n ) {\n _console.assertWithError(\n this.#availableCameraConfigurationTypes,\n \"must get initial cameraConfiguration\"\n );\n const isCameraConfigurationTypeAvailable =\n this.#availableCameraConfigurationTypes?.includes(\n cameraConfigurationType\n );\n _console.assertWithError(\n isCameraConfigurationTypeAvailable,\n `unavailable camera configuration type \"${cameraConfigurationType}\"`\n );\n return isCameraConfigurationTypeAvailable;\n }\n\n static AssertValidCameraConfigurationType(\n cameraConfigurationType: CameraConfigurationType\n ) {\n _console.assertEnumWithError(\n cameraConfigurationType,\n CameraConfigurationTypes\n );\n }\n static AssertValidCameraConfigurationTypeEnum(\n cameraConfigurationTypeEnum: number\n ) {\n _console.assertTypeWithError(cameraConfigurationTypeEnum, \"number\");\n _console.assertWithError(\n cameraConfigurationTypeEnum in CameraConfigurationTypes,\n `invalid cameraConfigurationTypeEnum ${cameraConfigurationTypeEnum}`\n );\n }\n\n #createData(cameraConfiguration: CameraConfiguration) {\n let cameraConfigurationTypes = Object.keys(\n cameraConfiguration\n ) as CameraConfigurationType[];\n cameraConfigurationTypes = cameraConfigurationTypes.filter(\n (cameraConfigurationType) =>\n this.#assertAvailableCameraConfigurationType(cameraConfigurationType)\n );\n\n const dataView = new DataView(\n new ArrayBuffer(cameraConfigurationTypes.length * 3)\n );\n cameraConfigurationTypes.forEach((cameraConfigurationType, index) => {\n CameraManager.AssertValidCameraConfigurationType(cameraConfigurationType);\n const cameraConfigurationTypeEnum = CameraConfigurationTypes.indexOf(\n cameraConfigurationType\n );\n dataView.setUint8(index * 3, cameraConfigurationTypeEnum);\n\n const value = cameraConfiguration[cameraConfigurationType]!;\n //this.#assertValidCameraConfigurationValue(cameraConfigurationType, value);\n dataView.setUint16(index * 3 + 1, value, true);\n });\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // MESSAGE\n parseMessage(messageType: CameraMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"cameraStatus\":\n this.#parseCameraStatus(dataView);\n break;\n case \"getCameraConfiguration\":\n case \"setCameraConfiguration\":\n this.#parseCameraConfiguration(dataView);\n break;\n case \"cameraData\":\n this.#parseCameraData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n // @ts-ignore\n this.#cameraStatus = undefined;\n this.#headerProgress = 0;\n this.#imageProgress = 0;\n this.#footerProgress = 0;\n }\n}\n\nexport default CameraManager;\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"AudioUtils\", { log: false });\n\nexport function float32ArrayToWav(\n audioData: Float32Array,\n sampleRate: number,\n numChannels: number\n): Blob {\n const wavBuffer = encodeWAV(audioData, sampleRate, numChannels);\n return new Blob([wavBuffer], { type: \"audio/wav\" });\n}\n\nfunction encodeWAV(\n interleaved: Float32Array,\n sampleRate: number,\n numChannels: number\n): ArrayBuffer {\n const buffer = new ArrayBuffer(44 + interleaved.length * 2); // 44 bytes for WAV header\n const view = new DataView(buffer);\n\n // RIFF identifier\n writeString(view, 0, \"RIFF\");\n // File length minus RIFF identifier length and file description length\n view.setUint32(4, 36 + interleaved.length * 2, true);\n // RIFF type\n writeString(view, 8, \"WAVE\");\n // Format chunk identifier\n writeString(view, 12, \"fmt \");\n // Format chunk length\n view.setUint32(16, 16, true);\n // Sample format (raw)\n view.setUint16(20, 1, true);\n // Channel count\n view.setUint16(22, numChannels, true);\n // Sample rate\n view.setUint32(24, sampleRate, true);\n // Byte rate (sample rate * block align)\n view.setUint32(28, sampleRate * numChannels * 2, true);\n // Block align (channel count * bytes per sample)\n view.setUint16(32, numChannels * 2, true);\n // Bits per sample\n view.setUint16(34, 16, true);\n // Data chunk identifier\n writeString(view, 36, \"data\");\n // Data chunk length\n view.setUint32(40, interleaved.length * 2, true);\n\n // Write interleaved audio data\n for (let i = 0; i < interleaved.length; i++) {\n view.setInt16(44 + i * 2, interleaved[i] * 0x7fff, true); // Convert float [-1, 1] to int16\n }\n\n return buffer;\n}\n\nexport function writeString(\n view: DataView,\n offset: number,\n string: string\n): void {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\nimport { float32ArrayToWav } from \"./utils/AudioUtils.ts\";\n\nconst _console = createConsole(\"MicrophoneManager\", { log: false });\n\nexport const MicrophoneSensorTypes = [\"microphone\"] as const;\nexport type MicrophoneSensorType = (typeof MicrophoneSensorTypes)[number];\n\nexport const MicrophoneCommands = [\"start\", \"stop\", \"vad\"] as const;\nexport type MicrophoneCommand = (typeof MicrophoneCommands)[number];\n\nexport const MicrophoneStatuses = [\"idle\", \"streaming\", \"vad\"] as const;\nexport type MicrophoneStatus = (typeof MicrophoneStatuses)[number];\n\nexport const MicrophoneConfigurationTypes = [\"sampleRate\", \"bitDepth\"] as const;\nexport type MicrophoneConfigurationType =\n (typeof MicrophoneConfigurationTypes)[number];\n\nexport const MicrophoneSampleRates = [\"8000\", \"16000\"] as const;\nexport type MicrophoneSampleRate = (typeof MicrophoneSampleRates)[number];\n\nexport const MicrophoneBitDepths = [\"8\", \"16\"] as const;\nexport type MicrophoneBitDepth = (typeof MicrophoneBitDepths)[number];\n\nexport const MicrophoneMessageTypes = [\n \"microphoneStatus\",\n \"microphoneCommand\",\n \"getMicrophoneConfiguration\",\n \"setMicrophoneConfiguration\",\n \"microphoneData\",\n] as const;\nexport type MicrophoneMessageType = (typeof MicrophoneMessageTypes)[number];\n\nexport type MicrophoneConfiguration = {\n sampleRate?: MicrophoneSampleRate;\n bitDepth?: MicrophoneBitDepth;\n};\n\nexport const MicrophoneConfigurationValues = {\n sampleRate: MicrophoneSampleRates,\n bitDepth: MicrophoneBitDepths,\n};\n\nexport const RequiredMicrophoneMessageTypes: MicrophoneMessageType[] = [\n \"getMicrophoneConfiguration\",\n \"microphoneStatus\",\n] as const;\n\nexport const MicrophoneEventTypes = [\n ...MicrophoneMessageTypes,\n \"isRecordingMicrophone\",\n \"microphoneRecording\",\n] as const;\nexport type MicrophoneEventType = (typeof MicrophoneEventTypes)[number];\n\nexport interface MicrophoneEventMessages {\n microphoneStatus: {\n microphoneStatus: MicrophoneStatus;\n previousMicrophoneStatus: MicrophoneStatus;\n };\n getMicrophoneConfiguration: {\n microphoneConfiguration: MicrophoneConfiguration;\n };\n microphoneData: {\n samples: Float32Array;\n sampleRate: MicrophoneSampleRate;\n bitDepth: MicrophoneBitDepth;\n };\n isRecordingMicrophone: {\n isRecordingMicrophone: boolean;\n };\n microphoneRecording: {\n samples: Float32Array;\n sampleRate: MicrophoneSampleRate;\n bitDepth: MicrophoneBitDepth;\n blob: Blob;\n url: string;\n };\n}\n\nexport type MicrophoneEventDispatcher = EventDispatcher<\n Device,\n MicrophoneEventType,\n MicrophoneEventMessages\n>;\nexport type SendMicrophoneMessageCallback =\n SendMessageCallback<MicrophoneMessageType>;\n\nclass MicrophoneManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendMicrophoneMessageCallback;\n\n eventDispatcher!: MicrophoneEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required microphone information\");\n const messages = RequiredMicrophoneMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // MICROPHONE STATUS\n #microphoneStatus!: MicrophoneStatus;\n get microphoneStatus() {\n return this.#microphoneStatus;\n }\n #parseMicrophoneStatus(dataView: DataView) {\n const microphoneStatusIndex = dataView.getUint8(0);\n const newMicrophoneStatus = MicrophoneStatuses[microphoneStatusIndex];\n this.#updateMicrophoneStatus(newMicrophoneStatus);\n }\n #updateMicrophoneStatus(newMicrophoneStatus: MicrophoneStatus) {\n _console.assertEnumWithError(newMicrophoneStatus, MicrophoneStatuses);\n if (newMicrophoneStatus == this.#microphoneStatus) {\n _console.log(`redundant microphoneStatus ${newMicrophoneStatus}`);\n return;\n }\n const previousMicrophoneStatus = this.#microphoneStatus;\n this.#microphoneStatus = newMicrophoneStatus;\n _console.log(`updated microphoneStatus to \"${this.microphoneStatus}\"`);\n this.#dispatchEvent(\"microphoneStatus\", {\n microphoneStatus: this.microphoneStatus,\n previousMicrophoneStatus,\n });\n }\n\n // MICROPHONE COMMAND\n async #sendMicrophoneCommand(\n command: MicrophoneCommand,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(command, MicrophoneCommands);\n _console.log(`sending microphone command \"${command}\"`);\n\n const promise = this.waitForEvent(\"microphoneStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = MicrophoneCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"microphoneCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsIdle() {\n _console.assertWithError(\n this.#microphoneStatus == \"idle\",\n `microphone is not idle - currently ${this.#microphoneStatus}`\n );\n }\n #assertIsNotIdle() {\n _console.assertWithError(\n this.#microphoneStatus != \"idle\",\n `microphone is idle`\n );\n }\n #assertIsStreaming() {\n _console.assertWithError(\n this.#microphoneStatus == \"streaming\",\n `microphone is not recording - currently ${this.#microphoneStatus}`\n );\n }\n\n async start() {\n await this.#sendMicrophoneCommand(\"start\");\n }\n async stop() {\n if (this.microphoneStatus == \"idle\") {\n _console.log(\"microphone is already idle\");\n return;\n }\n await this.#sendMicrophoneCommand(\"stop\");\n }\n async vad() {\n await this.#sendMicrophoneCommand(\"vad\");\n }\n async toggle() {\n switch (this.microphoneStatus) {\n case \"idle\":\n this.start();\n break;\n case \"streaming\":\n this.stop();\n break;\n }\n }\n\n // MICROPHONE DATA\n #assertValidBitDepth() {\n _console.assertEnumWithError(this.bitDepth!, MicrophoneBitDepths);\n }\n #fadeDuration = 0.001;\n #playbackTime = 0;\n #parseMicrophoneData(dataView: DataView) {\n this.#assertValidBitDepth();\n\n _console.log(\"parsing microphone data\", dataView);\n\n const numberOfSamples = dataView.byteLength / this.#bytesPerSample!;\n const samples = new Float32Array(numberOfSamples);\n\n for (let i = 0; i < numberOfSamples; i++) {\n let sample;\n switch (this.bitDepth) {\n case \"16\":\n sample = dataView.getInt16(i * 2, true);\n samples[i] = sample / 2 ** 15; // Normalize to [-1, 1]\n break;\n case \"8\":\n sample = dataView.getInt8(i);\n samples[i] = sample / 2 ** 7; // Normalize to [-1, 1]\n break;\n }\n }\n\n _console.log(\"samples\", samples);\n\n if (this.#isRecording && this.#microphoneRecordingData) {\n this.#microphoneRecordingData!.push(samples);\n }\n\n if (this.#audioContext) {\n if (this.#gainNode) {\n const audioBuffer = this.#audioContext.createBuffer(\n 1,\n samples.length,\n Number(this.sampleRate!)\n );\n audioBuffer.getChannelData(0).set(samples);\n\n const bufferSource = this.#audioContext.createBufferSource();\n bufferSource.buffer = audioBuffer;\n\n const channelData = audioBuffer.getChannelData(0);\n const sampleRate = Number(this.sampleRate!);\n\n for (let i = 0; i < this.#fadeDuration * sampleRate; i++) {\n channelData[i] *= i / (this.#fadeDuration * sampleRate);\n }\n\n for (\n let i = channelData.length - 1;\n i >= channelData.length - this.#fadeDuration * sampleRate;\n i--\n ) {\n channelData[i] *=\n (channelData.length - i) / (this.#fadeDuration * sampleRate);\n }\n\n bufferSource.connect(this.#gainNode!);\n\n if (this.#playbackTime < this.#audioContext.currentTime) {\n this.#playbackTime = this.#audioContext.currentTime;\n }\n bufferSource.start(this.#playbackTime);\n this.#playbackTime += audioBuffer.duration;\n }\n }\n\n this.#dispatchEvent(\"microphoneData\", {\n samples,\n sampleRate: this.sampleRate!,\n bitDepth: this.bitDepth!,\n });\n }\n get #bytesPerSample() {\n switch (this.bitDepth) {\n case \"8\":\n return 1;\n case \"16\":\n return 2;\n }\n }\n\n // CONFIG\n #microphoneConfiguration: MicrophoneConfiguration = {};\n get microphoneConfiguration() {\n return this.#microphoneConfiguration;\n }\n #availableMicrophoneConfigurationTypes!: MicrophoneConfigurationType[];\n get availableMicrophoneConfigurationTypes() {\n return this.#availableMicrophoneConfigurationTypes;\n }\n\n get bitDepth() {\n return this.#microphoneConfiguration.bitDepth;\n }\n get sampleRate() {\n return this.#microphoneConfiguration.sampleRate;\n }\n\n #parseMicrophoneConfiguration(dataView: DataView) {\n const parsedMicrophoneConfiguration: MicrophoneConfiguration = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const microphoneConfigurationTypeIndex = dataView.getUint8(byteOffset++);\n const microphoneConfigurationType =\n MicrophoneConfigurationTypes[microphoneConfigurationTypeIndex];\n _console.assertWithError(\n microphoneConfigurationType,\n `invalid microphoneConfigurationTypeIndex ${microphoneConfigurationTypeIndex}`\n );\n let rawValue = dataView.getUint8(byteOffset++);\n const values = MicrophoneConfigurationValues[microphoneConfigurationType];\n const value = values[rawValue];\n _console.assertEnumWithError(value, values);\n _console.log({ microphoneConfigurationType, value });\n // @ts-expect-error\n parsedMicrophoneConfiguration[microphoneConfigurationType] = value;\n }\n\n _console.log({ parsedMicrophoneConfiguration });\n this.#availableMicrophoneConfigurationTypes = Object.keys(\n parsedMicrophoneConfiguration\n ) as MicrophoneConfigurationType[];\n this.#microphoneConfiguration = parsedMicrophoneConfiguration;\n this.#dispatchEvent(\"getMicrophoneConfiguration\", {\n microphoneConfiguration: this.#microphoneConfiguration,\n });\n }\n\n #isMicrophoneConfigurationRedundant(\n microphoneConfiguration: MicrophoneConfiguration\n ) {\n let microphoneConfigurationTypes = Object.keys(\n microphoneConfiguration\n ) as MicrophoneConfigurationType[];\n return microphoneConfigurationTypes.every((microphoneConfigurationType) => {\n return (\n this.microphoneConfiguration[microphoneConfigurationType] ==\n microphoneConfiguration[microphoneConfigurationType]\n );\n });\n }\n async setMicrophoneConfiguration(\n newMicrophoneConfiguration: MicrophoneConfiguration\n ) {\n _console.log({ newMicrophoneConfiguration });\n if (this.#isMicrophoneConfigurationRedundant(newMicrophoneConfiguration)) {\n _console.log(\"redundant microphone configuration\");\n return;\n }\n const setMicrophoneConfigurationData = this.#createData(\n newMicrophoneConfiguration\n );\n _console.log({ setMicrophoneConfigurationData });\n\n const promise = this.waitForEvent(\"getMicrophoneConfiguration\");\n this.sendMessage([\n {\n type: \"setMicrophoneConfiguration\",\n data: setMicrophoneConfigurationData.buffer,\n },\n ]);\n await promise;\n }\n\n #assertAvailableMicrophoneConfigurationType(\n microphoneConfigurationType: MicrophoneConfigurationType\n ) {\n _console.assertWithError(\n this.#availableMicrophoneConfigurationTypes,\n \"must get initial microphoneConfiguration\"\n );\n const isMicrophoneConfigurationTypeAvailable =\n this.#availableMicrophoneConfigurationTypes?.includes(\n microphoneConfigurationType\n );\n _console.assertWithError(\n isMicrophoneConfigurationTypeAvailable,\n `unavailable microphone configuration type \"${microphoneConfigurationType}\"`\n );\n return isMicrophoneConfigurationTypeAvailable;\n }\n\n static AssertValidMicrophoneConfigurationType(\n microphoneConfigurationType: MicrophoneConfigurationType\n ) {\n _console.assertEnumWithError(\n microphoneConfigurationType,\n MicrophoneConfigurationTypes\n );\n }\n static AssertValidMicrophoneConfigurationTypeEnum(\n microphoneConfigurationTypeEnum: number\n ) {\n _console.assertTypeWithError(microphoneConfigurationTypeEnum, \"number\");\n _console.assertWithError(\n microphoneConfigurationTypeEnum in MicrophoneConfigurationTypes,\n `invalid microphoneConfigurationTypeEnum ${microphoneConfigurationTypeEnum}`\n );\n }\n\n #createData(microphoneConfiguration: MicrophoneConfiguration) {\n let microphoneConfigurationTypes = Object.keys(\n microphoneConfiguration\n ) as MicrophoneConfigurationType[];\n microphoneConfigurationTypes = microphoneConfigurationTypes.filter(\n (microphoneConfigurationType) =>\n this.#assertAvailableMicrophoneConfigurationType(\n microphoneConfigurationType\n )\n );\n\n const dataView = new DataView(\n new ArrayBuffer(microphoneConfigurationTypes.length * 2)\n );\n microphoneConfigurationTypes.forEach(\n (microphoneConfigurationType, index) => {\n MicrophoneManager.AssertValidMicrophoneConfigurationType(\n microphoneConfigurationType\n );\n const microphoneConfigurationTypeEnum =\n MicrophoneConfigurationTypes.indexOf(microphoneConfigurationType);\n dataView.setUint8(index * 2, microphoneConfigurationTypeEnum);\n\n let value = microphoneConfiguration[microphoneConfigurationType]!;\n if (typeof value == \"number\") {\n // @ts-ignore\n value = value.toString();\n }\n const values =\n MicrophoneConfigurationValues[microphoneConfigurationType];\n _console.assertEnumWithError(value, values);\n // @ts-expect-error\n const rawValue = values.indexOf(value);\n dataView.setUint8(index * 2 + 1, rawValue);\n }\n );\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // MESSAGE\n parseMessage(messageType: MicrophoneMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"microphoneStatus\":\n this.#parseMicrophoneStatus(dataView);\n break;\n case \"getMicrophoneConfiguration\":\n case \"setMicrophoneConfiguration\":\n this.#parseMicrophoneConfiguration(dataView);\n break;\n case \"microphoneData\":\n this.#parseMicrophoneData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n #audioContext?: AudioContext;\n get audioContext() {\n return this.#audioContext;\n }\n set audioContext(newAudioContext) {\n if (this.#audioContext == newAudioContext) {\n _console.log(\"redundant audioContext assignment\", this.#audioContext);\n return;\n }\n\n this.#audioContext = newAudioContext;\n\n _console.log(\"assigned new audioContext\", this.#audioContext);\n if (this.#audioContext) {\n this.#playbackTime = this.#audioContext.currentTime;\n } else {\n if (this.#mediaStreamDestination) {\n this.#mediaStreamDestination.disconnect();\n this.#mediaStreamDestination = undefined;\n }\n if (this.#gainNode) {\n this.#gainNode.disconnect();\n this.#gainNode = undefined;\n }\n }\n }\n\n #gainNode?: GainNode;\n get gainNode() {\n _console.assertWithError(\n this.#audioContext,\n \"audioContext assignment required for gainNode\"\n );\n if (!this.#gainNode) {\n _console.log(\"creating gainNode...\");\n this.#gainNode = this.#audioContext!.createGain();\n _console.log(\"created gainNode\", this.#gainNode);\n }\n return this.#gainNode;\n }\n\n #mediaStreamDestination?: MediaStreamAudioDestinationNode;\n get mediaStreamDestination() {\n _console.assertWithError(\n this.#audioContext,\n \"audioContext assignment required for mediaStreamDestination\"\n );\n if (!this.#mediaStreamDestination) {\n _console.log(\"creating mediaStreamDestination...\");\n this.#mediaStreamDestination =\n this.#audioContext!.createMediaStreamDestination();\n this.gainNode?.connect(this.#mediaStreamDestination);\n _console.log(\n \"created mediaStreamDestination\",\n this.#mediaStreamDestination\n );\n }\n return this.#mediaStreamDestination;\n }\n\n #isRecording = false;\n get isRecording() {\n return this.#isRecording;\n }\n #microphoneRecordingData?: Float32Array[];\n startRecording() {\n if (this.isRecording) {\n _console.log(\"already recording\");\n return;\n }\n this.#microphoneRecordingData = [];\n this.#isRecording = true;\n this.#dispatchEvent(\"isRecordingMicrophone\", {\n isRecordingMicrophone: this.isRecording,\n });\n }\n stopRecording() {\n if (!this.isRecording) {\n _console.log(\"already not recording\");\n return;\n }\n this.#isRecording = false;\n if (\n this.#microphoneRecordingData &&\n this.#microphoneRecordingData.length > 0\n ) {\n _console.log(\n \"parsing microphone data...\",\n this.#microphoneRecordingData.length\n );\n const arrayBuffer = concatenateArrayBuffers(\n ...this.#microphoneRecordingData\n );\n const samples = new Float32Array(arrayBuffer);\n\n const blob = float32ArrayToWav(samples, Number(this.sampleRate)!, 1);\n const url = URL.createObjectURL(blob);\n this.#dispatchEvent(\"microphoneRecording\", {\n samples,\n sampleRate: this.sampleRate!,\n bitDepth: this.bitDepth!,\n blob,\n url,\n });\n }\n this.#microphoneRecordingData = undefined;\n this.#dispatchEvent(\"isRecordingMicrophone\", {\n isRecordingMicrophone: this.isRecording,\n });\n }\n toggleRecording() {\n if (this.#isRecording) {\n this.stopRecording();\n } else {\n this.startRecording();\n }\n }\n\n clear() {\n // @ts-ignore\n this.#microphoneStatus = undefined;\n this.#microphoneConfiguration = {};\n if (this.isRecording) {\n this.stopRecording();\n }\n }\n}\n\nexport default MicrophoneManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport { parseTimestamp } from \"../utils/MathUtils.ts\";\nimport PressureSensorDataManager, {\n PressureDataEventMessages,\n} from \"./PressureSensorDataManager.ts\";\nimport MotionSensorDataManager, {\n MotionSensorDataEventMessages,\n} from \"./MotionSensorDataManager.ts\";\nimport BarometerSensorDataManager, {\n BarometerSensorDataEventMessages,\n} from \"./BarometerSensorDataManager.ts\";\nimport { parseMessage } from \"../utils/ParseUtils.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport {\n MotionSensorTypes,\n ContinuousMotionTypes,\n} from \"./MotionSensorDataManager.ts\";\nimport {\n PressureSensorTypes,\n ContinuousPressureSensorTypes,\n} from \"./PressureSensorDataManager.ts\";\nimport {\n BarometerSensorTypes,\n ContinuousBarometerSensorTypes,\n} from \"./BarometerSensorDataManager.ts\";\nimport Device from \"../Device.ts\";\nimport {\n AddKeysAsPropertyToInterface,\n ExtendInterfaceValues,\n ValueOf,\n} from \"../utils/TypeScriptUtils.ts\";\nimport { CameraSensorTypes } from \"../CameraManager.ts\";\nimport { MicrophoneSensorTypes } from \"../MicrophoneManager.ts\";\n\nconst _console = createConsole(\"SensorDataManager\", { log: false });\n\nexport const SensorTypes = [\n ...PressureSensorTypes,\n ...MotionSensorTypes,\n ...BarometerSensorTypes,\n ...CameraSensorTypes,\n ...MicrophoneSensorTypes,\n] as const;\nexport type SensorType = (typeof SensorTypes)[number];\n\nexport const ContinuousSensorTypes = [\n ...ContinuousPressureSensorTypes,\n ...ContinuousMotionTypes,\n ...ContinuousBarometerSensorTypes,\n] as const;\nexport type ContinuousSensorType = (typeof ContinuousSensorTypes)[number];\n\nexport const SensorDataMessageTypes = [\n \"getPressurePositions\",\n \"getSensorScalars\",\n \"sensorData\",\n] as const;\nexport type SensorDataMessageType = (typeof SensorDataMessageTypes)[number];\n\nexport const RequiredPressureMessageTypes: SensorDataMessageType[] = [\n \"getPressurePositions\",\n] as const;\n\nexport const SensorDataEventTypes = [\n ...SensorDataMessageTypes,\n ...SensorTypes,\n] as const;\nexport type SensorDataEventType = (typeof SensorDataEventTypes)[number];\n\ninterface BaseSensorDataEventMessage {\n timestamp: number;\n}\n\ntype BaseSensorDataEventMessages = BarometerSensorDataEventMessages &\n MotionSensorDataEventMessages &\n PressureDataEventMessages;\ntype _SensorDataEventMessages = ExtendInterfaceValues<\n AddKeysAsPropertyToInterface<BaseSensorDataEventMessages, \"sensorType\">,\n BaseSensorDataEventMessage\n>;\nexport type SensorDataEventMessage = ValueOf<_SensorDataEventMessages>;\ninterface AnySensorDataEventMessages {\n sensorData: SensorDataEventMessage;\n}\nexport type SensorDataEventMessages = _SensorDataEventMessages &\n AnySensorDataEventMessages;\n\nexport type SensorDataEventDispatcher = EventDispatcher<\n Device,\n SensorDataEventType,\n SensorDataEventMessages\n>;\n\nclass SensorDataManager {\n pressureSensorDataManager = new PressureSensorDataManager();\n motionSensorDataManager = new MotionSensorDataManager();\n barometerSensorDataManager = new BarometerSensorDataManager();\n\n #scalars: Map<SensorType, number> = new Map();\n\n static AssertValidSensorType(sensorType: SensorType) {\n _console.assertEnumWithError(sensorType, SensorTypes);\n }\n static AssertValidSensorTypeEnum(sensorTypeEnum: number) {\n _console.assertTypeWithError(sensorTypeEnum, \"number\");\n _console.assertWithError(\n sensorTypeEnum in SensorTypes,\n `invalid sensorTypeEnum ${sensorTypeEnum}`\n );\n }\n\n eventDispatcher!: SensorDataEventDispatcher;\n get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n parseMessage(messageType: SensorDataMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getSensorScalars\":\n this.parseScalars(dataView);\n break;\n case \"getPressurePositions\":\n this.pressureSensorDataManager.parsePositions(dataView);\n break;\n case \"sensorData\":\n this.parseData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n parseScalars(dataView: DataView) {\n for (\n let byteOffset = 0;\n byteOffset < dataView.byteLength;\n byteOffset += 5\n ) {\n const sensorTypeIndex = dataView.getUint8(byteOffset);\n const sensorType = SensorTypes[sensorTypeIndex];\n if (!sensorType) {\n _console.warn(`unknown sensorType index ${sensorTypeIndex}`);\n continue;\n }\n const sensorScalar = dataView.getFloat32(byteOffset + 1, true);\n _console.log({ sensorType, sensorScalar });\n this.#scalars.set(sensorType, sensorScalar);\n }\n }\n\n private parseData(dataView: DataView) {\n _console.log(\"sensorData\", Array.from(new Uint8Array(dataView.buffer)));\n\n let byteOffset = 0;\n const timestamp = parseTimestamp(dataView, byteOffset);\n byteOffset += 2;\n\n const _dataView = new DataView(dataView.buffer, byteOffset);\n\n parseMessage(_dataView, SensorTypes, this.parseDataCallback.bind(this), {\n timestamp,\n });\n }\n\n private parseDataCallback(\n sensorType: SensorType,\n dataView: DataView,\n { timestamp }: { timestamp: number }\n ) {\n const scalar = this.#scalars.get(sensorType) || 1;\n\n let sensorData = null;\n switch (sensorType) {\n case \"pressure\":\n sensorData = this.pressureSensorDataManager.parseData(dataView, scalar);\n break;\n case \"acceleration\":\n case \"gravity\":\n case \"linearAcceleration\":\n case \"gyroscope\":\n case \"magnetometer\":\n sensorData = this.motionSensorDataManager.parseVector3(\n dataView,\n scalar\n );\n break;\n case \"gameRotation\":\n case \"rotation\":\n sensorData = this.motionSensorDataManager.parseQuaternion(\n dataView,\n scalar\n );\n break;\n case \"orientation\":\n sensorData = this.motionSensorDataManager.parseEuler(dataView, scalar);\n break;\n case \"stepCounter\":\n sensorData = this.motionSensorDataManager.parseStepCounter(dataView);\n break;\n case \"stepDetector\":\n sensorData = {};\n break;\n case \"activity\":\n sensorData = this.motionSensorDataManager.parseActivity(dataView);\n break;\n case \"deviceOrientation\":\n sensorData =\n this.motionSensorDataManager.parseDeviceOrientation(dataView);\n break;\n case \"tapDetector\":\n sensorData = {};\n break;\n case \"barometer\":\n sensorData = this.barometerSensorDataManager.parseData(\n dataView,\n scalar\n );\n break;\n case \"camera\":\n // we parse camera data using CameraManager\n return;\n case \"microphone\":\n // we parse microphone data using MicrophoneManager\n return;\n default:\n _console.error(`uncaught sensorType \"${sensorType}\"`);\n }\n\n _console.assertWithError(\n sensorData != null,\n `no sensorData defined for sensorType \"${sensorType}\"`\n );\n\n _console.log({ sensorType, sensorData });\n // @ts-expect-error\n this.dispatchEvent(sensorType, {\n sensorType,\n [sensorType]: sensorData,\n timestamp,\n });\n // @ts-expect-error\n this.dispatchEvent(\"sensorData\", {\n sensorType,\n [sensorType]: sensorData,\n timestamp,\n });\n }\n}\n\nexport default SensorDataManager;\n","// Gets all non-builtin properties up the prototype chain.\nconst getAllProperties = object => {\n\tconst properties = new Set();\n\n\tdo {\n\t\tfor (const key of Reflect.ownKeys(object)) {\n\t\t\tproperties.add([object, key]);\n\t\t}\n\t} while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n\n\treturn properties;\n};\n\nexport default function autoBind(self, {include, exclude} = {}) {\n\tconst filter = key => {\n\t\tconst match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);\n\n\t\tif (include) {\n\t\t\treturn include.some(match); // eslint-disable-line unicorn/no-array-callback-reference\n\t\t}\n\n\t\tif (exclude) {\n\t\t\treturn !exclude.some(match); // eslint-disable-line unicorn/no-array-callback-reference\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tfor (const [object, key] of getAllProperties(self.constructor.prototype)) {\n\t\tif (key === 'constructor' || !filter(key)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n\t\tif (descriptor && typeof descriptor.value === 'function') {\n\t\t\tself[key] = self[key].bind(self);\n\t\t}\n\t}\n\n\treturn self;\n}\n","import { createConsole } from \"../utils/Console.ts\";\nimport SensorDataManager, {\n SensorTypes,\n SensorType,\n} from \"./SensorDataManager.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport Device, { SendMessageCallback } from \"../Device.ts\";\nimport autoBind from \"../../node_modules/auto-bind/index.js\";\n\nconst _console = createConsole(\"SensorConfigurationManager\", { log: false });\n\nexport type SensorConfiguration = { [sensorType in SensorType]?: number };\n\nexport const MaxSensorRate = 2 ** 16 - 1;\nexport const SensorRateStep = 5;\n\nexport const SensorConfigurationMessageTypes = [\n \"getSensorConfiguration\",\n \"setSensorConfiguration\",\n] as const;\nexport type SensorConfigurationMessageType =\n (typeof SensorConfigurationMessageTypes)[number];\n\nexport const SensorConfigurationEventTypes = SensorConfigurationMessageTypes;\nexport type SensorConfigurationEventType =\n (typeof SensorConfigurationEventTypes)[number];\n\nexport interface SensorConfigurationEventMessages {\n getSensorConfiguration: { sensorConfiguration: SensorConfiguration };\n}\n\nexport type SensorConfigurationEventDispatcher = EventDispatcher<\n Device,\n SensorConfigurationEventType,\n SensorConfigurationEventMessages\n>;\n\nexport type SendSensorConfigurationMessageCallback =\n SendMessageCallback<SensorConfigurationMessageType>;\n\nclass SensorConfigurationManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendSensorConfigurationMessageCallback;\n\n eventDispatcher!: SensorConfigurationEventDispatcher;\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #availableSensorTypes!: SensorType[];\n #assertAvailableSensorType(sensorType: SensorType) {\n _console.assertWithError(\n this.#availableSensorTypes,\n \"must get initial sensorConfiguration\"\n );\n const isSensorTypeAvailable =\n this.#availableSensorTypes?.includes(sensorType);\n _console.log(\n isSensorTypeAvailable,\n `unavailable sensor type \"${sensorType}\"`\n );\n return isSensorTypeAvailable;\n }\n\n #configuration: SensorConfiguration = {};\n get configuration() {\n return this.#configuration;\n }\n\n #updateConfiguration(updatedConfiguration: SensorConfiguration) {\n this.#configuration = updatedConfiguration;\n _console.log({ updatedConfiguration: this.#configuration });\n this.#dispatchEvent(\"getSensorConfiguration\", {\n sensorConfiguration: this.configuration,\n });\n }\n\n clear() {\n this.#updateConfiguration({});\n }\n\n #isRedundant(sensorConfiguration: SensorConfiguration) {\n let sensorTypes = Object.keys(sensorConfiguration) as SensorType[];\n return sensorTypes.every((sensorType) => {\n return this.configuration[sensorType] == sensorConfiguration[sensorType];\n });\n }\n\n async setConfiguration(\n newSensorConfiguration: SensorConfiguration,\n clearRest?: boolean,\n sendImmediately?: boolean\n ) {\n if (clearRest) {\n newSensorConfiguration = Object.assign(\n structuredClone(this.zeroSensorConfiguration),\n newSensorConfiguration\n );\n }\n _console.log({ newSensorConfiguration });\n if (this.#isRedundant(newSensorConfiguration)) {\n _console.log(\"redundant sensor configuration\");\n return;\n }\n const setSensorConfigurationData = this.#createData(newSensorConfiguration);\n _console.log({ setSensorConfigurationData });\n\n const promise = this.waitForEvent(\"getSensorConfiguration\");\n this.sendMessage(\n [\n {\n type: \"setSensorConfiguration\",\n data: setSensorConfigurationData.buffer,\n },\n ],\n sendImmediately\n );\n await promise;\n }\n\n #parse(dataView: DataView) {\n const parsedSensorConfiguration: SensorConfiguration = {};\n for (\n let byteOffset = 0;\n byteOffset < dataView.byteLength;\n byteOffset += 3\n ) {\n const sensorTypeIndex = dataView.getUint8(byteOffset);\n const sensorType = SensorTypes[sensorTypeIndex];\n\n const sensorRate = dataView.getUint16(byteOffset + 1, true);\n _console.log({ sensorType, sensorRate });\n\n if (!sensorType) {\n _console.warn(`unknown sensorType index ${sensorTypeIndex}`);\n continue;\n }\n parsedSensorConfiguration[sensorType] = sensorRate;\n }\n _console.log({ parsedSensorConfiguration });\n this.#availableSensorTypes = Object.keys(\n parsedSensorConfiguration\n ) as SensorType[];\n return parsedSensorConfiguration;\n }\n\n static #AssertValidSensorRate(sensorRate: number) {\n _console.assertTypeWithError(sensorRate, \"number\");\n _console.assertWithError(\n sensorRate >= 0,\n `sensorRate must be 0 or greater (got ${sensorRate})`\n );\n _console.assertWithError(\n sensorRate < MaxSensorRate,\n `sensorRate must be 0 or greater (got ${sensorRate})`\n );\n _console.assertWithError(\n sensorRate % SensorRateStep == 0,\n `sensorRate must be multiple of ${SensorRateStep}`\n );\n }\n\n #assertValidSensorRate(sensorRate: number) {\n SensorConfigurationManager.#AssertValidSensorRate(sensorRate);\n }\n\n #createData(sensorConfiguration: SensorConfiguration) {\n let sensorTypes = Object.keys(sensorConfiguration) as SensorType[];\n sensorTypes = sensorTypes.filter((sensorType) =>\n this.#assertAvailableSensorType(sensorType)\n );\n\n const dataView = new DataView(new ArrayBuffer(sensorTypes.length * 3));\n sensorTypes.forEach((sensorType, index) => {\n SensorDataManager.AssertValidSensorType(sensorType);\n const sensorTypeEnum = SensorTypes.indexOf(sensorType);\n dataView.setUint8(index * 3, sensorTypeEnum);\n\n const sensorRate = sensorConfiguration[sensorType]!;\n this.#assertValidSensorRate(sensorRate);\n dataView.setUint16(index * 3 + 1, sensorRate, true);\n });\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // ZERO\n static #ZeroSensorConfiguration: SensorConfiguration = {};\n static get ZeroSensorConfiguration() {\n return this.#ZeroSensorConfiguration;\n }\n static {\n SensorTypes.forEach((sensorType) => {\n this.#ZeroSensorConfiguration[sensorType] = 0;\n });\n }\n get zeroSensorConfiguration() {\n const zeroSensorConfiguration: SensorConfiguration = {};\n this.#availableSensorTypes.forEach((sensorType) => {\n zeroSensorConfiguration[sensorType] = 0;\n });\n return zeroSensorConfiguration;\n }\n async clearSensorConfiguration() {\n return this.setConfiguration(this.zeroSensorConfiguration);\n }\n\n // MESSAGE\n parseMessage(\n messageType: SensorConfigurationMessageType,\n dataView: DataView\n ) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getSensorConfiguration\":\n case \"setSensorConfiguration\":\n const newSensorConfiguration = this.#parse(dataView);\n this.#updateConfiguration(newSensorConfiguration);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default SensorConfigurationManager;\n","import { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport SensorDataManager, { SensorTypes } from \"./sensor/SensorDataManager.ts\";\nimport { arrayWithoutDuplicates } from \"./utils/ArrayUtils.ts\";\nimport { SensorRateStep } from \"./sensor/SensorConfigurationManager.ts\";\nimport { parseTimestamp } from \"./utils/MathUtils.ts\";\nimport { SensorType } from \"./sensor/SensorDataManager.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport autoBind from \"auto-bind\";\nimport { FileConfiguration as BaseFileConfiguration } from \"./FileTransferManager.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\n\nconst _console = createConsole(\"TfliteManager\", { log: false });\n\nexport const TfliteMessageTypes = [\n \"getTfliteName\",\n \"setTfliteName\",\n \"getTfliteTask\",\n \"setTfliteTask\",\n \"getTfliteSampleRate\",\n \"setTfliteSampleRate\",\n \"getTfliteSensorTypes\",\n \"setTfliteSensorTypes\",\n \"tfliteIsReady\",\n \"getTfliteCaptureDelay\",\n \"setTfliteCaptureDelay\",\n \"getTfliteThreshold\",\n \"setTfliteThreshold\",\n \"getTfliteInferencingEnabled\",\n \"setTfliteInferencingEnabled\",\n \"tfliteInference\",\n] as const;\nexport type TfliteMessageType = (typeof TfliteMessageTypes)[number];\n\nexport const TfliteEventTypes = TfliteMessageTypes;\nexport type TfliteEventType = (typeof TfliteEventTypes)[number];\n\nexport const RequiredTfliteMessageTypes: TfliteMessageType[] = [\n \"getTfliteName\",\n \"getTfliteTask\",\n \"getTfliteSampleRate\",\n \"getTfliteSensorTypes\",\n \"tfliteIsReady\",\n \"getTfliteCaptureDelay\",\n \"getTfliteThreshold\",\n \"getTfliteInferencingEnabled\",\n];\n\nexport const TfliteTasks = [\"classification\", \"regression\"] as const;\nexport type TfliteTask = (typeof TfliteTasks)[number];\n\nexport interface TfliteEventMessages {\n getTfliteName: { tfliteName: string };\n getTfliteTask: { tfliteTask: TfliteTask };\n getTfliteSampleRate: { tfliteSampleRate: number };\n getTfliteSensorTypes: { tfliteSensorTypes: SensorType[] };\n tfliteIsReady: { tfliteIsReady: boolean };\n getTfliteCaptureDelay: { tfliteCaptureDelay: number };\n getTfliteThreshold: { tfliteThreshold: number };\n getTfliteInferencingEnabled: { tfliteInferencingEnabled: boolean };\n tfliteInference: { tfliteInference: TfliteInference };\n}\n\nexport interface TfliteInference {\n timestamp: number;\n values: number[];\n maxValue?: number;\n maxIndex?: number;\n maxClass?: string;\n classValues?: { [key: string]: number };\n}\n\nexport type TfliteEventDispatcher = EventDispatcher<\n Device,\n TfliteEventType,\n TfliteEventMessages\n>;\nexport type SendTfliteMessageCallback = SendMessageCallback<TfliteMessageType>;\n\nexport const TfliteSensorTypes = [\n \"pressure\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n] as const satisfies readonly SensorType[];\nexport type TfliteSensorType = (typeof TfliteSensorTypes)[number];\n\nexport interface TfliteFileConfiguration extends BaseFileConfiguration {\n type: \"tflite\";\n name: string;\n sensorTypes: TfliteSensorType[];\n task: TfliteTask;\n sampleRate: number;\n captureDelay?: number;\n threshold?: number;\n classes?: string[];\n}\n\nclass TfliteManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendTfliteMessageCallback;\n\n #assertValidTask(task: TfliteTask) {\n _console.assertEnumWithError(task, TfliteTasks);\n }\n #assertValidTaskEnum(taskEnum: number) {\n _console.assertWithError(\n taskEnum in TfliteTasks,\n `invalid taskEnum ${taskEnum}`\n );\n }\n\n eventDispatcher!: TfliteEventDispatcher;\n get addEventListenter() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // PROPERTIES\n\n #name!: string;\n get name() {\n return this.#name;\n }\n #parseName(dataView: DataView) {\n _console.log(\"parseName\", dataView);\n const name = textDecoder.decode(dataView.buffer);\n this.#updateName(name);\n }\n #updateName(name: string) {\n _console.log({ name });\n this.#name = name;\n this.#dispatchEvent(\"getTfliteName\", { tfliteName: name });\n }\n async setName(newName: string, sendImmediately?: boolean) {\n _console.assertTypeWithError(newName, \"string\");\n if (this.name == newName) {\n _console.log(`redundant name assignment ${newName}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteName\");\n\n const setNameData = textEncoder.encode(newName);\n this.sendMessage(\n [{ type: \"setTfliteName\", data: setNameData.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #task!: TfliteTask;\n get task() {\n return this.#task;\n }\n #parseTask(dataView: DataView) {\n _console.log(\"parseTask\", dataView);\n const taskEnum = dataView.getUint8(0);\n this.#assertValidTaskEnum(taskEnum);\n const task = TfliteTasks[taskEnum];\n this.#updateTask(task);\n }\n #updateTask(task: TfliteTask) {\n _console.log({ task });\n this.#task = task;\n this.#dispatchEvent(\"getTfliteTask\", { tfliteTask: task });\n }\n async setTask(newTask: TfliteTask, sendImmediately?: boolean) {\n this.#assertValidTask(newTask);\n if (this.task == newTask) {\n _console.log(`redundant task assignment ${newTask}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteTask\");\n\n const taskEnum = TfliteTasks.indexOf(newTask);\n this.sendMessage(\n [{ type: \"setTfliteTask\", data: UInt8ByteBuffer(taskEnum) }],\n sendImmediately\n );\n\n await promise;\n }\n\n #sampleRate!: number;\n get sampleRate() {\n return this.#sampleRate;\n }\n #parseSampleRate(dataView: DataView) {\n _console.log(\"parseSampleRate\", dataView);\n const sampleRate = dataView.getUint16(0, true);\n this.#updateSampleRate(sampleRate);\n }\n #updateSampleRate(sampleRate: number) {\n _console.log({ sampleRate });\n this.#sampleRate = sampleRate;\n this.#dispatchEvent(\"getTfliteSampleRate\", {\n tfliteSampleRate: sampleRate,\n });\n }\n async setSampleRate(newSampleRate: number, sendImmediately?: boolean) {\n _console.assertTypeWithError(newSampleRate, \"number\");\n newSampleRate -= newSampleRate % SensorRateStep;\n _console.assertWithError(\n newSampleRate >= SensorRateStep,\n `sampleRate must be multiple of ${SensorRateStep} greater than 0 (got ${newSampleRate})`\n );\n if (this.#sampleRate == newSampleRate) {\n _console.log(`redundant sampleRate assignment ${newSampleRate}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteSampleRate\");\n\n const dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, newSampleRate, true);\n this.sendMessage(\n [{ type: \"setTfliteSampleRate\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n static AssertValidSensorType(sensorType: SensorType) {\n SensorDataManager.AssertValidSensorType(sensorType);\n const tfliteSensorType = sensorType as TfliteSensorType;\n _console.assertWithError(\n TfliteSensorTypes.includes(tfliteSensorType),\n `invalid tflite sensorType \"${sensorType}\"`\n );\n }\n\n #sensorTypes: TfliteSensorType[] = [];\n get sensorTypes() {\n return this.#sensorTypes.slice();\n }\n #parseSensorTypes(dataView: DataView) {\n _console.log(\"parseSensorTypes\", dataView);\n const sensorTypes: TfliteSensorType[] = [];\n for (let index = 0; index < dataView.byteLength; index++) {\n const sensorTypeEnum = dataView.getUint8(index);\n const sensorType = SensorTypes[sensorTypeEnum] as TfliteSensorType;\n if (sensorType) {\n if (TfliteSensorTypes.includes(sensorType)) {\n sensorTypes.push(sensorType);\n } else {\n _console.error(`invalid tfliteSensorType ${sensorType}`);\n }\n } else {\n _console.error(`invalid sensorTypeEnum ${sensorTypeEnum}`);\n }\n }\n this.#updateSensorTypes(sensorTypes);\n }\n #updateSensorTypes(sensorTypes: TfliteSensorType[]) {\n _console.log({ sensorTypes });\n this.#sensorTypes = sensorTypes;\n this.#dispatchEvent(\"getTfliteSensorTypes\", {\n tfliteSensorTypes: sensorTypes,\n });\n }\n async setSensorTypes(\n newSensorTypes: SensorType[],\n sendImmediately?: boolean\n ) {\n newSensorTypes.forEach((sensorType) => {\n TfliteManager.AssertValidSensorType(sensorType);\n });\n\n const promise = this.waitForEvent(\"getTfliteSensorTypes\");\n\n newSensorTypes = arrayWithoutDuplicates(newSensorTypes);\n const newSensorTypeEnums = newSensorTypes\n .map((sensorType) => SensorTypes.indexOf(sensorType))\n .sort();\n _console.log(newSensorTypes, newSensorTypeEnums);\n this.sendMessage(\n [\n {\n type: \"setTfliteSensorTypes\",\n data: Uint8Array.from(newSensorTypeEnums).buffer,\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n\n #isReady!: boolean;\n get isReady() {\n return this.#isReady;\n }\n #parseIsReady(dataView: DataView) {\n _console.log(\"parseIsReady\", dataView);\n const isReady = Boolean(dataView.getUint8(0));\n this.#updateIsReady(isReady);\n }\n #updateIsReady(isReady: boolean) {\n _console.log({ isReady });\n this.#isReady = isReady;\n this.#dispatchEvent(\"tfliteIsReady\", { tfliteIsReady: isReady });\n }\n #assertIsReady() {\n _console.assertWithError(this.isReady, `tflite is not ready`);\n }\n\n #captureDelay!: number;\n get captureDelay() {\n return this.#captureDelay;\n }\n #parseCaptureDelay(dataView: DataView) {\n _console.log(\"parseCaptureDelay\", dataView);\n const captureDelay = dataView.getUint16(0, true);\n this.#updateCaptueDelay(captureDelay);\n }\n #updateCaptueDelay(captureDelay: number) {\n _console.log({ captureDelay });\n this.#captureDelay = captureDelay;\n this.#dispatchEvent(\"getTfliteCaptureDelay\", {\n tfliteCaptureDelay: captureDelay,\n });\n }\n async setCaptureDelay(newCaptureDelay: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newCaptureDelay, \"number\");\n if (this.#captureDelay == newCaptureDelay) {\n _console.log(`redundant captureDelay assignment ${newCaptureDelay}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteCaptureDelay\");\n\n const dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, newCaptureDelay, true);\n this.sendMessage(\n [{ type: \"setTfliteCaptureDelay\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #threshold!: number;\n get threshold() {\n return this.#threshold;\n }\n #parseThreshold(dataView: DataView) {\n _console.log(\"parseThreshold\", dataView);\n const threshold = dataView.getFloat32(0, true);\n this.#updateThreshold(threshold);\n }\n #updateThreshold(threshold: number) {\n _console.log({ threshold });\n this.#threshold = threshold;\n this.#dispatchEvent(\"getTfliteThreshold\", { tfliteThreshold: threshold });\n }\n async setThreshold(newThreshold: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newThreshold, \"number\");\n _console.assertWithError(\n newThreshold >= 0,\n `threshold must be positive (got ${newThreshold})`\n );\n if (this.#threshold == newThreshold) {\n _console.log(`redundant threshold assignment ${newThreshold}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteThreshold\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setFloat32(0, newThreshold, true);\n this.sendMessage(\n [{ type: \"setTfliteThreshold\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #inferencingEnabled!: boolean;\n get inferencingEnabled() {\n return this.#inferencingEnabled;\n }\n #parseInferencingEnabled(dataView: DataView) {\n _console.log(\"parseInferencingEnabled\", dataView);\n const inferencingEnabled = Boolean(dataView.getUint8(0));\n this.#updateInferencingEnabled(inferencingEnabled);\n }\n #updateInferencingEnabled(inferencingEnabled: boolean) {\n _console.log({ inferencingEnabled });\n this.#inferencingEnabled = inferencingEnabled;\n this.#dispatchEvent(\"getTfliteInferencingEnabled\", {\n tfliteInferencingEnabled: inferencingEnabled,\n });\n }\n async setInferencingEnabled(\n newInferencingEnabled: boolean,\n sendImmediately: boolean = true\n ) {\n _console.assertTypeWithError(newInferencingEnabled, \"boolean\");\n if (!newInferencingEnabled && !this.isReady) {\n return;\n }\n this.#assertIsReady();\n if (this.#inferencingEnabled == newInferencingEnabled) {\n _console.log(\n `redundant inferencingEnabled assignment ${newInferencingEnabled}`\n );\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteInferencingEnabled\");\n\n this.sendMessage(\n [\n {\n type: \"setTfliteInferencingEnabled\",\n\n data: UInt8ByteBuffer(Number(newInferencingEnabled)),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n async toggleInferencingEnabled() {\n return this.setInferencingEnabled(!this.inferencingEnabled);\n }\n\n async enableInferencing() {\n if (this.inferencingEnabled) {\n return;\n }\n this.setInferencingEnabled(true);\n }\n async disableInferencing() {\n if (!this.inferencingEnabled) {\n return;\n }\n this.setInferencingEnabled(false);\n }\n\n #parseInference(dataView: DataView) {\n _console.log(\"parseInference\", dataView);\n\n const timestamp = parseTimestamp(dataView, 0);\n _console.log({ timestamp });\n\n const values: number[] = [];\n for (\n let index = 0, byteOffset = 2;\n byteOffset < dataView.byteLength;\n index++, byteOffset += 4\n ) {\n const value = dataView.getFloat32(byteOffset, true);\n values.push(value);\n }\n _console.log(\"values\", values);\n\n const inference: TfliteInference = {\n timestamp,\n values,\n };\n\n if (this.task == \"classification\") {\n let maxValue = 0;\n let maxIndex = 0;\n values.forEach((value, index) => {\n if (value > maxValue) {\n maxValue = value;\n maxIndex = index;\n }\n });\n _console.log({ maxIndex, maxValue });\n inference.maxIndex = maxIndex;\n inference.maxValue = maxValue;\n if (this.#configuration?.classes) {\n const { classes } = this.#configuration;\n inference.maxClass = classes[maxIndex];\n inference.classValues = {};\n values.forEach((value, index) => {\n const key = classes[index];\n inference.classValues![key] = value;\n });\n }\n }\n\n this.#dispatchEvent(\"tfliteInference\", { tfliteInference: inference });\n }\n\n parseMessage(messageType: TfliteMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getTfliteName\":\n case \"setTfliteName\":\n this.#parseName(dataView);\n break;\n case \"getTfliteTask\":\n case \"setTfliteTask\":\n this.#parseTask(dataView);\n break;\n case \"getTfliteSampleRate\":\n case \"setTfliteSampleRate\":\n this.#parseSampleRate(dataView);\n break;\n case \"getTfliteSensorTypes\":\n case \"setTfliteSensorTypes\":\n this.#parseSensorTypes(dataView);\n break;\n case \"tfliteIsReady\":\n this.#parseIsReady(dataView);\n break;\n case \"getTfliteCaptureDelay\":\n case \"setTfliteCaptureDelay\":\n this.#parseCaptureDelay(dataView);\n break;\n case \"getTfliteThreshold\":\n case \"setTfliteThreshold\":\n this.#parseThreshold(dataView);\n break;\n case \"getTfliteInferencingEnabled\":\n case \"setTfliteInferencingEnabled\":\n this.#parseInferencingEnabled(dataView);\n break;\n case \"tfliteInference\":\n this.#parseInference(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n #configuration?: TfliteFileConfiguration;\n get configuration() {\n return this.#configuration;\n }\n sendConfiguration(\n configuration: TfliteFileConfiguration,\n sendImmediately?: boolean\n ) {\n if (configuration == this.#configuration) {\n _console.log(\"redundant tflite configuration assignment\");\n return;\n }\n this.#configuration = configuration;\n _console.log(\"assigned new tflite configuration\", this.configuration);\n if (!this.configuration) {\n return;\n }\n const { name, task, captureDelay, sampleRate, threshold, sensorTypes } =\n this.configuration;\n this.setName(name, false);\n this.setTask(task, false);\n if (captureDelay != undefined) {\n this.setCaptureDelay(captureDelay, false);\n }\n this.setSampleRate(sampleRate, false);\n if (threshold != undefined) {\n this.setThreshold(threshold, false);\n }\n this.setSensorTypes(sensorTypes, sendImmediately);\n }\n\n clear() {\n this.#configuration = undefined;\n this.#inferencingEnabled = false;\n this.#sensorTypes = [];\n this.#sampleRate = 0;\n this.#isReady = false;\n // @ts-expect-error\n this.#name = undefined;\n // @ts-expect-error\n this.#task = undefined;\n // @ts-expect-error\n this.#sampleRate = undefined;\n this.#sensorTypes.length = 0;\n // @ts-expect-error\n this.#isReady = undefined;\n // @ts-expect-error\n this.#captureDelay = undefined;\n // @ts-expect-error\n this.#threshold = undefined;\n // @ts-expect-error\n this.#inferencingEnabled = undefined;\n this.#configuration = undefined;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required tflite information\");\n const messages = RequiredTfliteMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n}\n\nexport default TfliteManager;\n","import Device from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder } from \"./utils/Text.ts\";\n\nconst _console = createConsole(\"DeviceInformationManager\", { log: false });\n\nexport interface PnpId {\n source: \"Bluetooth\" | \"USB\";\n vendorId: number;\n productId: number;\n productVersion: number;\n}\n\nexport interface DeviceInformation {\n manufacturerName: string;\n modelNumber: string;\n softwareRevision: string;\n hardwareRevision: string;\n firmwareRevision: string;\n pnpId: PnpId;\n serialNumber: string;\n}\n\nexport const DeviceInformationTypes = [\n \"manufacturerName\",\n \"modelNumber\",\n \"hardwareRevision\",\n \"firmwareRevision\",\n \"softwareRevision\",\n \"pnpId\",\n \"serialNumber\",\n] as const;\nexport type DeviceInformationType = (typeof DeviceInformationTypes)[number];\n\nexport const DeviceInformationEventTypes = [\n ...DeviceInformationTypes,\n \"deviceInformation\",\n] as const;\nexport type DeviceInformationEventType =\n (typeof DeviceInformationEventTypes)[number];\n\nexport interface DeviceInformationEventMessages {\n manufacturerName: { manufacturerName: string };\n modelNumber: { modelNumber: string };\n softwareRevision: { softwareRevision: string };\n hardwareRevision: { hardwareRevision: string };\n firmwareRevision: { firmwareRevision: string };\n pnpId: { pnpId: PnpId };\n serialNumber: { serialNumber: string };\n deviceInformation: { deviceInformation: DeviceInformation };\n}\n\nexport type DeviceInformationEventDispatcher = EventDispatcher<\n Device,\n DeviceInformationEventType,\n DeviceInformationEventMessages\n>;\n\nclass DeviceInformationManager {\n eventDispatcher!: DeviceInformationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n #information: Partial<DeviceInformation> = {};\n get information() {\n return this.#information as DeviceInformation;\n }\n clear() {\n this.#information = {};\n }\n get #isComplete() {\n return DeviceInformationTypes.filter((key) => key != \"serialNumber\").every(\n (key) => key in this.#information\n );\n }\n\n #update(partialDeviceInformation: Partial<DeviceInformation>) {\n _console.log({ partialDeviceInformation });\n const deviceInformationNames = Object.keys(\n partialDeviceInformation\n ) as (keyof DeviceInformation)[];\n deviceInformationNames.forEach((deviceInformationName) => {\n // @ts-expect-error\n this.#dispatchEvent(deviceInformationName, {\n [deviceInformationName]:\n partialDeviceInformation[deviceInformationName],\n });\n });\n\n Object.assign(this.#information, partialDeviceInformation);\n _console.log({ deviceInformation: this.#information });\n if (this.#isComplete) {\n _console.log(\"completed deviceInformation\");\n this.#dispatchEvent(\"deviceInformation\", {\n deviceInformation: this.information,\n });\n }\n }\n\n parseMessage(messageType: DeviceInformationType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"manufacturerName\":\n const manufacturerName = textDecoder.decode(dataView.buffer);\n _console.log({ manufacturerName });\n this.#update({ manufacturerName });\n break;\n case \"modelNumber\":\n const modelNumber = textDecoder.decode(dataView.buffer);\n _console.log({ modelNumber });\n this.#update({ modelNumber });\n break;\n case \"softwareRevision\":\n const softwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ softwareRevision });\n this.#update({ softwareRevision });\n break;\n case \"hardwareRevision\":\n const hardwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ hardwareRevision });\n this.#update({ hardwareRevision });\n break;\n case \"firmwareRevision\":\n const firmwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ firmwareRevision });\n this.#update({ firmwareRevision });\n break;\n case \"pnpId\":\n const pnpId: PnpId = {\n source: dataView.getUint8(0) === 1 ? \"Bluetooth\" : \"USB\",\n productId: dataView.getUint16(3, true),\n productVersion: dataView.getUint16(5, true),\n vendorId: 0,\n };\n if (pnpId.source == \"Bluetooth\") {\n pnpId.vendorId = dataView.getUint16(1, true);\n } else {\n // no need to implement\n }\n _console.log({ pnpId });\n this.#update({ pnpId });\n break;\n case \"serialNumber\":\n const serialNumber = textDecoder.decode(dataView.buffer);\n _console.log({ serialNumber });\n // will only be used for node\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default DeviceInformationManager;\n","import { ConnectionType } from \"./connection/BaseConnectionManager.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { Uint16Max } from \"./utils/MathUtils.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"InformationManager\", { log: false });\n\nexport const DeviceTypes = [\n \"leftInsole\",\n \"rightInsole\",\n \"leftGlove\",\n \"rightGlove\",\n \"glasses\",\n \"generic\",\n] as const;\nexport type DeviceType = (typeof DeviceTypes)[number];\n\nexport const Sides = [\"left\", \"right\"] as const;\nexport type Side = (typeof Sides)[number];\n\nexport const MinNameLength = 2;\nexport const MaxNameLength = 30;\n\nexport const InformationMessageTypes = [\n \"isCharging\",\n \"getBatteryCurrent\",\n \"getMtu\",\n \"getId\",\n \"getName\",\n \"setName\",\n \"getType\",\n \"setType\",\n \"getCurrentTime\",\n \"setCurrentTime\",\n] as const;\nexport type InformationMessageType = (typeof InformationMessageTypes)[number];\n\nexport const InformationEventTypes = InformationMessageTypes;\nexport type InformationEventType = (typeof InformationEventTypes)[number];\n\nexport interface InformationEventMessages {\n isCharging: { isCharging: boolean };\n getBatteryCurrent: { batteryCurrent: number };\n getMtu: { mtu: number };\n getId: { id: string };\n getName: { name: string };\n getType: { type: DeviceType };\n getCurrentTime: { currentTime: number };\n}\n\nexport type InformationEventDispatcher = EventDispatcher<\n Device,\n InformationEventType,\n InformationEventMessages\n>;\nexport type SendInformationMessageCallback =\n SendMessageCallback<InformationMessageType>;\n\nclass InformationManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendInformationMessageCallback;\n\n eventDispatcher!: InformationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // PROPERTIES\n\n #isCharging = false;\n get isCharging() {\n return this.#isCharging;\n }\n #updateIsCharging(updatedIsCharging: boolean) {\n _console.assertTypeWithError(updatedIsCharging, \"boolean\");\n this.#isCharging = updatedIsCharging;\n _console.log({ isCharging: this.#isCharging });\n this.#dispatchEvent(\"isCharging\", { isCharging: this.#isCharging });\n }\n\n #batteryCurrent!: number;\n get batteryCurrent() {\n return this.#batteryCurrent;\n }\n async getBatteryCurrent() {\n _console.log(\"getting battery current...\");\n const promise = this.waitForEvent(\"getBatteryCurrent\");\n this.sendMessage([{ type: \"getBatteryCurrent\" }]);\n await promise;\n }\n #updateBatteryCurrent(updatedBatteryCurrent: number) {\n _console.assertTypeWithError(updatedBatteryCurrent, \"number\");\n this.#batteryCurrent = updatedBatteryCurrent;\n _console.log({ batteryCurrent: this.#batteryCurrent });\n this.#dispatchEvent(\"getBatteryCurrent\", {\n batteryCurrent: this.#batteryCurrent,\n });\n }\n\n #id!: string;\n get id() {\n return this.#id;\n }\n #updateId(updatedId: string) {\n _console.assertTypeWithError(updatedId, \"string\");\n this.#id = updatedId;\n _console.log({ id: this.#id });\n this.#dispatchEvent(\"getId\", { id: this.#id });\n }\n\n #name = \"\";\n get name() {\n return this.#name;\n }\n\n updateName(updatedName: string) {\n _console.assertTypeWithError(updatedName, \"string\");\n this.#name = updatedName;\n _console.log({ updatedName: this.#name });\n this.#dispatchEvent(\"getName\", { name: this.#name });\n }\n async setName(newName: string) {\n _console.assertTypeWithError(newName, \"string\");\n _console.assertRangeWithError(\n \"newName\",\n newName.length,\n MinNameLength,\n MaxNameLength\n );\n const setNameData = textEncoder.encode(newName);\n _console.log({ setNameData });\n\n const promise = this.waitForEvent(\"getName\");\n this.sendMessage([{ type: \"setName\", data: setNameData.buffer }]);\n await promise;\n }\n\n // TYPE\n #type!: DeviceType;\n get type() {\n return this.#type;\n }\n get typeEnum() {\n return DeviceTypes.indexOf(this.type);\n }\n #assertValidDeviceType(type: DeviceType) {\n _console.assertEnumWithError(type, DeviceTypes);\n }\n #assertValidDeviceTypeEnum(typeEnum: number) {\n _console.assertTypeWithError(typeEnum, \"number\");\n _console.assertWithError(\n typeEnum in DeviceTypes,\n `invalid typeEnum ${typeEnum}`\n );\n }\n updateType(updatedType: DeviceType) {\n this.#assertValidDeviceType(updatedType);\n // if (updatedType == this.type) {\n // _console.log(\"redundant type assignment\");\n // return;\n // }\n this.#type = updatedType;\n _console.log({ updatedType: this.#type });\n\n this.#dispatchEvent(\"getType\", { type: this.#type });\n }\n async #setTypeEnum(newTypeEnum: number) {\n this.#assertValidDeviceTypeEnum(newTypeEnum);\n\n const setTypeData = UInt8ByteBuffer(newTypeEnum);\n _console.log({ setTypeData });\n const promise = this.waitForEvent(\"getType\");\n this.sendMessage([{ type: \"setType\", data: setTypeData }]);\n await promise;\n }\n async setType(newType: DeviceType) {\n this.#assertValidDeviceType(newType);\n const newTypeEnum = DeviceTypes.indexOf(newType);\n this.#setTypeEnum(newTypeEnum);\n }\n\n get isInsole() {\n switch (this.type) {\n case \"leftInsole\":\n case \"rightInsole\":\n return true;\n default:\n return false;\n }\n }\n\n get isGlove() {\n switch (this.type) {\n case \"leftGlove\":\n case \"rightGlove\":\n return true;\n default:\n return false;\n }\n }\n\n get side(): Side {\n switch (this.type) {\n case \"leftInsole\":\n case \"leftGlove\":\n return \"left\";\n case \"rightInsole\":\n case \"rightGlove\":\n return \"right\";\n default:\n return \"left\";\n }\n }\n\n #mtu = 0;\n get mtu() {\n return this.#mtu;\n }\n #updateMtu(newMtu: number) {\n _console.assertTypeWithError(newMtu, \"number\");\n if (this.#mtu == newMtu) {\n _console.log(\"redundant mtu assignment\", newMtu);\n return;\n }\n this.#mtu = newMtu;\n\n this.#dispatchEvent(\"getMtu\", { mtu: this.#mtu });\n }\n\n #isCurrentTimeSet = false;\n get isCurrentTimeSet() {\n return this.#isCurrentTimeSet;\n }\n\n #onCurrentTime(currentTime: number) {\n _console.log({ currentTime });\n this.#isCurrentTimeSet =\n currentTime != 0 || Math.abs(Date.now() - currentTime) < Uint16Max;\n if (!this.#isCurrentTimeSet) {\n this.#setCurrentTime(false);\n }\n }\n async #setCurrentTime(sendImmediately?: boolean) {\n _console.log(\"setting current time...\");\n const dataView = new DataView(new ArrayBuffer(8));\n dataView.setBigUint64(0, BigInt(Date.now()), true);\n const promise = this.waitForEvent(\"getCurrentTime\");\n this.sendMessage(\n [{ type: \"setCurrentTime\", data: dataView.buffer }],\n sendImmediately\n );\n await promise;\n }\n\n // MESSAGE\n parseMessage(messageType: InformationMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"isCharging\":\n const isCharging = Boolean(dataView.getUint8(0));\n _console.log({ isCharging });\n this.#updateIsCharging(isCharging);\n break;\n case \"getBatteryCurrent\":\n const batteryCurrent = dataView.getFloat32(0, true);\n _console.log({ batteryCurrent });\n this.#updateBatteryCurrent(batteryCurrent);\n break;\n case \"getId\":\n const id = textDecoder.decode(dataView.buffer);\n _console.log({ id });\n this.#updateId(id);\n break;\n case \"getName\":\n case \"setName\":\n const name = textDecoder.decode(dataView.buffer);\n _console.log({ name });\n this.updateName(name);\n break;\n case \"getType\":\n case \"setType\":\n const typeEnum = dataView.getUint8(0);\n const type = DeviceTypes[typeEnum];\n _console.log({ typeEnum, type });\n this.updateType(type);\n break;\n case \"getMtu\":\n let mtu = dataView.getUint16(0, true);\n if (\n this.connectionType != \"webSocket\" &&\n this.connectionType != \"udp\"\n ) {\n mtu = Math.min(mtu, 512);\n }\n _console.log({ mtu });\n this.#updateMtu(mtu);\n break;\n case \"getCurrentTime\":\n case \"setCurrentTime\":\n const currentTime = Number(dataView.getBigUint64(0, true));\n this.#onCurrentTime(currentTime);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n this.#isCurrentTimeSet = false;\n this.#mtu = 0;\n }\n\n connectionType?: ConnectionType;\n}\n\nexport default InformationManager;\n","export const VibrationWaveformEffects = [\n \"none\",\n \"strongClick100\",\n \"strongClick60\",\n \"strongClick30\",\n \"sharpClick100\",\n \"sharpClick60\",\n \"sharpClick30\",\n \"softBump100\",\n \"softBump60\",\n \"softBump30\",\n \"doubleClick100\",\n \"doubleClick60\",\n \"tripleClick100\",\n \"softFuzz60\",\n \"strongBuzz100\",\n \"alert750ms\",\n \"alert1000ms\",\n \"strongClick1_100\",\n \"strongClick2_80\",\n \"strongClick3_60\",\n \"strongClick4_30\",\n \"mediumClick100\",\n \"mediumClick80\",\n \"mediumClick60\",\n \"sharpTick100\",\n \"sharpTick80\",\n \"sharpTick60\",\n \"shortDoubleClickStrong100\",\n \"shortDoubleClickStrong80\",\n \"shortDoubleClickStrong60\",\n \"shortDoubleClickStrong30\",\n \"shortDoubleClickMedium100\",\n \"shortDoubleClickMedium80\",\n \"shortDoubleClickMedium60\",\n \"shortDoubleSharpTick100\",\n \"shortDoubleSharpTick80\",\n \"shortDoubleSharpTick60\",\n \"longDoubleSharpClickStrong100\",\n \"longDoubleSharpClickStrong80\",\n \"longDoubleSharpClickStrong60\",\n \"longDoubleSharpClickStrong30\",\n \"longDoubleSharpClickMedium100\",\n \"longDoubleSharpClickMedium80\",\n \"longDoubleSharpClickMedium60\",\n \"longDoubleSharpTick100\",\n \"longDoubleSharpTick80\",\n \"longDoubleSharpTick60\",\n \"buzz100\",\n \"buzz80\",\n \"buzz60\",\n \"buzz40\",\n \"buzz20\",\n \"pulsingStrong100\",\n \"pulsingStrong60\",\n \"pulsingMedium100\",\n \"pulsingMedium60\",\n \"pulsingSharp100\",\n \"pulsingSharp60\",\n \"transitionClick100\",\n \"transitionClick80\",\n \"transitionClick60\",\n \"transitionClick40\",\n \"transitionClick20\",\n \"transitionClick10\",\n \"transitionHum100\",\n \"transitionHum80\",\n \"transitionHum60\",\n \"transitionHum40\",\n \"transitionHum20\",\n \"transitionHum10\",\n \"transitionRampDownLongSmooth2_100\",\n \"transitionRampDownLongSmooth1_100\",\n \"transitionRampDownMediumSmooth1_100\",\n \"transitionRampDownMediumSmooth2_100\",\n \"transitionRampDownShortSmooth1_100\",\n \"transitionRampDownShortSmooth2_100\",\n \"transitionRampDownLongSharp1_100\",\n \"transitionRampDownLongSharp2_100\",\n \"transitionRampDownMediumSharp1_100\",\n \"transitionRampDownMediumSharp2_100\",\n \"transitionRampDownShortSharp1_100\",\n \"transitionRampDownShortSharp2_100\",\n \"transitionRampUpLongSmooth1_100\",\n \"transitionRampUpLongSmooth2_100\",\n \"transitionRampUpMediumSmooth1_100\",\n \"transitionRampUpMediumSmooth2_100\",\n \"transitionRampUpShortSmooth1_100\",\n \"transitionRampUpShortSmooth2_100\",\n \"transitionRampUpLongSharp1_100\",\n \"transitionRampUpLongSharp2_100\",\n \"transitionRampUpMediumSharp1_100\",\n \"transitionRampUpMediumSharp2_100\",\n \"transitionRampUpShortSharp1_100\",\n \"transitionRampUpShortSharp2_100\",\n \"transitionRampDownLongSmooth1_50\",\n \"transitionRampDownLongSmooth2_50\",\n \"transitionRampDownMediumSmooth1_50\",\n \"transitionRampDownMediumSmooth2_50\",\n \"transitionRampDownShortSmooth1_50\",\n \"transitionRampDownShortSmooth2_50\",\n \"transitionRampDownLongSharp1_50\",\n \"transitionRampDownLongSharp2_50\",\n \"transitionRampDownMediumSharp1_50\",\n \"transitionRampDownMediumSharp2_50\",\n \"transitionRampDownShortSharp1_50\",\n \"transitionRampDownShortSharp2_50\",\n \"transitionRampUpLongSmooth1_50\",\n \"transitionRampUpLongSmooth2_50\",\n \"transitionRampUpMediumSmooth1_50\",\n \"transitionRampUpMediumSmooth2_50\",\n \"transitionRampUpShortSmooth1_50\",\n \"transitionRampUpShortSmooth2_50\",\n \"transitionRampUpLongSharp1_50\",\n \"transitionRampUpLongSharp2_50\",\n \"transitionRampUpMediumSharp1_50\",\n \"transitionRampUpMediumSharp2_50\",\n \"transitionRampUpShortSharp1_50\",\n \"transitionRampUpShortSharp2_50\",\n \"longBuzz100\",\n \"smoothHum50\",\n \"smoothHum40\",\n \"smoothHum30\",\n \"smoothHum20\",\n \"smoothHum10\",\n] as const;\n\nexport type VibrationWaveformEffect = (typeof VibrationWaveformEffects)[number];\n","import { createConsole } from \"../utils/Console.ts\";\nimport {\n VibrationWaveformEffect,\n VibrationWaveformEffects,\n} from \"./VibrationWaveformEffects.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport Device, { SendMessageCallback } from \"../Device.ts\";\nimport autoBind from \"auto-bind\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\n\nconst _console = createConsole(\"VibrationManager\", { log: false });\n\nexport const VibrationLocations = [\"front\", \"rear\"] as const;\nexport type VibrationLocation = (typeof VibrationLocations)[number];\n\nexport const VibrationTypes = [\"waveformEffect\", \"waveform\"] as const;\nexport type VibrationType = (typeof VibrationTypes)[number];\n\nexport interface VibrationWaveformEffectSegment {\n effect?: VibrationWaveformEffect;\n delay?: number;\n loopCount?: number;\n}\n\nexport interface VibrationWaveformSegment {\n duration: number;\n amplitude: number;\n}\n\nexport const VibrationMessageTypes = [\n \"getVibrationLocations\",\n \"triggerVibration\",\n] as const;\nexport type VibrationMessageType = (typeof VibrationMessageTypes)[number];\n\nexport const VibrationEventTypes = VibrationMessageTypes;\nexport type VibrationEventType = (typeof VibrationEventTypes)[number];\n\nexport interface VibrationEventMessages {\n getVibrationLocations: { vibrationLocations: VibrationLocation[] };\n}\n\nexport const MaxNumberOfVibrationWaveformEffectSegments = 8;\nexport const MaxVibrationWaveformSegmentDuration = 2550;\nexport const MaxVibrationWaveformEffectSegmentDelay = 1270;\nexport const MaxVibrationWaveformEffectSegmentLoopCount = 3;\nexport const MaxNumberOfVibrationWaveformSegments = 20;\nexport const MaxVibrationWaveformEffectSequenceLoopCount = 6;\n\ninterface BaseVibrationConfiguration {\n type: VibrationType;\n locations?: VibrationLocation[];\n}\n\nexport interface VibrationWaveformEffectConfiguration\n extends BaseVibrationConfiguration {\n type: \"waveformEffect\";\n segments: VibrationWaveformEffectSegment[];\n loopCount?: number;\n}\n\nexport interface VibrationWaveformConfiguration\n extends BaseVibrationConfiguration {\n type: \"waveform\";\n segments: VibrationWaveformSegment[];\n}\n\nexport type VibrationConfiguration =\n | VibrationWaveformEffectConfiguration\n | VibrationWaveformConfiguration;\n\nexport type SendVibrationMessageCallback =\n SendMessageCallback<VibrationMessageType>;\n\nexport type VibrationEventDispatcher = EventDispatcher<\n Device,\n VibrationEventType,\n VibrationEventMessages\n>;\n\nclass VibrationManager {\n constructor() {\n autoBind(this);\n }\n sendMessage!: SendVibrationMessageCallback;\n\n eventDispatcher!: VibrationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #verifyLocation(location: VibrationLocation) {\n _console.assertTypeWithError(location, \"string\");\n _console.assertWithError(\n VibrationLocations.includes(location),\n `invalid location \"${location}\"`\n );\n }\n #verifyLocations(locations: VibrationLocation[]) {\n this.#assertNonEmptyArray(locations);\n locations.forEach((location) => {\n this.#verifyLocation(location);\n });\n }\n #createLocationsBitmask(locations: VibrationLocation[]) {\n this.#verifyLocations(locations);\n\n let locationsBitmask = 0;\n locations.forEach((location) => {\n const locationIndex = VibrationLocations.indexOf(location);\n locationsBitmask |= 1 << locationIndex;\n });\n _console.log({ locationsBitmask });\n _console.assertWithError(\n locationsBitmask > 0,\n `locationsBitmask must not be zero`\n );\n return locationsBitmask;\n }\n\n #assertNonEmptyArray(array: any[]) {\n _console.assertWithError(Array.isArray(array), \"passed non-array\");\n _console.assertWithError(array.length > 0, \"passed empty array\");\n }\n\n #verifyWaveformEffect(waveformEffect: VibrationWaveformEffect) {\n _console.assertWithError(\n VibrationWaveformEffects.includes(waveformEffect),\n `invalid waveformEffect \"${waveformEffect}\"`\n );\n }\n\n #verifyWaveformEffectSegment(\n waveformEffectSegment: VibrationWaveformEffectSegment\n ) {\n if (waveformEffectSegment.effect != undefined) {\n const waveformEffect = waveformEffectSegment.effect;\n this.#verifyWaveformEffect(waveformEffect);\n } else if (waveformEffectSegment.delay != undefined) {\n const { delay } = waveformEffectSegment;\n _console.assertWithError(\n delay >= 0,\n `delay must be 0ms or greater (got ${delay})`\n );\n _console.assertWithError(\n delay <= MaxVibrationWaveformEffectSegmentDelay,\n `delay must be ${MaxVibrationWaveformEffectSegmentDelay}ms or less (got ${delay})`\n );\n } else {\n throw Error(\"no effect or delay found in waveformEffectSegment\");\n }\n\n if (waveformEffectSegment.loopCount != undefined) {\n const { loopCount } = waveformEffectSegment;\n this.#verifyWaveformEffectSegmentLoopCount(loopCount);\n }\n }\n\n #verifyWaveformEffectSegmentLoopCount(\n waveformEffectSegmentLoopCount: number\n ) {\n _console.assertTypeWithError(waveformEffectSegmentLoopCount, \"number\");\n _console.assertWithError(\n waveformEffectSegmentLoopCount >= 0,\n `waveformEffectSegmentLoopCount must be 0 or greater (got ${waveformEffectSegmentLoopCount})`\n );\n _console.assertWithError(\n waveformEffectSegmentLoopCount <=\n MaxVibrationWaveformEffectSegmentLoopCount,\n `waveformEffectSegmentLoopCount must be ${MaxVibrationWaveformEffectSegmentLoopCount} or fewer (got ${waveformEffectSegmentLoopCount})`\n );\n }\n\n #verifyWaveformEffectSegments(\n waveformEffectSegments: VibrationWaveformEffectSegment[]\n ) {\n this.#assertNonEmptyArray(waveformEffectSegments);\n _console.assertWithError(\n waveformEffectSegments.length <=\n MaxNumberOfVibrationWaveformEffectSegments,\n `must have ${MaxNumberOfVibrationWaveformEffectSegments} waveformEffectSegments or fewer (got ${waveformEffectSegments.length})`\n );\n waveformEffectSegments.forEach((waveformEffectSegment) => {\n this.#verifyWaveformEffectSegment(waveformEffectSegment);\n });\n }\n\n #verifyWaveformEffectSequenceLoopCount(\n waveformEffectSequenceLoopCount: number\n ) {\n _console.assertTypeWithError(waveformEffectSequenceLoopCount, \"number\");\n _console.assertWithError(\n waveformEffectSequenceLoopCount >= 0,\n `waveformEffectSequenceLoopCount must be 0 or greater (got ${waveformEffectSequenceLoopCount})`\n );\n _console.assertWithError(\n waveformEffectSequenceLoopCount <=\n MaxVibrationWaveformEffectSequenceLoopCount,\n `waveformEffectSequenceLoopCount must be ${MaxVibrationWaveformEffectSequenceLoopCount} or fewer (got ${waveformEffectSequenceLoopCount})`\n );\n }\n\n #verifyWaveformSegment(waveformSegment: VibrationWaveformSegment) {\n _console.assertTypeWithError(waveformSegment.amplitude, \"number\");\n _console.assertWithError(\n waveformSegment.amplitude >= 0,\n `amplitude must be 0 or greater (got ${waveformSegment.amplitude})`\n );\n _console.assertWithError(\n waveformSegment.amplitude <= 1,\n `amplitude must be 1 or less (got ${waveformSegment.amplitude})`\n );\n\n _console.assertTypeWithError(waveformSegment.duration, \"number\");\n _console.assertWithError(\n waveformSegment.duration > 0,\n `duration must be greater than 0ms (got ${waveformSegment.duration}ms)`\n );\n _console.assertWithError(\n waveformSegment.duration <= MaxVibrationWaveformSegmentDuration,\n `duration must be ${MaxVibrationWaveformSegmentDuration}ms or less (got ${waveformSegment.duration}ms)`\n );\n }\n\n #verifyWaveformSegments(waveformSegments: VibrationWaveformSegment[]) {\n this.#assertNonEmptyArray(waveformSegments);\n _console.assertWithError(\n waveformSegments.length <= MaxNumberOfVibrationWaveformSegments,\n `must have ${MaxNumberOfVibrationWaveformSegments} waveformSegments or fewer (got ${waveformSegments.length})`\n );\n waveformSegments.forEach((waveformSegment) => {\n this.#verifyWaveformSegment(waveformSegment);\n });\n }\n\n #createWaveformEffectsData(\n locations: VibrationLocation[],\n waveformEffectSegments: VibrationWaveformEffectSegment[],\n waveformEffectSequenceLoopCount: number = 0\n ) {\n this.#verifyWaveformEffectSegments(waveformEffectSegments);\n this.#verifyWaveformEffectSequenceLoopCount(\n waveformEffectSequenceLoopCount\n );\n\n let dataArray = [];\n let byteOffset = 0;\n\n const hasAtLeast1WaveformEffectWithANonzeroLoopCount =\n waveformEffectSegments.some((waveformEffectSegment) => {\n const { loopCount } = waveformEffectSegment;\n return loopCount != undefined && loopCount > 0;\n });\n\n const includeAllWaveformEffectSegments =\n hasAtLeast1WaveformEffectWithANonzeroLoopCount ||\n waveformEffectSequenceLoopCount != 0;\n\n for (\n let index = 0;\n index < waveformEffectSegments.length ||\n (includeAllWaveformEffectSegments &&\n index < MaxNumberOfVibrationWaveformEffectSegments);\n index++\n ) {\n const waveformEffectSegment = waveformEffectSegments[index] || {\n effect: \"none\",\n };\n if (waveformEffectSegment.effect != undefined) {\n const waveformEffect = waveformEffectSegment.effect;\n dataArray[byteOffset++] =\n VibrationWaveformEffects.indexOf(waveformEffect);\n } else if (waveformEffectSegment.delay != undefined) {\n const { delay } = waveformEffectSegment;\n dataArray[byteOffset++] = (1 << 7) | Math.floor(delay / 10); // set most significant bit to 1\n } else {\n throw Error(\"invalid waveformEffectSegment\");\n }\n }\n\n const includeAllWaveformEffectSegmentLoopCounts =\n waveformEffectSequenceLoopCount != 0;\n for (\n let index = 0;\n index < waveformEffectSegments.length ||\n (includeAllWaveformEffectSegmentLoopCounts &&\n index < MaxNumberOfVibrationWaveformEffectSegments);\n index++\n ) {\n const waveformEffectSegmentLoopCount =\n waveformEffectSegments[index]?.loopCount || 0;\n if (index == 0 || index == 4) {\n dataArray[byteOffset] = 0;\n }\n const bitOffset = 2 * (index % 4);\n dataArray[byteOffset] |= waveformEffectSegmentLoopCount << bitOffset;\n if (index == 3 || index == 7) {\n byteOffset++;\n }\n }\n\n if (waveformEffectSequenceLoopCount != 0) {\n dataArray[byteOffset++] = waveformEffectSequenceLoopCount;\n }\n const dataView = new DataView(Uint8Array.from(dataArray).buffer);\n _console.log({ dataArray, dataView });\n return this.#createData(locations, \"waveformEffect\", dataView);\n }\n #createWaveformData(\n locations: VibrationLocation[],\n waveformSegments: VibrationWaveformSegment[]\n ) {\n this.#verifyWaveformSegments(waveformSegments);\n const dataView = new DataView(new ArrayBuffer(waveformSegments.length * 2));\n waveformSegments.forEach((waveformSegment, index) => {\n dataView.setUint8(index * 2, Math.floor(waveformSegment.amplitude * 127));\n dataView.setUint8(\n index * 2 + 1,\n Math.floor(waveformSegment.duration / 10)\n );\n });\n _console.log({ dataView });\n return this.#createData(locations, \"waveform\", dataView);\n }\n\n #verifyVibrationType(vibrationType: VibrationType) {\n _console.assertTypeWithError(vibrationType, \"string\");\n _console.assertWithError(\n VibrationTypes.includes(vibrationType),\n `invalid vibrationType \"${vibrationType}\"`\n );\n }\n\n #createData(\n locations: VibrationLocation[],\n vibrationType: VibrationType,\n dataView: DataView\n ) {\n _console.assertWithError(dataView?.byteLength > 0, \"no data received\");\n const locationsBitmask = this.#createLocationsBitmask(locations);\n this.#verifyVibrationType(vibrationType);\n const vibrationTypeIndex = VibrationTypes.indexOf(vibrationType);\n _console.log({ locationsBitmask, vibrationTypeIndex, dataView });\n const data = concatenateArrayBuffers(\n locationsBitmask,\n vibrationTypeIndex,\n dataView.byteLength,\n dataView\n );\n _console.log({ data });\n return data;\n }\n\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately: boolean = true\n ) {\n let triggerVibrationData!: ArrayBuffer;\n vibrationConfigurations.forEach((vibrationConfiguration) => {\n const { type } = vibrationConfiguration;\n\n let { locations } = vibrationConfiguration;\n locations = locations || this.vibrationLocations.slice();\n locations = locations.filter((location) =>\n this.vibrationLocations.includes(location)\n );\n\n let arrayBuffer: ArrayBuffer;\n\n switch (type) {\n case \"waveformEffect\":\n {\n const { segments, loopCount } = vibrationConfiguration;\n arrayBuffer = this.#createWaveformEffectsData(\n locations,\n segments,\n loopCount\n );\n }\n break;\n case \"waveform\":\n {\n const { segments } = vibrationConfiguration;\n arrayBuffer = this.#createWaveformData(locations, segments);\n }\n break;\n default:\n throw Error(`invalid vibration type \"${type}\"`);\n }\n _console.log({ type, arrayBuffer });\n triggerVibrationData = concatenateArrayBuffers(\n triggerVibrationData,\n arrayBuffer\n );\n });\n await this.sendMessage(\n [{ type: \"triggerVibration\", data: triggerVibrationData }],\n sendImmediately\n );\n }\n\n #vibrationLocations: VibrationLocation[] = [];\n get vibrationLocations() {\n return this.#vibrationLocations;\n }\n #onVibrationLocations(vibrationLocations: VibrationLocation[]) {\n this.#vibrationLocations = vibrationLocations;\n _console.log(\"vibrationLocations\", vibrationLocations);\n this.#dispatchEvent(\"getVibrationLocations\", {\n vibrationLocations: this.#vibrationLocations,\n });\n }\n\n // MESSAGE\n parseMessage(messageType: VibrationMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getVibrationLocations\":\n const vibrationLocations = Array.from(new Uint8Array(dataView.buffer))\n .map((index) => VibrationLocations[index])\n .filter(Boolean);\n this.#onVibrationLocations(vibrationLocations);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default VibrationManager;\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInNode } from \"./utils/environment.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"WifiManager\", { log: false });\n\nexport const MinWifiSSIDLength = 1;\nexport const MaxWifiSSIDLength = 32;\n\nexport const MinWifiPasswordLength = 8;\nexport const MaxWifiPasswordLength = 64;\n\nexport const WifiMessageTypes = [\n \"isWifiAvailable\",\n \"getWifiSSID\",\n \"setWifiSSID\",\n \"getWifiPassword\",\n \"setWifiPassword\",\n \"getWifiConnectionEnabled\",\n \"setWifiConnectionEnabled\",\n \"isWifiConnected\",\n \"ipAddress\",\n \"isWifiSecure\",\n] as const;\nexport type WifiMessageType = (typeof WifiMessageTypes)[number];\n\nexport const RequiredWifiMessageTypes: WifiMessageType[] = [\n \"getWifiSSID\",\n \"getWifiPassword\",\n \"getWifiConnectionEnabled\",\n \"isWifiConnected\",\n \"ipAddress\",\n \"isWifiSecure\",\n] as const;\n\nexport const WifiEventTypes = WifiMessageTypes;\nexport type WifiEventType = (typeof WifiEventTypes)[number];\n\nexport interface WifiEventMessages {\n isWifiAvailable: { isWifiAvailable: boolean };\n getWifiSSID: { wifiSSID: string };\n getWifiPassword: { wifiPassword: string };\n getEnableWifiConnection: { wifiConnectionEnabled: boolean };\n isWifiConnected: { isWifiConnected: boolean };\n ipAddress: { ipAddress?: string };\n}\n\nexport type WifiEventDispatcher = EventDispatcher<\n Device,\n WifiEventType,\n WifiEventMessages\n>;\nexport type SendWifiMessageCallback = SendMessageCallback<WifiMessageType>;\n\nclass WifiManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendWifiMessageCallback;\n\n eventDispatcher!: WifiEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required wifi information\");\n const messages = RequiredWifiMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // PROPERTIES\n\n #isWifiAvailable = false;\n get isWifiAvailable() {\n return this.#isWifiAvailable;\n }\n #updateIsWifiAvailable(updatedIsWifiAvailable: boolean) {\n _console.assertTypeWithError(updatedIsWifiAvailable, \"boolean\");\n this.#isWifiAvailable = updatedIsWifiAvailable;\n _console.log({ isWifiAvailable: this.#isWifiAvailable });\n this.#dispatchEvent(\"isWifiAvailable\", {\n isWifiAvailable: this.#isWifiAvailable,\n });\n }\n\n #assertWifiIsAvailable() {\n _console.assertWithError(this.#isWifiAvailable, \"wifi is not available\");\n }\n\n // WIFI SSID\n #wifiSSID = \"\";\n get wifiSSID() {\n return this.#wifiSSID;\n }\n\n #updateWifiSSID(updatedWifiSSID: string) {\n _console.assertTypeWithError(updatedWifiSSID, \"string\");\n this.#wifiSSID = updatedWifiSSID;\n _console.log({ wifiSSID: this.#wifiSSID });\n this.#dispatchEvent(\"getWifiSSID\", { wifiSSID: this.#wifiSSID });\n }\n async setWifiSSID(newWifiSSID: string) {\n this.#assertWifiIsAvailable();\n if (this.#wifiConnectionEnabled) {\n _console.error(\"cannot change ssid while wifi connection is enabled\");\n return;\n }\n _console.assertTypeWithError(newWifiSSID, \"string\");\n _console.assertRangeWithError(\n \"wifiSSID\",\n newWifiSSID.length,\n MinWifiSSIDLength,\n MaxWifiSSIDLength\n );\n\n const setWifiSSIDData = textEncoder.encode(newWifiSSID);\n _console.log({ setWifiSSIDData });\n\n const promise = this.waitForEvent(\"getWifiSSID\");\n this.sendMessage([{ type: \"setWifiSSID\", data: setWifiSSIDData.buffer }]);\n await promise;\n }\n\n // WIFI PASSWORD\n #wifiPassword = \"\";\n get wifiPassword() {\n return this.#wifiPassword;\n }\n\n #updateWifiPassword(updatedWifiPassword: string) {\n _console.assertTypeWithError(updatedWifiPassword, \"string\");\n this.#wifiPassword = updatedWifiPassword;\n _console.log({ wifiPassword: this.#wifiPassword });\n this.#dispatchEvent(\"getWifiPassword\", {\n wifiPassword: this.#wifiPassword,\n });\n }\n async setWifiPassword(newWifiPassword: string) {\n this.#assertWifiIsAvailable();\n if (this.#wifiConnectionEnabled) {\n _console.error(\"cannot change password while wifi connection is enabled\");\n return;\n }\n _console.assertTypeWithError(newWifiPassword, \"string\");\n if (newWifiPassword.length > 0) {\n _console.assertRangeWithError(\n \"wifiPassword\",\n newWifiPassword.length,\n MinWifiPasswordLength,\n MaxWifiPasswordLength\n );\n }\n\n const setWifiPasswordData = textEncoder.encode(newWifiPassword);\n _console.log({ setWifiPasswordData });\n\n const promise = this.waitForEvent(\"getWifiPassword\");\n this.sendMessage([\n { type: \"setWifiPassword\", data: setWifiPasswordData.buffer },\n ]);\n await promise;\n }\n\n // ENABLE WIFI CONNECTION\n #wifiConnectionEnabled!: boolean;\n get wifiConnectionEnabled() {\n return this.#wifiConnectionEnabled;\n }\n #updateWifiConnectionEnabled(wifiConnectionEnabled: boolean) {\n _console.log({ wifiConnectionEnabled });\n this.#wifiConnectionEnabled = wifiConnectionEnabled;\n this.#dispatchEvent(\"getWifiConnectionEnabled\", {\n wifiConnectionEnabled: wifiConnectionEnabled,\n });\n }\n async setWifiConnectionEnabled(\n newWifiConnectionEnabled: boolean,\n sendImmediately: boolean = true\n ) {\n this.#assertWifiIsAvailable();\n _console.assertTypeWithError(newWifiConnectionEnabled, \"boolean\");\n if (this.#wifiConnectionEnabled == newWifiConnectionEnabled) {\n _console.log(\n `redundant wifiConnectionEnabled assignment ${newWifiConnectionEnabled}`\n );\n return;\n }\n\n const promise = this.waitForEvent(\"getWifiConnectionEnabled\");\n\n this.sendMessage(\n [\n {\n type: \"setWifiConnectionEnabled\",\n\n data: UInt8ByteBuffer(Number(newWifiConnectionEnabled)),\n },\n ],\n sendImmediately\n );\n await promise;\n }\n async toggleWifiConnection() {\n return this.setWifiConnectionEnabled(!this.wifiConnectionEnabled);\n }\n async enableWifiConnection() {\n return this.setWifiConnectionEnabled(true);\n }\n async disableWifiConnection() {\n return this.setWifiConnectionEnabled(false);\n }\n\n // IS WIFI CONNECTED\n #isWifiConnected = false;\n get isWifiConnected() {\n return this.#isWifiConnected;\n }\n #updateIsWifiConnected(updatedIsWifiConnected: boolean) {\n _console.assertTypeWithError(updatedIsWifiConnected, \"boolean\");\n this.#isWifiConnected = updatedIsWifiConnected;\n _console.log({ isWifiConnected: this.#isWifiConnected });\n this.#dispatchEvent(\"isWifiConnected\", {\n isWifiConnected: this.#isWifiConnected,\n });\n }\n\n // IP ADDRESS\n #ipAddress?: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n\n #updateIpAddress(updatedIpAddress?: string) {\n this.#ipAddress = updatedIpAddress;\n _console.log({ ipAddress: this.#ipAddress });\n this.#dispatchEvent(\"ipAddress\", {\n ipAddress: this.#ipAddress,\n });\n }\n\n // IS WIFI SECURE\n #isWifiSecure = false;\n get isWifiSecure() {\n return this.#isWifiSecure;\n }\n #updateIsWifiSecure(updatedIsWifiSecure: boolean) {\n _console.assertTypeWithError(updatedIsWifiSecure, \"boolean\");\n this.#isWifiSecure = updatedIsWifiSecure;\n _console.log({ isWifiSecure: this.#isWifiSecure });\n this.#dispatchEvent(\"isWifiSecure\", {\n isWifiSecure: this.#isWifiSecure,\n });\n }\n\n // MESSAGE\n parseMessage(messageType: WifiMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"isWifiAvailable\":\n const isWifiAvailable = Boolean(dataView.getUint8(0));\n _console.log({ isWifiAvailable });\n this.#updateIsWifiAvailable(isWifiAvailable);\n break;\n case \"getWifiSSID\":\n case \"setWifiSSID\":\n const ssid = textDecoder.decode(dataView.buffer);\n _console.log({ ssid });\n this.#updateWifiSSID(ssid);\n break;\n case \"getWifiPassword\":\n case \"setWifiPassword\":\n const password = textDecoder.decode(dataView.buffer);\n _console.log({ password });\n this.#updateWifiPassword(password);\n break;\n case \"getWifiConnectionEnabled\":\n case \"setWifiConnectionEnabled\":\n const enableWifiConnection = Boolean(dataView.getUint8(0));\n _console.log({ enableWifiConnection });\n this.#updateWifiConnectionEnabled(enableWifiConnection);\n break;\n case \"isWifiConnected\":\n const isWifiConnected = Boolean(dataView.getUint8(0));\n _console.log({ isWifiConnected });\n this.#updateIsWifiConnected(isWifiConnected);\n break;\n case \"ipAddress\":\n let ipAddress: string | undefined = undefined;\n if (dataView.byteLength == 4) {\n ipAddress = new Uint8Array(dataView.buffer.slice(0, 4)).join(\".\");\n }\n _console.log({ ipAddress });\n this.#updateIpAddress(ipAddress);\n break;\n case \"isWifiSecure\":\n const isWifiSecure = Boolean(dataView.getUint8(0));\n _console.log({ isWifiSecure });\n this.#updateIsWifiSecure(isWifiSecure);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n this.#wifiSSID = \"\";\n this.#wifiPassword = \"\";\n this.#ipAddress = \"\";\n this.#isWifiConnected = false;\n this.#isWifiAvailable = false;\n }\n}\n\nexport default WifiManager;\n","import { createConsole } from \"./Console.ts\";\nimport { DisplayColorRGB } from \"./DisplayUtils.ts\";\n\nconst _console = createConsole(\"ColorUtils\", { log: false });\n\nexport function hexToRGB(hex: string): DisplayColorRGB {\n hex = hex.replace(/^#/, \"\");\n\n if (hex.length == 3) {\n hex = hex\n .split(\"\")\n .map((char) => char + char)\n .join(\"\");\n }\n\n _console.assertWithError(\n hex.length == 6,\n `hex length must be 6 (got ${hex.length})`\n );\n\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n\n return { r, g, b };\n}\n\nexport const blackColor: DisplayColorRGB = { r: 0, g: 0, b: 0 };\nexport function colorNameToRGB(colorName: string): DisplayColorRGB {\n const temp = document.createElement(\"div\");\n temp.style.color = colorName;\n document.body.appendChild(temp);\n\n const computedColor = getComputedStyle(temp).color;\n document.body.removeChild(temp);\n\n // Match \"rgb(r, g, b)\" or \"rgba(r, g, b, a)\"\n const match = computedColor.match(/^rgba?\\((\\d+), (\\d+), (\\d+)/);\n if (!match) return blackColor;\n\n return {\n r: parseInt(match[1], 10),\n g: parseInt(match[2], 10),\n b: parseInt(match[3], 10),\n };\n}\n\nexport function stringToRGB(string: string): DisplayColorRGB {\n if (string.startsWith(\"#\")) {\n return hexToRGB(string);\n } else {\n return colorNameToRGB(string);\n }\n}\n\nexport function rgbToHex({ r, g, b }: DisplayColorRGB): string {\n const toHex = (value: number) =>\n value.toString(16).padStart(2, \"0\").toLowerCase();\n\n _console.assertWithError(\n [r, g, b].every((v) => v >= 0 && v <= 255),\n `RGB values must be between 0 and 255 (got r=${r}, g=${g}, b=${b})`\n );\n\n return `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n}\n\nexport function colorDistanceSq(\n a: DisplayColorRGB,\n b: DisplayColorRGB\n): number {\n return (a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2;\n}\n\nexport interface KMeansOptions {\n useInputColors?: boolean; // pick nearest input or average\n maxIterations?: number;\n}\nexport const defaultKMeansOptions: KMeansOptions = {\n useInputColors: true,\n maxIterations: 20,\n};\n\nexport interface KMeansResult {\n palette: string[]; // reduced colors\n mapping: Record<string, number>; // original -> palette index\n}\n\nexport function kMeansColors(\n colors: string[],\n k: number,\n options?: KMeansOptions\n): KMeansResult {\n _console.assertTypeWithError(k, \"number\");\n _console.assertWithError(k > 0, `invalid k ${k}`);\n options = { ...defaultKMeansOptions, ...options };\n const maxIter = options.maxIterations!;\n const useInputColors = options.useInputColors!;\n\n // cache parsed colors\n const colorMap = new Map<string, DisplayColorRGB>();\n for (const c of colors) {\n if (!colorMap.has(c)) {\n colorMap.set(c, stringToRGB(c));\n }\n }\n\n const uniqueColors = Array.from(colorMap.values());\n const uniqueKeys = Array.from(colorMap.keys());\n\n //_console.log({ uniqueColors, uniqueKeys });\n\n if (uniqueColors.length <= k) {\n const mapping: Record<string, number> = {};\n uniqueKeys.forEach((key, idx) => (mapping[key] = idx));\n return { palette: uniqueKeys, mapping };\n }\n\n // Initialize centroids\n let centroids: DisplayColorRGB[] = uniqueColors.slice(0, k);\n\n for (let iter = 0; iter < maxIter; iter++) {\n const clusters: number[][] = Array.from({ length: k }, () => []);\n //_console.log({ clusters, k });\n uniqueColors.forEach((p, idx) => {\n let best = 0;\n let bestDist = Infinity;\n centroids.forEach((c, ci) => {\n const d = colorDistanceSq(p, c);\n if (d < bestDist) {\n bestDist = d;\n best = ci;\n }\n });\n clusters[best].push(idx);\n });\n\n centroids = clusters.map((cluster) => {\n if (cluster.length === 0) return { ...blackColor };\n if (useInputColors) {\n let bestIdx = cluster[0];\n let bestDist = Infinity;\n cluster.forEach((idx) => {\n const d = colorDistanceSq(uniqueColors[idx], centroids[0]);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = idx;\n }\n });\n return uniqueColors[bestIdx];\n } else {\n const sum = cluster.reduce(\n (acc, idx) => {\n const p = uniqueColors[idx];\n return {\n r: acc.r + p.r,\n g: acc.g + p.g,\n b: acc.b + p.b,\n } as DisplayColorRGB;\n },\n { ...blackColor }\n );\n return {\n r: sum.r / cluster.length,\n g: sum.g / cluster.length,\n b: sum.b / cluster.length,\n };\n }\n });\n }\n\n const palette = centroids.map((c) => rgbToHex(c));\n\n // Build mapping: original color -> palette index\n const mapping: Record<string, number> = {};\n for (const [orig, DisplayColorRGB] of colorMap.entries()) {\n let bestIdx = 0;\n let bestDist = Infinity;\n centroids.forEach((c, ci) => {\n const d = colorDistanceSq(c, DisplayColorRGB);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = ci;\n }\n });\n mapping[orig] = bestIdx;\n }\n\n return { palette, mapping };\n}\n\nexport function mapToClosestPaletteIndex(\n colors: string[],\n palette: string[]\n): Record<string, number> {\n const paletteRGB: DisplayColorRGB[] = palette.map(stringToRGB);\n const mapping: Record<string, number> = {};\n\n for (const color of colors) {\n const rgb = stringToRGB(color);\n let bestIdx = 0;\n let bestDist = Infinity;\n\n paletteRGB.forEach((p, idx) => {\n const d = colorDistanceSq(rgb, p);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = idx;\n }\n });\n\n mapping[color] = bestIdx;\n }\n\n return mapping;\n}\n","export const DisplaySegmentCaps = [\"flat\", \"round\"] as const;\nexport type DisplaySegmentCap = (typeof DisplaySegmentCaps)[number];\n\nexport const DisplayAlignments = [\"start\", \"center\", \"end\"] as const;\nexport type DisplayAlignment = (typeof DisplayAlignments)[number];\n\nexport const DisplayAlignmentDirections = [\"horizontal\", \"vertical\"] as const;\nexport type DisplayAlignmentDirection =\n (typeof DisplayAlignmentDirections)[number];\n\nexport const DisplayDirections = [\"right\", \"left\", \"up\", \"down\"] as const;\nexport type DisplayDirection = (typeof DisplayDirections)[number];\n\nexport type DisplayContextState = {\n backgroundColorIndex: number;\n fillColorIndex: number;\n lineColorIndex: number;\n\n ignoreFill: boolean;\n ignoreLine: boolean;\n fillBackground: boolean;\n\n lineWidth: number;\n rotation: number;\n\n horizontalAlignment: DisplayAlignment;\n verticalAlignment: DisplayAlignment;\n\n segmentStartCap: DisplaySegmentCap;\n segmentEndCap: DisplaySegmentCap;\n\n segmentStartRadius: number;\n segmentEndRadius: number;\n\n cropTop: number;\n cropRight: number;\n cropBottom: number;\n cropLeft: number;\n\n rotationCropTop: number;\n rotationCropRight: number;\n rotationCropBottom: number;\n rotationCropLeft: number;\n\n bitmapColorIndices: number[];\n bitmapScaleX: number;\n bitmapScaleY: number;\n\n spriteColorIndices: number[];\n spriteScaleX: number;\n spriteScaleY: number;\n\n spriteSheetName?: string;\n\n spritesLineHeight: number;\n spritesDirection: DisplayDirection;\n spritesLineDirection: DisplayDirection;\n spritesSpacing: number;\n spritesLineSpacing: number;\n spritesAlignment: DisplayAlignment;\n spritesLineAlignment: DisplayAlignment;\n};\nexport type DisplayContextStateKey = keyof DisplayContextState;\nexport type PartialDisplayContextState = Partial<DisplayContextState>;\n\nexport const DefaultDisplayContextState: DisplayContextState = {\n backgroundColorIndex: 0,\n fillColorIndex: 1,\n lineColorIndex: 1,\n\n ignoreFill: false,\n ignoreLine: false,\n fillBackground: false,\n\n lineWidth: 0,\n rotation: 0,\n\n horizontalAlignment: \"center\",\n verticalAlignment: \"center\",\n\n segmentStartCap: \"flat\",\n segmentEndCap: \"flat\",\n\n segmentStartRadius: 1,\n segmentEndRadius: 1,\n\n cropTop: 0,\n cropRight: 0,\n cropBottom: 0,\n cropLeft: 0,\n\n rotationCropTop: 0,\n rotationCropRight: 0,\n rotationCropBottom: 0,\n rotationCropLeft: 0,\n\n bitmapColorIndices: new Array(0).fill(0),\n bitmapScaleX: 1,\n bitmapScaleY: 1,\n\n spriteColorIndices: new Array(0).fill(0),\n spriteScaleX: 1,\n spriteScaleY: 1,\n\n spriteSheetName: undefined,\n\n spritesLineHeight: 0,\n\n spritesDirection: \"right\",\n spritesLineDirection: \"down\",\n\n spritesSpacing: 0,\n spritesLineSpacing: 0,\n\n spritesAlignment: \"end\",\n spritesLineAlignment: \"start\",\n};\n\nexport function isDirectionPositive(direction: DisplayDirection) {\n switch (direction) {\n case \"right\":\n case \"down\":\n return true;\n case \"left\":\n case \"up\":\n return false;\n }\n}\nexport function isDirectionHorizontal(direction: DisplayDirection) {\n switch (direction) {\n case \"right\":\n case \"left\":\n return true;\n case \"down\":\n case \"up\":\n return false;\n }\n}\n","export function deepEqual(obj1: any, obj2: any): boolean {\n if (obj1 === obj2) {\n return true;\n }\n\n if (\n typeof obj1 !== \"object\" ||\n obj1 === null ||\n typeof obj2 !== \"object\" ||\n obj2 === null\n ) {\n return false;\n }\n\n const keys1 = Object.keys(obj1);\n const keys2 = Object.keys(obj2);\n\n if (keys1.length !== keys2.length) return false;\n\n for (let key of keys1) {\n if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function removeRedundancies(array: any[]) {\n return Array.from(new Set(array));\n}\n","import { createConsole } from \"./Console.ts\";\nimport {\n DefaultDisplayContextState,\n DisplayContextState,\n DisplayContextStateKey,\n PartialDisplayContextState,\n} from \"./DisplayContextState.ts\";\nimport { deepEqual } from \"./ObjectUtils.ts\";\n\nconst _console = createConsole(\"DisplayContextStateHelper\", { log: false });\n\nclass DisplayContextStateHelper {\n #state: DisplayContextState = Object.assign({}, DefaultDisplayContextState);\n get state() {\n return this.#state;\n }\n\n get isSegmentUniform() {\n return (\n this.state.segmentStartRadius == this.state.segmentEndRadius &&\n this.state.segmentStartCap == this.state.segmentEndCap\n );\n }\n\n diff(other: PartialDisplayContextState) {\n let differences: DisplayContextStateKey[] = [];\n const keys = Object.keys(other) as DisplayContextStateKey[];\n keys.forEach((key) => {\n const value = other[key]!;\n\n if (!deepEqual(this.#state[key], value)) {\n differences.push(key);\n }\n });\n _console.log(\"diff\", other, differences);\n return differences;\n }\n update(newState: PartialDisplayContextState) {\n let differences = this.diff(newState);\n if (differences.length == 0) {\n _console.log(\"redundant contextState\", newState);\n }\n differences.forEach((key) => {\n const value = newState[key]!;\n // @ts-expect-error\n this.#state[key] = value;\n });\n return differences;\n }\n reset() {\n Object.assign(this.#state, DefaultDisplayContextState);\n }\n}\n\nexport default DisplayContextStateHelper;\n","import {\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayBrightness,\n DisplayBrightnesses,\n DisplayPixelDepth,\n DisplayPixelDepths,\n DisplayPointDataType,\n DisplayPointDataTypes,\n displayPointDataTypeToRange,\n displayPointDataTypeToSize,\n DisplayWireframe,\n DisplayWireframeEdge,\n} from \"../DisplayManager.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { DisplayContextCommandType } from \"./DisplayContextCommand.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayAlignmentDirections,\n DisplayAlignments,\n DisplayContextStateKey,\n DisplayDirection,\n DisplayDirections,\n DisplaySegmentCap,\n DisplaySegmentCaps,\n} from \"./DisplayContextState.ts\";\nimport {\n getVector2Distance,\n Int16Max,\n Uint16Max,\n Vector2,\n} from \"./MathUtils.ts\";\nimport RangeHelper from \"./RangeHelper.ts\";\n\nconst _console = createConsole(\"DisplayUtils\", { log: false });\n\nexport function formatRotation(\n rotation: number,\n isRadians?: boolean,\n isSigned?: boolean\n) {\n if (isRadians) {\n const rotationRad = rotation;\n _console.log({ rotationRad });\n rotation %= 2 * Math.PI;\n rotation /= 2 * Math.PI;\n } else {\n const rotationDeg = rotation;\n _console.log({ rotationDeg });\n rotation %= 360;\n rotation /= 360;\n }\n if (isSigned) {\n rotation *= Int16Max;\n } else {\n rotation *= Uint16Max;\n }\n rotation = Math.floor(rotation);\n _console.log({ formattedRotation: rotation });\n return rotation;\n}\n\nexport function roundToStep(value: number, step: number) {\n const roundedValue = Math.round(value / step) * step;\n //_console.log(value, step, roundedValue);\n return roundedValue;\n}\n\nexport const minDisplayScale = -50;\nexport const maxDisplayScale = 50;\nexport const displayScaleStep = 0.002;\nexport function formatScale(bitmapScale: number) {\n bitmapScale /= displayScaleStep;\n //_console.log({ formattedBitmapScale: bitmapScale });\n return bitmapScale;\n}\nexport function roundScale(bitmapScale: number) {\n return roundToStep(bitmapScale, displayScaleStep);\n}\n\nexport function assertValidSegmentCap(segmentCap: DisplaySegmentCap) {\n _console.assertEnumWithError(segmentCap, DisplaySegmentCaps);\n}\n\nexport function assertValidDisplayBrightness(\n displayBrightness: DisplayBrightness\n) {\n _console.assertEnumWithError(displayBrightness, DisplayBrightnesses);\n}\n\nexport function assertValidColorValue(name: string, value: number) {\n _console.assertRangeWithError(name, value, 0, 255);\n}\nexport function assertValidColor(color: DisplayColorRGB) {\n assertValidColorValue(\"red\", color.r);\n assertValidColorValue(\"green\", color.g);\n assertValidColorValue(\"blue\", color.b);\n}\n\nexport function assertValidOpacity(value: number) {\n _console.assertRangeWithError(\"opacity\", value, 0, 1);\n}\n\nexport const DisplayCropDirections = [\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n] as const;\nexport type DisplayCropDirection = (typeof DisplayCropDirections)[number];\n\nexport const DisplayContextCropStateKeys = [\n \"cropTop\",\n \"cropRight\",\n \"cropBottom\",\n \"cropLeft\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextCropStateKey =\n (typeof DisplayContextCropStateKeys)[number];\n\nexport const DisplayCropDirectionToStateKey: Record<\n DisplayCropDirection,\n DisplayContextCropStateKey\n> = {\n top: \"cropTop\",\n right: \"cropRight\",\n bottom: \"cropBottom\",\n left: \"cropLeft\",\n};\n\nexport const DisplayContextCropCommandTypes = [\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextCropCommandType =\n (typeof DisplayContextCropCommandTypes)[number];\n\nexport const DisplayCropDirectionToCommandType: Record<\n DisplayCropDirection,\n DisplayContextCropCommandType\n> = {\n top: \"setCropTop\",\n right: \"setCropRight\",\n bottom: \"setCropBottom\",\n left: \"setCropLeft\",\n};\n\nexport const DisplayContextRotationCropStateKeys = [\n \"rotationCropTop\",\n \"rotationCropRight\",\n \"rotationCropBottom\",\n \"rotationCropLeft\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextRotationCropStateKey =\n (typeof DisplayContextRotationCropStateKeys)[number];\n\nexport const DisplayRotationCropDirectionToStateKey: Record<\n DisplayCropDirection,\n DisplayContextRotationCropStateKey\n> = {\n top: \"rotationCropTop\",\n right: \"rotationCropRight\",\n bottom: \"rotationCropBottom\",\n left: \"rotationCropLeft\",\n};\n\nexport const DisplayContextRotationCropCommandTypes = [\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextRotationCropCommandType =\n (typeof DisplayContextRotationCropCommandTypes)[number];\n\nexport const DisplayRotationCropDirectionToCommandType: Record<\n DisplayCropDirection,\n DisplayContextRotationCropCommandType\n> = {\n top: \"setRotationCropTop\",\n right: \"setRotationCropRight\",\n bottom: \"setRotationCropBottom\",\n left: \"setRotationCropLeft\",\n};\n\nexport const DisplayContextAlignmentCommandTypes = [\n \"setVerticalAlignment\",\n \"setHorizontalAlignment\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextAlignmentCommandType =\n (typeof DisplayContextAlignmentCommandTypes)[number];\nexport const DisplayAlignmentDirectionToCommandType: Record<\n DisplayAlignmentDirection,\n DisplayContextAlignmentCommandType\n> = {\n horizontal: \"setHorizontalAlignment\",\n vertical: \"setVerticalAlignment\",\n};\n\nexport const DisplayContextAlignmentStateKeys = [\n \"verticalAlignment\",\n \"horizontalAlignment\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextAlignmentStateKey =\n (typeof DisplayContextAlignmentStateKeys)[number];\n\nexport const DisplayAlignmentDirectionToStateKey: Record<\n DisplayAlignmentDirection,\n DisplayContextAlignmentStateKey\n> = {\n horizontal: \"horizontalAlignment\",\n vertical: \"verticalAlignment\",\n};\n\nexport function pixelDepthToNumberOfColors(pixelDepth: DisplayPixelDepth) {\n return 2 ** Number(pixelDepth);\n}\nexport function pixelDepthToPixelsPerByte(pixelDepth: DisplayPixelDepth) {\n return 8 / Number(pixelDepth);\n}\nexport function pixelDepthToPixelBitWidth(pixelDepth: DisplayPixelDepth) {\n return Number(pixelDepth);\n}\nexport function numberOfColorsToPixelDepth(numberOfColors: number) {\n return DisplayPixelDepths.find(\n (pixelDepth) => numberOfColors <= pixelDepthToNumberOfColors(pixelDepth)\n );\n}\n\nexport const DisplayScaleDirections = [\"x\", \"y\", \"all\"] as const;\nexport type DisplayScaleDirection = (typeof DisplayScaleDirections)[number];\n\nexport const DisplayBitmapScaleDirectionToCommandType: Record<\n DisplayScaleDirection,\n DisplayContextCommandType\n> = {\n x: \"setBitmapScaleX\",\n y: \"setBitmapScaleY\",\n all: \"setBitmapScale\",\n};\n\nexport const DisplaySpriteScaleDirectionToCommandType: Record<\n DisplayScaleDirection,\n DisplayContextCommandType\n> = {\n x: \"setSpriteScaleX\",\n y: \"setSpriteScaleY\",\n all: \"setSpriteScale\",\n};\n\nexport type DisplayColorRGB = {\n r: number;\n g: number;\n b: number;\n};\nexport type DisplayColorYCbCr = {\n y: number;\n cb: number;\n cr: number;\n};\n\nexport function assertValidAlignment(alignment: DisplayAlignment) {\n _console.assertEnumWithError(alignment, DisplayAlignments);\n}\n\nexport function assertValidDirection(direction: DisplayDirection) {\n _console.assertEnumWithError(direction, DisplayDirections);\n}\n\nexport function assertValidAlignmentDirection(\n direction: DisplayAlignmentDirection\n) {\n _console.assertEnumWithError(direction, DisplayAlignmentDirections);\n}\n\nexport const displayCurveTypeToNumberOfControlPoints: Record<\n DisplayBezierCurveType,\n number\n> = {\n segment: 2,\n quadratic: 3,\n cubic: 4,\n};\nexport const displayCurveTolerance = 2.0;\nexport const displayCurveToleranceSquared = displayCurveTolerance ** 2;\n\nexport const maxNumberOfDisplayCurvePoints = 150;\nexport function assertValidNumberOfControlPoints(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n isPath = false\n) {\n let numberOfControlPoints =\n displayCurveTypeToNumberOfControlPoints[curveType];\n if (isPath) {\n numberOfControlPoints -= 1;\n }\n _console.assertWithError(\n controlPoints.length == numberOfControlPoints,\n `invalid number of control points ${controlPoints.length}, expected ${numberOfControlPoints}`\n );\n}\nexport function assertValidPathNumberOfControlPoints(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[]\n) {\n const numberOfControlPoints =\n displayCurveTypeToNumberOfControlPoints[curveType];\n _console.assertWithError(\n (controlPoints.length - 1) % (numberOfControlPoints - 1) == 0,\n `invalid number of path control points ${controlPoints.length} for path \"${curveType}\"`\n );\n}\n\nexport function assertValidPath(curves: DisplayBezierCurve[]) {\n curves.forEach((curve, index) => {\n const { type, controlPoints } = curve;\n assertValidNumberOfControlPoints(type, controlPoints, index > 0);\n });\n}\n\nexport function assertValidWireframe({ points, edges }: DisplayWireframe) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n _console.assertRangeWithError(\"numberOfEdges\", edges.length, 1, 255);\n\n edges.forEach((edge, index) => {\n _console.assertRangeWithError(\n `edgeStartIndex.${index}`,\n edge.startIndex,\n 0,\n points.length\n );\n _console.assertRangeWithError(\n `edgeEndIndex.${index}`,\n edge.endIndex,\n 0,\n points.length\n );\n });\n}\nexport function isWireframePolygon({\n points,\n edges,\n}: DisplayWireframe): Vector2[] | undefined {\n _console.log(\"isWireframePolygon?\", points, edges);\n if (points.length != edges.length) {\n return;\n }\n const _edges = edges.slice();\n let pointIndices: number[] = [];\n for (let i = 0; i < points.length; i++) {\n if (i == 0) {\n const { startIndex, endIndex } = _edges.shift()!;\n pointIndices.push(startIndex);\n pointIndices.push(endIndex);\n } else {\n const startIndex = pointIndices.at(-1);\n const edge = _edges.find(\n (edge) => edge.startIndex == startIndex || edge.endIndex == startIndex\n );\n _console.log(i, \"edge\", edge);\n if (edge) {\n _edges.splice(_edges.indexOf(edge), 1);\n const endIndex =\n edge.startIndex == startIndex ? edge.endIndex : edge.startIndex;\n if (i == points.length - 1) {\n if (endIndex != pointIndices[0]) {\n return;\n }\n } else if (pointIndices.includes(endIndex)) {\n _console.log(\"duplicate endIndex\", endIndex);\n return;\n }\n pointIndices.push(endIndex);\n } else {\n _console.log(\"no edge found\");\n return;\n }\n }\n _console.log(\"remaining edges\", _edges);\n }\n _console.log(\"pointIndices\", pointIndices);\n const polygon = pointIndices\n .map((pointIndex) => points[pointIndex])\n .filter((point, index, polygon) => polygon.indexOf(point) == index);\n\n if (polygon.length == points.length) {\n polygon.push(polygon[0]);\n _console.log(\"polygon\", polygon);\n return polygon;\n }\n}\n\nexport function mergeWireframes(a: DisplayWireframe, b: DisplayWireframe) {\n const wireframe: DisplayWireframe = structuredClone(a);\n const pointIndexOffset = a.points.length;\n b.points.forEach((point) => {\n wireframe.points.push(point);\n });\n b.edges.forEach(({ startIndex, endIndex }) => {\n wireframe.edges.push({\n startIndex: startIndex + pointIndexOffset,\n endIndex: endIndex + pointIndexOffset,\n });\n });\n return trimWireframe(wireframe);\n}\n\nexport function intersectWireframes(\n a: DisplayWireframe,\n b: DisplayWireframe,\n ignoreDirection = true\n) {\n a = trimWireframe(a);\n b = trimWireframe(b);\n //_console.log(\"intersectWireframes\", a, b);\n const wireframe: DisplayWireframe = { points: [], edges: [] };\n const pointIndices: { a: number; b: number }[] = [];\n const aPointIndices: number[] = [];\n const bPointIndices: number[] = [];\n a.points.forEach((point, aPointIndex) => {\n const bPointIndex = b.points.findIndex((_point) => {\n const distance = getVector2Distance(point, _point);\n return distance == 0;\n });\n if (bPointIndex != -1) {\n pointIndices.push({ a: aPointIndex, b: bPointIndex });\n aPointIndices.push(aPointIndex);\n bPointIndices.push(bPointIndex);\n wireframe.points.push(structuredClone(point));\n }\n });\n a.edges.forEach((aEdge) => {\n if (\n !aPointIndices.includes(aEdge.startIndex) ||\n !aPointIndices.includes(aEdge.endIndex)\n ) {\n return;\n }\n const startIndex = aPointIndices.indexOf(aEdge.startIndex);\n const endIndex = aPointIndices.indexOf(aEdge.endIndex);\n\n const bEdge = b.edges.find((bEdge) => {\n if (\n !bPointIndices.includes(bEdge.startIndex) ||\n !bPointIndices.includes(bEdge.endIndex)\n ) {\n return false;\n }\n const bStartIndex = bPointIndices.indexOf(bEdge.startIndex);\n const bEndIndex = bPointIndices.indexOf(bEdge.endIndex);\n if (ignoreDirection) {\n return (\n (startIndex == bStartIndex && endIndex == bEndIndex) ||\n (startIndex == bEndIndex && endIndex == bStartIndex)\n );\n } else {\n return startIndex == bStartIndex && endIndex == bEndIndex;\n }\n });\n\n if (!bEdge) {\n return;\n }\n\n wireframe.edges.push({\n startIndex,\n endIndex,\n });\n });\n //_console.log(\"intersectedWireframe\", wireframe);\n return wireframe;\n}\n\nexport function trimWireframe(wireframe: DisplayWireframe): DisplayWireframe {\n _console.log(\"trimming wireframe\", wireframe);\n const { points, edges } = wireframe;\n const trimmedPoints: Vector2[] = [];\n const trimmedEdges: DisplayWireframeEdge[] = [];\n edges.forEach((edge) => {\n const { startIndex, endIndex } = edge;\n let startPoint = points[startIndex];\n let endPoint = points[endIndex];\n\n let trimmedStartIndex = trimmedPoints.findIndex(\n ({ x, y }) => startPoint.x == x && startPoint.y == y\n );\n if (trimmedStartIndex == -1) {\n //_console.log(\"adding startPoint\", startPoint);\n trimmedPoints.push(startPoint);\n trimmedStartIndex = trimmedPoints.length - 1;\n }\n\n let trimmedEndIndex = trimmedPoints.findIndex(\n ({ x, y }) => endPoint.x == x && endPoint.y == y\n );\n if (trimmedEndIndex == -1) {\n //_console.log(\"adding endPoint\", endPoint);\n trimmedPoints.push(endPoint);\n trimmedEndIndex = trimmedPoints.length - 1;\n }\n\n const trimmedEdge: DisplayWireframeEdge = {\n startIndex: trimmedStartIndex,\n endIndex: trimmedEndIndex,\n };\n let trimmedEdgeIndex = trimmedEdges.findIndex(\n ({ startIndex, endIndex }) =>\n startIndex == trimmedEdge.startIndex && endIndex == trimmedEdge.endIndex\n );\n if (trimmedEdgeIndex == -1) {\n //_console.log(\"adding edge\", trimmedEdge);\n trimmedEdges.push(trimmedEdge);\n trimmedEdgeIndex = trimmedEdges.length - 1;\n }\n });\n _console.log(\"trimmedWireframe\", trimmedPoints, trimmedEdges);\n return { points: trimmedPoints, edges: trimmedEdges };\n}\n\nexport function getPointDataType(points: Vector2[]): DisplayPointDataType {\n const range = new RangeHelper();\n points.forEach(({ x, y }) => {\n range.update(x);\n range.update(y);\n });\n const pointDataType = DisplayPointDataTypes.find((pointDataType) => {\n const { min, max } = displayPointDataTypeToRange[pointDataType];\n return range.min >= min && range.max <= max;\n })!;\n _console.log(\"pointDataType\", pointDataType, points);\n return pointDataType!;\n}\nexport function serializePoints(\n points: Vector2[],\n pointDataType?: DisplayPointDataType,\n isPath = false\n) {\n pointDataType = pointDataType || getPointDataType(points);\n _console.assertEnumWithError(pointDataType, DisplayPointDataTypes);\n const pointDataSize = displayPointDataTypeToSize[pointDataType];\n let dataViewLength = points.length * pointDataSize;\n if (!isPath) {\n dataViewLength += 2; // pointDataType + points.length\n }\n const dataView = new DataView(new ArrayBuffer(dataViewLength));\n _console.log(\n `serializing ${points.length} ${pointDataType} points (${dataView.byteLength} bytes)...`\n );\n let offset = 0;\n if (!isPath) {\n dataView.setUint8(offset++, DisplayPointDataTypes.indexOf(pointDataType));\n dataView.setUint8(offset++, points.length);\n }\n points.forEach(({ x, y }) => {\n switch (pointDataType) {\n case \"int8\":\n dataView.setInt8(offset, x);\n offset += 1;\n dataView.setInt8(offset, y);\n offset += 1;\n break;\n case \"int16\":\n dataView.setInt16(offset, x, true);\n offset += 2;\n dataView.setInt16(offset, y, true);\n offset += 2;\n break;\n case \"float\":\n dataView.setFloat32(offset, x, true);\n offset += 4;\n dataView.setFloat32(offset, y, true);\n offset += 4;\n break;\n }\n });\n return dataView;\n}\n","import {\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayBezierCurveTypes,\n DisplayBitmap,\n DisplayBitmapColorPair,\n displayCurveTypeBitWidth,\n DisplayPointDataTypes,\n displayCurveTypesPerByte,\n DisplaySpriteColorPair,\n DisplayWireframe,\n} from \"../DisplayManager.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./ArrayBufferUtils.ts\";\nimport { rgbToHex, stringToRGB } from \"./ColorUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { drawBitmapHeaderLength, getBitmapData } from \"./DisplayBitmapUtils.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignments,\n DisplayDirection,\n DisplayDirections,\n DisplaySegmentCap,\n DisplaySegmentCaps,\n} from \"./DisplayContextState.ts\";\nimport { DisplayManagerInterface } from \"./DisplayManagerInterface.ts\";\nimport { DisplaySpriteSerializedLines } from \"./DisplaySpriteSheetUtils.ts\";\nimport {\n assertValidAlignment,\n assertValidColor,\n assertValidDirection,\n assertValidPathNumberOfControlPoints,\n assertValidNumberOfControlPoints,\n assertValidOpacity,\n assertValidPath,\n assertValidSegmentCap,\n assertValidWireframe,\n DisplayColorRGB,\n formatRotation,\n formatScale,\n maxDisplayScale,\n minDisplayScale,\n roundScale,\n serializePoints,\n getPointDataType,\n} from \"./DisplayUtils.ts\";\nimport {\n clamp,\n degToRad,\n Int16Max,\n Int16Min,\n normalizeRadians,\n twoPi,\n Vector2,\n} from \"./MathUtils.ts\";\n\nconst _console = createConsole(\"DisplayContextCommand\", { log: false });\n\nexport const DisplayContextCommandTypes = [\n \"show\",\n \"clear\",\n\n \"setColor\",\n \"setColorOpacity\",\n \"setOpacity\",\n\n \"saveContext\",\n \"restoreContext\",\n\n \"selectBackgroundColor\",\n \"selectFillColor\",\n \"selectLineColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setHorizontalAlignment\",\n \"setVerticalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"clearRect\",\n\n \"drawRect\",\n \"drawRoundRect\",\n\n \"drawCircle\",\n \"drawArc\",\n\n \"drawEllipse\",\n \"drawArcEllipse\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawBitmap\",\n\n \"selectSpriteSheet\",\n \"drawSprite\",\n \"drawSprites\",\n\n \"startSprite\",\n \"endSprite\",\n] as const;\nexport type DisplayContextCommandType =\n (typeof DisplayContextCommandTypes)[number];\n\nexport const DisplaySpriteContextCommandTypes = [\n \"selectFillColor\",\n \"selectLineColor\",\n // \"selectBackgroundColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n // \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setVerticalAlignment\",\n \"setHorizontalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"clearRect\",\n\n \"drawRect\",\n \"drawRoundRect\",\n \"drawCircle\",\n \"drawEllipse\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawArc\",\n \"drawArcEllipse\",\n\n \"drawBitmap\",\n \"drawSprite\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplaySpriteContextCommandType =\n (typeof DisplaySpriteContextCommandTypes)[number];\n\nexport interface BaseDisplayContextCommand {\n type: DisplayContextCommandType | \"runDisplayContextCommands\";\n hide?: boolean;\n}\n\nexport interface SimpleDisplayCommand extends BaseDisplayContextCommand {\n type:\n | \"show\"\n | \"clear\"\n | \"saveContext\"\n | \"restoreContext\"\n | \"clearRotation\"\n | \"clearCrop\"\n | \"clearRotationCrop\"\n | \"resetBitmapScale\"\n | \"resetSpriteColors\"\n | \"resetSpriteScale\"\n | \"resetAlignment\"\n | \"endSprite\";\n}\n\nexport interface SetDisplayColorCommand extends BaseDisplayContextCommand {\n type: \"setColor\";\n colorIndex: number;\n color: DisplayColorRGB | string;\n}\nexport interface SetDisplayColorOpacityCommand\n extends BaseDisplayContextCommand {\n type: \"setColorOpacity\";\n colorIndex: number;\n opacity: number;\n}\nexport interface SetDisplayOpacityCommand extends BaseDisplayContextCommand {\n type: \"setOpacity\";\n opacity: number;\n}\n\nexport interface SetDisplayHorizontalAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setHorizontalAlignment\";\n horizontalAlignment: DisplayAlignment;\n}\nexport interface SetDisplayVerticalAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setVerticalAlignment\";\n verticalAlignment: DisplayAlignment;\n}\n\nexport interface SelectDisplayBackgroundColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectBackgroundColor\";\n backgroundColorIndex: number;\n}\nexport interface SelectDisplayFillColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectFillColor\";\n fillColorIndex: number;\n}\nexport interface SelectDisplayLineColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectLineColor\";\n lineColorIndex: number;\n}\nexport interface SelectDisplayIgnoreFillCommand\n extends BaseDisplayContextCommand {\n type: \"setIgnoreFill\";\n ignoreFill: boolean;\n}\nexport interface SelectDisplayIgnoreLineCommand\n extends BaseDisplayContextCommand {\n type: \"setIgnoreLine\";\n ignoreLine: boolean;\n}\nexport interface SelectDisplayFillBackgroundCommand\n extends BaseDisplayContextCommand {\n type: \"setFillBackground\";\n fillBackground: boolean;\n}\nexport interface SetDisplayLineWidthCommand extends BaseDisplayContextCommand {\n type: \"setLineWidth\";\n lineWidth: number;\n}\nexport interface SetDisplayRotationCommand extends BaseDisplayContextCommand {\n type: \"setRotation\";\n rotation: number;\n isRadians?: boolean;\n}\n\nexport interface SetDisplaySegmentStartCapCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentStartCap\";\n segmentStartCap: DisplaySegmentCap;\n}\nexport interface SetDisplaySegmentEndCapCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentEndCap\";\n segmentEndCap: DisplaySegmentCap;\n}\nexport interface SetDisplaySegmentCapCommand extends BaseDisplayContextCommand {\n type: \"setSegmentCap\";\n segmentCap: DisplaySegmentCap;\n}\n\nexport interface SetDisplaySegmentStartRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentStartRadius\";\n segmentStartRadius: number;\n}\nexport interface SetDisplaySegmentEndRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentEndRadius\";\n segmentEndRadius: number;\n}\nexport interface SetDisplaySegmentRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentRadius\";\n segmentRadius: number;\n}\n\nexport interface SetDisplayCropTopCommand extends BaseDisplayContextCommand {\n type: \"setCropTop\";\n cropTop: number;\n}\nexport interface SetDisplayCropRightCommand extends BaseDisplayContextCommand {\n type: \"setCropRight\";\n cropRight: number;\n}\nexport interface SetDisplayCropBottomCommand extends BaseDisplayContextCommand {\n type: \"setCropBottom\";\n cropBottom: number;\n}\nexport interface SetDisplayCropLeftCommand extends BaseDisplayContextCommand {\n type: \"setCropLeft\";\n cropLeft: number;\n}\n\nexport interface SetDisplayRotationCropTopCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropTop\";\n rotationCropTop: number;\n}\nexport interface SetDisplayRotationCropRightCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropRight\";\n rotationCropRight: number;\n}\nexport interface SetDisplayRotationCropBottomCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropBottom\";\n rotationCropBottom: number;\n}\nexport interface SetDisplayRotationCropLeftCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropLeft\";\n rotationCropLeft: number;\n}\n\nexport interface SelectDisplayBitmapColorIndexCommand\n extends BaseDisplayContextCommand {\n type: \"selectBitmapColor\";\n bitmapColorIndex: number;\n colorIndex: number;\n}\nexport interface SelectDisplayBitmapColorIndicesCommand\n extends BaseDisplayContextCommand {\n type: \"selectBitmapColors\";\n bitmapColorPairs: DisplayBitmapColorPair[];\n}\n\nexport interface SetDisplayBitmapScaleXCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScaleX\";\n bitmapScaleX: number;\n}\nexport interface SetDisplayBitmapScaleYCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScaleY\";\n bitmapScaleY: number;\n}\nexport interface SetDisplayBitmapScaleCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScale\";\n bitmapScale: number;\n}\n\nexport interface SelectDisplaySpriteColorIndexCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteColor\";\n spriteColorIndex: number;\n colorIndex: number;\n}\nexport interface SelectDisplaySpriteColorIndicesCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteColors\";\n spriteColorPairs: DisplaySpriteColorPair[];\n}\n\nexport interface SetDisplaySpriteScaleXCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScaleX\";\n spriteScaleX: number;\n}\nexport interface SetDisplaySpriteScaleYCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScaleY\";\n spriteScaleY: number;\n}\nexport interface SetDisplaySpriteScaleCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScale\";\n spriteScale: number;\n}\n\nexport interface SetDisplaySpritesLineHeightCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineHeight\";\n spritesLineHeight: number;\n}\n\nexport interface SetDisplaySpritesDirectionCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesDirection\";\n spritesDirection: DisplayDirection;\n}\nexport interface SetDisplaySpritesLineDirectionCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineDirection\";\n spritesLineDirection: DisplayDirection;\n}\n\nexport interface SetDisplaySpritesSpacingCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesSpacing\";\n spritesSpacing: number;\n}\nexport interface SetDisplaySpritesLineSpacingCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineSpacing\";\n spritesLineSpacing: number;\n}\n\nexport interface SetDisplaySpritesAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesAlignment\";\n spritesAlignment: DisplayAlignment;\n}\nexport interface SetDisplaySpritesLineAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineAlignment\";\n spritesLineAlignment: DisplayAlignment;\n}\n\nexport interface BasePositionDisplayContextCommand\n extends BaseDisplayContextCommand {\n x: number;\n y: number;\n}\nexport interface BaseOffsetPositionDisplayContextCommand\n extends BaseDisplayContextCommand {\n offsetX: number;\n offsetY: number;\n}\nexport interface BaseSizeDisplayContextCommand\n extends BaseDisplayContextCommand {\n width: number;\n height: number;\n}\n\nexport interface BaseDisplayRectCommand\n extends BasePositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {}\nexport interface BaseDisplayCenterRectCommand\n extends BaseOffsetPositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {}\n\nexport interface ClearDisplayRectCommand extends BaseDisplayRectCommand {\n type: \"clearRect\";\n}\nexport interface DrawDisplayRectCommand extends BaseDisplayCenterRectCommand {\n type: \"drawRect\";\n}\n\nexport interface DrawDisplayRoundedRectCommand\n extends BaseOffsetPositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {\n type: \"drawRoundRect\";\n borderRadius: number;\n}\n\nexport interface DrawDisplayCircleCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawCircle\";\n radius: number;\n}\nexport interface DrawDisplayEllipseCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawEllipse\";\n radiusX: number;\n radiusY: number;\n}\n\nexport interface DrawDisplayRegularPolygonCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawRegularPolygon\";\n radius: number;\n numberOfSides: number;\n}\nexport interface DrawDisplayPolygonCommand extends BaseDisplayContextCommand {\n type: \"drawPolygon\";\n points: Vector2[];\n}\nexport interface DrawDisplaySegmentCommand extends BaseDisplayContextCommand {\n type: \"drawSegment\";\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}\nexport interface DrawDisplaySegmentsCommand extends BaseDisplayContextCommand {\n type: \"drawSegments\";\n points: Vector2[];\n}\n\nexport interface DrawDisplayBezierCurveCommand\n extends BaseDisplayContextCommand {\n type:\n | \"drawQuadraticBezierCurve\"\n | \"drawQuadraticBezierCurves\"\n | \"drawCubicBezierCurve\"\n | \"drawCubicBezierCurves\";\n controlPoints: Vector2[];\n}\n\nexport interface DrawDisplayPathCommand extends BaseDisplayContextCommand {\n type: \"drawPath\" | \"drawClosedPath\";\n curves: DisplayBezierCurve[];\n}\n\nexport interface DrawDisplayWireframeCommand extends BaseDisplayContextCommand {\n type: \"drawWireframe\";\n wireframe: DisplayWireframe;\n}\n\nexport interface DrawDisplayArcCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawArc\";\n radius: number;\n startAngle: number;\n angleOffset: number;\n isRadians?: boolean;\n}\nexport interface DrawDisplayArcEllipseCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawArcEllipse\";\n radiusX: number;\n radiusY: number;\n startAngle: number;\n angleOffset: number;\n isRadians?: boolean;\n}\n\nexport interface DrawDisplayBitmapCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawBitmap\";\n bitmap: DisplayBitmap;\n}\n\nexport interface SelectDisplaySpriteSheetCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteSheet\";\n spriteSheetIndex: number;\n}\n\nexport interface DrawDisplaySpriteCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawSprite\";\n spriteIndex: number;\n use2Bytes: boolean;\n}\n\nexport interface DrawDisplaySpritesCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawSprites\";\n spriteSerializedLines: DisplaySpriteSerializedLines;\n}\n\nexport interface StartDisplaySpriteCommand\n extends BaseDisplayCenterRectCommand {\n type: \"startSprite\";\n}\n\nexport type DisplayContextCommand =\n | SimpleDisplayCommand\n | SetDisplayColorCommand\n | SetDisplayColorOpacityCommand\n | SetDisplayOpacityCommand\n | SelectDisplayBackgroundColorCommand\n | SelectDisplayFillColorCommand\n | SelectDisplayLineColorCommand\n | SetDisplayLineWidthCommand\n | SetDisplayRotationCommand\n | SetDisplaySegmentStartCapCommand\n | SetDisplaySegmentEndCapCommand\n | SetDisplaySegmentCapCommand\n | SetDisplaySegmentStartRadiusCommand\n | SetDisplaySegmentEndRadiusCommand\n | SetDisplaySegmentRadiusCommand\n | SetDisplayCropTopCommand\n | SetDisplayCropRightCommand\n | SetDisplayCropBottomCommand\n | SetDisplayCropLeftCommand\n | SetDisplayRotationCropTopCommand\n | SetDisplayRotationCropRightCommand\n | SetDisplayRotationCropBottomCommand\n | SetDisplayRotationCropLeftCommand\n | SelectDisplayBitmapColorIndexCommand\n | SelectDisplayBitmapColorIndicesCommand\n | SetDisplayBitmapScaleXCommand\n | SetDisplayBitmapScaleYCommand\n | SetDisplayBitmapScaleCommand\n | SelectDisplaySpriteColorIndexCommand\n | SelectDisplaySpriteColorIndicesCommand\n | SetDisplaySpriteScaleXCommand\n | SetDisplaySpriteScaleYCommand\n | SetDisplaySpriteScaleCommand\n | ClearDisplayRectCommand\n | DrawDisplayRectCommand\n | DrawDisplayRoundedRectCommand\n | DrawDisplayCircleCommand\n | DrawDisplayEllipseCommand\n | DrawDisplayRegularPolygonCommand\n | DrawDisplayPolygonCommand\n | DrawDisplaySegmentCommand\n | DrawDisplaySegmentsCommand\n | DrawDisplayArcCommand\n | DrawDisplayArcEllipseCommand\n | DrawDisplayBitmapCommand\n | DrawDisplaySpriteCommand\n | DrawDisplaySpritesCommand\n | SelectDisplaySpriteSheetCommand\n | SetDisplayHorizontalAlignmentCommand\n | SetDisplayVerticalAlignmentCommand\n | SetDisplaySpritesDirectionCommand\n | SetDisplaySpritesLineDirectionCommand\n | SetDisplaySpritesSpacingCommand\n | SetDisplaySpritesLineSpacingCommand\n | SetDisplaySpritesAlignmentCommand\n | SetDisplaySpritesLineAlignmentCommand\n | SetDisplaySpritesLineHeightCommand\n | DrawDisplayWireframeCommand\n | DrawDisplayBezierCurveCommand\n | DrawDisplayPathCommand\n | SelectDisplayIgnoreFillCommand\n | SelectDisplayIgnoreLineCommand\n | SelectDisplayFillBackgroundCommand\n | StartDisplaySpriteCommand;\n\nexport function serializeContextCommand(\n displayManager: DisplayManagerInterface,\n command: DisplayContextCommand\n) {\n let dataView: DataView | undefined;\n\n switch (command.type) {\n case \"show\":\n case \"clear\":\n case \"saveContext\":\n case \"restoreContext\":\n case \"clearRotation\":\n case \"clearCrop\":\n case \"clearRotationCrop\":\n case \"resetBitmapScale\":\n case \"resetSpriteColors\":\n case \"resetSpriteScale\":\n case \"resetAlignment\":\n case \"endSprite\":\n break;\n case \"setColor\":\n {\n const { color, colorIndex } = command;\n\n let colorRGB: DisplayColorRGB;\n if (typeof color == \"string\") {\n colorRGB = stringToRGB(color);\n } else {\n colorRGB = color;\n }\n const colorHex = rgbToHex(colorRGB);\n if (displayManager.colors[colorIndex] == colorHex) {\n _console.log(`redundant color #${colorIndex} ${colorHex}`);\n return;\n }\n\n //_console.log(`setting color #${colorIndex}`, colorRGB);\n displayManager.assertValidColorIndex(colorIndex);\n assertValidColor(colorRGB);\n dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, colorRGB.r);\n dataView.setUint8(2, colorRGB.g);\n dataView.setUint8(3, colorRGB.b);\n }\n break;\n case \"setColorOpacity\":\n {\n const { colorIndex, opacity } = command;\n displayManager.assertValidColorIndex(colorIndex);\n assertValidOpacity(opacity);\n if (\n Math.floor(255 * displayManager.opacities[colorIndex]) ==\n Math.floor(255 * opacity)\n ) {\n _console.log(`redundant opacity #${colorIndex} ${opacity}`);\n return;\n }\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, opacity * 255);\n }\n break;\n case \"setOpacity\":\n {\n const { opacity } = command;\n assertValidOpacity(opacity);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, Math.round(opacity * 255));\n }\n break;\n case \"selectFillColor\":\n {\n const { fillColorIndex } = command;\n displayManager.assertValidColorIndex(fillColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, fillColorIndex);\n }\n break;\n case \"selectBackgroundColor\":\n {\n const { backgroundColorIndex } = command;\n displayManager.assertValidColorIndex(backgroundColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, backgroundColorIndex);\n }\n break;\n case \"selectLineColor\":\n {\n const { lineColorIndex } = command;\n displayManager.assertValidColorIndex(lineColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, lineColorIndex);\n }\n break;\n case \"setIgnoreFill\":\n {\n const { ignoreFill } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, ignoreFill ? 1 : 0);\n }\n break;\n case \"setIgnoreLine\":\n {\n const { ignoreLine } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, ignoreLine ? 1 : 0);\n }\n break;\n case \"setFillBackground\":\n {\n const { fillBackground } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, fillBackground ? 1 : 0);\n }\n break;\n case \"setLineWidth\":\n {\n const { lineWidth } = command;\n displayManager.assertValidLineWidth(lineWidth);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, lineWidth, true);\n }\n break;\n case \"setHorizontalAlignment\":\n {\n const { horizontalAlignment } = command;\n assertValidAlignment(horizontalAlignment);\n _console.log({ horizontalAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(horizontalAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setVerticalAlignment\":\n {\n const { verticalAlignment } = command;\n assertValidAlignment(verticalAlignment);\n _console.log({ verticalAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(verticalAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setRotation\":\n {\n let { rotation, isRadians } = command;\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation = normalizeRadians(rotation);\n isRadians = true;\n // _console.log({ rotation, isRadians });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, formatRotation(rotation, isRadians), true);\n }\n break;\n case \"setSegmentStartCap\":\n {\n const { segmentStartCap } = command;\n assertValidSegmentCap(segmentStartCap);\n _console.log({ segmentStartCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentStartCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentEndCap\":\n {\n const { segmentEndCap } = command;\n assertValidSegmentCap(segmentEndCap);\n _console.log({ segmentEndCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentEndCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentCap\":\n {\n const { segmentCap } = command;\n assertValidSegmentCap(segmentCap);\n _console.log({ segmentCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentStartRadius\":\n {\n const { segmentStartRadius } = command;\n _console.log({ segmentStartRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentStartRadius, true);\n }\n break;\n case \"setSegmentEndRadius\":\n {\n const { segmentEndRadius } = command;\n _console.log({ segmentEndRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentEndRadius, true);\n }\n break;\n case \"setSegmentRadius\":\n {\n const { segmentRadius } = command;\n _console.log({ segmentRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentRadius, true);\n }\n break;\n case \"setCropTop\":\n {\n const { cropTop } = command;\n _console.log({ cropTop });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropTop, true);\n }\n break;\n case \"setCropRight\":\n {\n const { cropRight } = command;\n _console.log({ cropRight });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropRight, true);\n }\n break;\n case \"setCropBottom\":\n {\n const { cropBottom } = command;\n _console.log({ cropBottom });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropBottom, true);\n }\n break;\n case \"setCropLeft\":\n {\n const { cropLeft } = command;\n _console.log({ cropLeft });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropLeft, true);\n }\n break;\n case \"setRotationCropTop\":\n {\n const { rotationCropTop } = command;\n _console.log({ rotationCropTop });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropTop, true);\n }\n break;\n case \"setRotationCropRight\":\n {\n const { rotationCropRight } = command;\n _console.log({ rotationCropRight });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropRight, true);\n }\n break;\n case \"setRotationCropBottom\":\n {\n const { rotationCropBottom } = command;\n _console.log({ rotationCropBottom });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropBottom, true);\n }\n break;\n case \"setRotationCropLeft\":\n {\n const { rotationCropLeft } = command;\n _console.log({ rotationCropLeft });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropLeft, true);\n }\n break;\n case \"selectBitmapColor\":\n {\n const { bitmapColorIndex, colorIndex } = command;\n displayManager.assertValidColorIndex(bitmapColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, bitmapColorIndex);\n dataView.setUint8(1, colorIndex);\n }\n break;\n case \"selectBitmapColors\":\n {\n const { bitmapColorPairs } = command;\n\n _console.assertRangeWithError(\n \"bitmapColors\",\n bitmapColorPairs.length,\n 1,\n displayManager.numberOfColors\n );\n const bitmapColorIndices =\n displayManager.contextState.bitmapColorIndices.slice();\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n displayManager.assertValidColorIndex(bitmapColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n });\n\n dataView = new DataView(\n new ArrayBuffer(bitmapColorPairs.length * 2 + 1)\n );\n let offset = 0;\n dataView.setUint8(offset++, bitmapColorPairs.length);\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n dataView!.setUint8(offset, bitmapColorIndex);\n dataView!.setUint8(offset + 1, colorIndex);\n offset += 2;\n });\n }\n break;\n case \"setBitmapScaleX\":\n {\n let { bitmapScaleX } = command;\n bitmapScaleX = clamp(bitmapScaleX, minDisplayScale, maxDisplayScale);\n bitmapScaleX = roundScale(bitmapScaleX);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScaleX), true);\n }\n break;\n case \"setBitmapScaleY\":\n {\n let { bitmapScaleY } = command;\n bitmapScaleY = clamp(bitmapScaleY, minDisplayScale, maxDisplayScale);\n bitmapScaleY = roundScale(bitmapScaleY);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScaleY), true);\n }\n break;\n case \"setBitmapScale\":\n {\n let { bitmapScale } = command;\n bitmapScale = clamp(bitmapScale, minDisplayScale, maxDisplayScale);\n bitmapScale = roundScale(bitmapScale);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScale), true);\n }\n break;\n case \"selectSpriteColor\":\n {\n const { spriteColorIndex, colorIndex } = command;\n displayManager.assertValidColorIndex(spriteColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, spriteColorIndex);\n dataView.setUint8(1, colorIndex);\n }\n break;\n case \"selectSpriteColors\":\n {\n const { spriteColorPairs } = command;\n _console.assertRangeWithError(\n \"spriteColors\",\n spriteColorPairs.length,\n 1,\n displayManager.numberOfColors\n );\n const spriteColorIndices =\n displayManager.contextState.spriteColorIndices.slice();\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n displayManager.assertValidColorIndex(spriteColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n spriteColorIndices[spriteColorIndex] = colorIndex;\n });\n\n dataView = new DataView(\n new ArrayBuffer(spriteColorPairs.length * 2 + 1)\n );\n let offset = 0;\n dataView.setUint8(offset++, spriteColorPairs.length);\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n dataView!.setUint8(offset, spriteColorIndex);\n dataView!.setUint8(offset + 1, colorIndex);\n offset += 2;\n });\n }\n break;\n case \"setSpriteScaleX\":\n {\n let { spriteScaleX } = command;\n spriteScaleX = clamp(spriteScaleX, minDisplayScale, maxDisplayScale);\n spriteScaleX = roundScale(spriteScaleX);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScaleX), true);\n }\n break;\n case \"setSpriteScaleY\":\n {\n let { spriteScaleY } = command;\n spriteScaleY = clamp(spriteScaleY, minDisplayScale, maxDisplayScale);\n spriteScaleY = roundScale(spriteScaleY);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScaleY), true);\n }\n break;\n case \"setSpriteScale\":\n {\n let { spriteScale } = command;\n spriteScale = clamp(spriteScale, minDisplayScale, maxDisplayScale);\n spriteScale = roundScale(spriteScale);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScale), true);\n }\n break;\n case \"setSpritesLineHeight\":\n {\n const { spritesLineHeight } = command;\n displayManager.assertValidLineWidth(spritesLineHeight);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, spritesLineHeight, true);\n }\n break;\n case \"setSpritesDirection\":\n {\n const { spritesDirection } = command;\n assertValidDirection(spritesDirection);\n _console.log({ spritesDirection });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayDirections.indexOf(spritesDirection);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesLineDirection\":\n {\n const { spritesLineDirection } = command;\n assertValidDirection(spritesLineDirection);\n _console.log({ spritesLineDirection });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayDirections.indexOf(spritesLineDirection);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesSpacing\":\n {\n const { spritesSpacing } = command;\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, spritesSpacing, true);\n }\n break;\n case \"setSpritesLineSpacing\":\n {\n const { spritesLineSpacing } = command;\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, spritesLineSpacing, true);\n }\n break;\n case \"setSpritesAlignment\":\n {\n const { spritesAlignment } = command;\n assertValidAlignment(spritesAlignment);\n _console.log({ spritesAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(spritesAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesLineAlignment\":\n {\n const { spritesLineAlignment } = command;\n assertValidAlignment(spritesLineAlignment);\n _console.log({ spritesLineAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(spritesLineAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"clearRect\":\n {\n const { x, y, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, x, true);\n dataView.setInt16(2, y, true);\n dataView.setInt16(4, width, true);\n dataView.setInt16(6, height, true);\n }\n break;\n case \"drawRect\":\n {\n const { offsetX, offsetY, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n }\n break;\n case \"drawRoundRect\":\n {\n const { offsetX, offsetY, width, height, borderRadius } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4 + 1));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n dataView.setUint8(8, borderRadius);\n }\n break;\n case \"drawCircle\":\n {\n const { offsetX, offsetY, radius } = command;\n dataView = new DataView(new ArrayBuffer(2 * 3));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n }\n break;\n case \"drawEllipse\":\n {\n const { offsetX, offsetY, radiusX, radiusY } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radiusX, true);\n dataView.setUint16(6, radiusY, true);\n }\n break;\n case \"drawRegularPolygon\":\n {\n const { offsetX, offsetY, radius, numberOfSides } = command;\n dataView = new DataView(new ArrayBuffer(2 * 3 + 1));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n dataView.setUint8(6, numberOfSides);\n }\n break;\n case \"drawPolygon\":\n {\n const { points } = command;\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n dataView = serializePoints(points);\n }\n break;\n case \"drawWireframe\":\n {\n const { wireframe } = command;\n const { points, edges } = wireframe;\n if (wireframe.points.length == 0) {\n return;\n }\n assertValidWireframe(wireframe);\n // [pointDataType, numberOfPoints, ...points, numberOfEdges, ...edges]\n const pointsDataView = serializePoints(points);\n\n const edgesDataView = new DataView(\n new ArrayBuffer(1 + 2 * edges.length)\n );\n let edgesDataOffset = 0;\n edgesDataView.setUint8(edgesDataOffset++, edges.length);\n edges.forEach((edge) => {\n edgesDataView.setUint8(edgesDataOffset++, edge.startIndex);\n edgesDataView.setUint8(edgesDataOffset++, edge.endIndex);\n });\n\n dataView = new DataView(\n concatenateArrayBuffers(pointsDataView, edgesDataView)\n );\n }\n break;\n case \"drawQuadraticBezierCurve\":\n case \"drawCubicBezierCurve\":\n {\n const { controlPoints } = command;\n const curveType: DisplayBezierCurveType =\n command.type == \"drawCubicBezierCurve\" ? \"cubic\" : \"quadratic\";\n assertValidNumberOfControlPoints(curveType, controlPoints);\n dataView = new DataView(new ArrayBuffer(4 * controlPoints.length));\n let offset = 0;\n controlPoints.forEach((controlPoint) => {\n dataView!.setInt16(offset, controlPoint.x, true);\n offset += 2;\n dataView!.setInt16(offset, controlPoint.y, true);\n offset += 2;\n });\n }\n break;\n case \"drawQuadraticBezierCurves\":\n case \"drawCubicBezierCurves\":\n {\n const { controlPoints } = command;\n const curveType: DisplayBezierCurveType =\n command.type == \"drawCubicBezierCurves\" ? \"cubic\" : \"quadratic\";\n assertValidPathNumberOfControlPoints(curveType, controlPoints);\n dataView = serializePoints(controlPoints);\n }\n break;\n case \"drawPath\":\n case \"drawClosedPath\":\n {\n const { curves } = command;\n // _console.log(\"curves\", curves);\n assertValidPath(curves);\n const typesDataView = new DataView(\n new ArrayBuffer(Math.ceil(curves.length / displayCurveTypesPerByte))\n );\n // _console.log({ \"curves.length\": curves.length, typesDataView });\n const controlPointsDataViews: DataView[] = [];\n\n // [pointDataType, numberOfCurves, numberOfPoints, ...curveTypes, ...points]\n\n const allControlPoints: Vector2[] = [];\n curves.forEach((curve) => {\n allControlPoints.push(...curve.controlPoints);\n });\n const pointDataType = getPointDataType(allControlPoints);\n const numberOfControlPoints = allControlPoints.length;\n _console.log({ numberOfControlPoints });\n\n curves.forEach((curve, index) => {\n const { type, controlPoints } = curve;\n const typeByteIndex = Math.floor(index / displayCurveTypesPerByte);\n const typeBitShift =\n (index % displayCurveTypesPerByte) * displayCurveTypeBitWidth;\n // _console.log({ type, typeByteIndex, typeBitShift });\n let typeValue = typesDataView.getUint8(typeByteIndex) || 0;\n typeValue |= DisplayBezierCurveTypes.indexOf(type) << typeBitShift;\n typesDataView.setUint8(typeByteIndex, typeValue);\n\n const controlPointsDataView = serializePoints(\n controlPoints,\n pointDataType,\n true\n );\n controlPointsDataViews.push(controlPointsDataView);\n });\n\n const controlPointsBuffer = concatenateArrayBuffers(\n ...controlPointsDataViews\n );\n const headerDataView = new DataView(new ArrayBuffer(3));\n headerDataView.setUint8(\n 0,\n DisplayPointDataTypes.indexOf(pointDataType)\n );\n headerDataView.setUint8(1, curves.length);\n headerDataView.setUint8(2, numberOfControlPoints);\n dataView = new DataView(\n concatenateArrayBuffers(\n headerDataView,\n typesDataView,\n controlPointsBuffer\n )\n );\n }\n break;\n case \"drawSegment\":\n {\n const { startX, startY, endX, endY } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, startX, true);\n dataView.setInt16(2, startY, true);\n dataView.setInt16(4, endX, true);\n dataView.setInt16(6, endY, true);\n }\n break;\n case \"drawSegments\":\n {\n const { points } = command;\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n dataView = serializePoints(points);\n }\n break;\n case \"drawArc\":\n {\n let { offsetX, offsetY, radius, isRadians, startAngle, angleOffset } =\n command;\n\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n startAngle = normalizeRadians(startAngle);\n\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n angleOffset = clamp(angleOffset, -twoPi, twoPi);\n\n angleOffset /= twoPi;\n angleOffset *= (angleOffset > 0 ? Int16Max - 1 : -Int16Min) - 1;\n\n isRadians = true;\n\n dataView = new DataView(new ArrayBuffer(2 * 5));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n dataView.setUint16(6, formatRotation(startAngle, isRadians), true);\n dataView.setInt16(8, angleOffset, true);\n }\n break;\n case \"drawArcEllipse\":\n {\n let {\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n isRadians,\n startAngle,\n angleOffset,\n } = command;\n\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n startAngle = normalizeRadians(startAngle);\n\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n angleOffset = clamp(angleOffset, -twoPi, twoPi);\n\n angleOffset /= twoPi;\n angleOffset *= (angleOffset > 0 ? Int16Max : -Int16Min) - 1;\n\n isRadians = true;\n\n dataView = new DataView(new ArrayBuffer(2 * 6));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radiusX, true);\n dataView.setUint16(6, radiusY, true);\n dataView.setUint16(8, formatRotation(startAngle, isRadians), true);\n dataView.setUint16(10, angleOffset, true);\n }\n break;\n case \"drawBitmap\":\n {\n const { bitmap, offsetX, offsetY } = command;\n displayManager.assertValidBitmap(bitmap, false);\n dataView = new DataView(new ArrayBuffer(drawBitmapHeaderLength));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, bitmap.width, true);\n dataView.setUint32(6, bitmap.pixels.length, true);\n dataView.setUint8(10, bitmap.numberOfColors);\n\n const bitmapData = getBitmapData(bitmap);\n dataView.setUint16(11, bitmapData.byteLength, true);\n const buffer = concatenateArrayBuffers(dataView, bitmapData);\n dataView = new DataView(buffer);\n }\n break;\n case \"selectSpriteSheet\":\n {\n const { spriteSheetIndex } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, spriteSheetIndex);\n }\n break;\n case \"drawSprite\":\n {\n const { offsetX, offsetY, spriteIndex, use2Bytes } = command;\n dataView = new DataView(new ArrayBuffer(2 * 2 + (use2Bytes ? 2 : 1)));\n let offset = 0;\n dataView.setInt16(offset, offsetX, true);\n offset += 2;\n dataView.setInt16(offset, offsetY, true);\n offset += 2;\n if (use2Bytes) {\n dataView.setUint16(offset, spriteIndex, true);\n offset += 2;\n } else {\n dataView.setUint8(offset++, spriteIndex!);\n }\n }\n break;\n case \"drawSprites\":\n {\n const { offsetX, offsetY, spriteSerializedLines } = command;\n const lineArrayBuffers: ArrayBuffer[] = [];\n spriteSerializedLines.forEach((spriteLines) => {\n const subLineArrayBuffers: ArrayBuffer[] = [];\n spriteLines.forEach((subSpriteLine) => {\n const { spriteSheetIndex, spriteIndices, use2Bytes } =\n subSpriteLine;\n const subLineSpriteIndicesDataView = new DataView(\n new ArrayBuffer(spriteIndices.length * (use2Bytes ? 2 : 1))\n );\n spriteIndices.forEach((spriteIndex, i) => {\n if (use2Bytes) {\n subLineSpriteIndicesDataView.setUint16(\n i * 2,\n spriteIndex,\n true\n );\n } else {\n subLineSpriteIndicesDataView.setUint8(i, spriteIndex);\n }\n });\n const subLineHeaderDataView = new DataView(new ArrayBuffer(2));\n subLineHeaderDataView.setUint8(0, spriteSheetIndex);\n subLineHeaderDataView.setUint8(1, spriteIndices.length);\n subLineArrayBuffers.push(\n concatenateArrayBuffers(\n subLineHeaderDataView,\n subLineSpriteIndicesDataView\n )\n );\n });\n const lineArrayHeaderDataView = new DataView(new ArrayBuffer(2));\n const concatenatedSubLineArrayBuffers = concatenateArrayBuffers(\n ...subLineArrayBuffers\n );\n lineArrayHeaderDataView.setUint16(\n 0,\n concatenatedSubLineArrayBuffers.byteLength,\n true\n );\n lineArrayBuffers.push(\n concatenateArrayBuffers(\n lineArrayHeaderDataView,\n concatenatedSubLineArrayBuffers\n )\n );\n });\n\n const concatenatedLineArrayBuffers = concatenateArrayBuffers(\n ...lineArrayBuffers\n );\n\n dataView = new DataView(new ArrayBuffer(2 * 3));\n let offset = 0;\n dataView.setInt16(offset, offsetX, true);\n offset += 2;\n dataView.setInt16(offset, offsetY, true);\n offset += 2;\n dataView.setUint16(\n offset,\n concatenatedLineArrayBuffers.byteLength,\n true\n );\n offset += 2;\n\n const buffer = concatenateArrayBuffers(\n dataView,\n concatenatedLineArrayBuffers\n );\n dataView = new DataView(buffer);\n }\n break;\n case \"startSprite\":\n {\n const { offsetX, offsetY, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n }\n break;\n }\n\n return dataView;\n}\nexport function serializeContextCommands(\n displayManager: DisplayManagerInterface,\n commands: DisplayContextCommand[]\n) {\n const serializedContextCommandArray = commands\n .filter((command) => !command.hide)\n .map((command) => {\n const displayContextCommandEnum = DisplayContextCommandTypes.indexOf(\n command.type\n );\n const serializedContextCommand = serializeContextCommand(\n displayManager,\n command\n );\n return concatenateArrayBuffers(\n UInt8ByteBuffer(displayContextCommandEnum),\n serializedContextCommand\n );\n });\n const serializedContextCommands = concatenateArrayBuffers(\n serializedContextCommandArray\n );\n _console.log(\n \"serializedContextCommands\",\n commands,\n serializedContextCommandArray,\n serializedContextCommands\n );\n return serializedContextCommands;\n}\n\nconst DrawDisplayContextCommandTypes = [\n \"drawRect\",\n \"drawRoundRect\",\n\n \"drawCircle\",\n \"drawArc\",\n\n \"drawEllipse\",\n \"drawArcEllipse\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawBitmap\",\n\n \"drawSprite\",\n \"drawSprites\",\n] as const satisfies readonly DisplayContextCommandType[];\ntype DrawDisplayContextCommandType =\n (typeof DrawDisplayContextCommandTypes)[number];\n\nconst StateDisplayContextCommandTypes = [\n \"setColor\",\n \"setColorOpacity\",\n \"setOpacity\",\n\n \"saveContext\",\n \"restoreContext\",\n\n \"selectBackgroundColor\",\n \"selectFillColor\",\n \"selectLineColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setHorizontalAlignment\",\n \"setVerticalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"selectSpriteSheet\",\n] as const satisfies readonly DisplayContextCommandType[];\ntype StateDisplayContextCommandType =\n (typeof StateDisplayContextCommandTypes)[number];\n\nconst SpritesDisplayContextCommandTypes = [\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"selectSpriteSheet\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type SpritesDisplayContextCommandType =\n (typeof SpritesDisplayContextCommandTypes)[number];\n\nconst PathDrawDisplayContextCommandTypes = [\n \"drawSegment\",\n \"drawSegments\",\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n \"drawPath\",\n \"drawWireframe\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type PathDrawDisplayContextCommandType =\n (typeof PathDrawDisplayContextCommandTypes)[number];\n\nconst PathStateDisplayContextCommandTypes = [\n \"setSegmentRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentStartRadius\",\n \"setSegmentCap\",\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type PathStateDisplayContextCommandType =\n (typeof PathStateDisplayContextCommandTypes)[number];\n\nconst BitmapDisplayContextCommandTypes = [\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type BitmapDisplayContextCommandType =\n (typeof BitmapDisplayContextCommandTypes)[number];\n\nconst contextCommandDependencies: Map<\n Set<DisplayContextCommandType>,\n Set<DisplayContextCommandType>\n> = new Map();\nfunction appendContextCommandDependencyPair(\n key: DisplayContextCommandType[],\n value: DisplayContextCommandType[]\n) {\n contextCommandDependencies.set(new Set(key), new Set(value));\n}\nappendContextCommandDependencyPair(\n [...PathStateDisplayContextCommandTypes],\n [...PathDrawDisplayContextCommandTypes]\n);\nappendContextCommandDependencyPair(\n [...StateDisplayContextCommandTypes],\n [...DrawDisplayContextCommandTypes]\n);\nappendContextCommandDependencyPair(\n [...SpritesDisplayContextCommandTypes],\n [\"drawSprite\", \"drawSprites\"]\n);\nappendContextCommandDependencyPair(\n [...BitmapDisplayContextCommandTypes],\n [\"drawBitmap\"]\n);\n\n// TODO - can refine more (e.g. if ignoreLine, then skip setLineWidth, or skip if a set value is already default, etc)\n\nexport function trimContextCommands(commands: DisplayContextCommand[]) {\n _console.log(\"trimming commands\", commands);\n const trimmedCommands: DisplayContextCommand[] = [];\n\n commands\n .slice()\n .reverse()\n .forEach((command) => {\n let include = true;\n\n let dependencies: Set<DisplayContextCommandType> | undefined;\n for (const [keys, values] of contextCommandDependencies) {\n if (keys.has(command.type)) {\n dependencies = values;\n break;\n }\n }\n\n //_console.log(\"command\", command, \"dependencies\", dependencies);\n\n if (dependencies) {\n const similarCommandIndex = trimmedCommands.findIndex(\n (trimmedCommand) => {\n return trimmedCommand.type == command.type;\n }\n );\n const dependentCommandIndex = trimmedCommands.findIndex(\n (trimmedCommand) => dependencies.has(trimmedCommand.type)\n );\n\n //_console.log({ similarCommandIndex, dependentCommandIndex });\n\n if (dependentCommandIndex == -1) {\n include = false;\n } else if (similarCommandIndex != -1) {\n include = similarCommandIndex > dependentCommandIndex;\n }\n }\n if (include) {\n trimmedCommands.unshift(command);\n } else {\n //_console.log(\"skipping command\", command);\n }\n });\n\n _console.log(\"trimmedCommands\", trimmedCommands);\n return trimmedCommands;\n}\n","import { createConsole } from \"./Console.ts\";\nimport { Vector2 } from \"./MathUtils.ts\";\nimport { DisplayBezierCurve } from \"../DisplayManager.ts\";\nimport simplify from \"simplify-js\";\nimport fitCurve from \"fit-curve\";\n\nconst _console = createConsole(\"PathUtils\", { log: false });\n\nfunction perpendicularDistance(p: Vector2, p1: Vector2, p2: Vector2): number {\n const dx = p2.x - p1.x;\n const dy = p2.y - p1.y;\n if (dx === 0 && dy === 0) return Math.hypot(p.x - p1.x, p.y - p1.y);\n const t = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);\n const projX = p1.x + t * dx;\n const projY = p1.y + t * dy;\n return Math.hypot(p.x - projX, p.y - projY);\n}\n\nfunction rdp(points: Vector2[], epsilon: number): Vector2[] {\n if (points.length < 3) return points;\n let maxDist = 0;\n let index = 0;\n for (let i = 1; i < points.length - 1; i++) {\n const d = perpendicularDistance(\n points[i],\n points[0],\n points[points.length - 1]\n );\n if (d > maxDist) {\n maxDist = d;\n index = i;\n }\n }\n if (maxDist > epsilon) {\n const left = rdp(points.slice(0, index + 1), epsilon);\n const right = rdp(points.slice(index), epsilon);\n return left.slice(0, -1).concat(right);\n }\n return [points[0], points[points.length - 1]];\n}\n\n// Linear interpolation\nfunction lerp(a: number, b: number, t: number) {\n return a + (b - a) * t;\n}\n\n// Sample quadratic Bezier\nfunction sampleQuadratic(\n p0: Vector2,\n p1: Vector2,\n p2: Vector2,\n steps: number = 5\n): Vector2[] {\n const points: Vector2[] = [];\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n const x = (1 - t) ** 2 * p0.x + 2 * (1 - t) * t * p1.x + t ** 2 * p2.x;\n const y = (1 - t) ** 2 * p0.y + 2 * (1 - t) * t * p1.y + t ** 2 * p2.y;\n points.push({ x, y });\n }\n return points;\n}\n\n// Sample cubic Bezier\nfunction sampleCubic(\n p0: Vector2,\n p1: Vector2,\n p2: Vector2,\n p3: Vector2,\n steps: number = 5\n): Vector2[] {\n const points: Vector2[] = [];\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n const mt = 1 - t;\n const x =\n mt ** 3 * p0.x +\n 3 * mt ** 2 * t * p1.x +\n 3 * mt * t ** 2 * p2.x +\n t ** 3 * p3.x;\n const y =\n mt ** 3 * p0.y +\n 3 * mt ** 2 * t * p1.y +\n 3 * mt * t ** 2 * p2.y +\n t ** 3 * p3.y;\n points.push({ x, y });\n }\n return points;\n}\n\nfunction areCollinear(\n p1: Vector2,\n p2: Vector2,\n p3: Vector2,\n epsilon = 1e-6\n): boolean {\n // Vector p1->p2\n const dx1 = p2.x - p1.x;\n const dy1 = p2.y - p1.y;\n\n // Vector p2->p3\n const dx2 = p3.x - p2.x;\n const dy2 = p3.y - p2.y;\n\n // Cross product\n const cross = dx1 * dy2 - dy1 * dx2;\n return Math.abs(cross) < epsilon;\n}\n\nexport function simplifyCurves(curves: DisplayBezierCurve[], epsilon = 1) {\n const simplified: DisplayBezierCurve[] = [];\n //_console.log(\"simplifying\", curves, { epsilon });\n let cursor: Vector2;\n curves.forEach((curve, index) => {\n const { controlPoints } = curve;\n const isFirst = index == 0;\n if (isFirst) {\n cursor = controlPoints[0];\n }\n\n switch (curve.type) {\n case \"segment\":\n {\n // Merge collinear lines\n const lastPoint = controlPoints.at(-1)!;\n const lastCommand = simplified.at(-1);\n if (lastCommand?.type == \"segment\" && simplified.length >= 2) {\n const [c1, c2] = [simplified.at(-1)!, simplified.at(-2)!];\n if (\n areCollinear(\n c2.controlPoints.at(-1)!,\n c1.controlPoints.at(-1)!,\n lastPoint\n )\n ) {\n // Remove middle collinear point\n simplified.pop();\n }\n }\n simplified.push({ ...curve });\n cursor = lastPoint;\n }\n break;\n case \"quadratic\":\n {\n const p0 = cursor;\n const p1 = controlPoints.at(-2)!;\n const p2 = controlPoints.at(-1)!;\n\n // Sample points along the curve\n const sampled = sampleQuadratic(p0, p1, p2, 5);\n const simplifiedPoints = rdp(sampled, epsilon);\n\n // If curve is almost straight, convert to a line\n if (simplifiedPoints.length === 2) {\n simplified.push({\n type: \"segment\",\n controlPoints: [{ x: p2.x, y: p2.y }],\n });\n if (isFirst) {\n simplified.at(-1)!.controlPoints.unshift({ ...p0 });\n }\n } else {\n simplified.push({ ...curve }); // Keep the curve\n }\n cursor = p2;\n }\n break;\n case \"cubic\":\n {\n const p0 = cursor;\n const p1 = controlPoints.at(-3)!;\n const p2 = controlPoints.at(-2)!;\n const p3 = controlPoints.at(-1)!;\n\n const sampled = sampleCubic(p0, p1, p2, p3, 5);\n const simplifiedPoints = rdp(sampled, epsilon);\n\n if (simplifiedPoints.length === 2) {\n simplified.push({\n type: \"segment\",\n controlPoints: [{ x: p3.x, y: p3.y }],\n });\n if (isFirst) {\n simplified.at(-1)!.controlPoints.unshift({ ...p0 });\n }\n } else {\n simplified.push({ ...curve }); // Keep the curve\n }\n cursor = p3;\n }\n break;\n }\n cursor = curve.controlPoints[curve.controlPoints.length - 1];\n });\n //_console.log(\"simplified\", simplified);\n return simplified;\n}\n\nexport function simplifyPoints(points: Vector2[], tolerance?: number) {\n points = simplify(points, tolerance, false);\n return points;\n}\nexport function simplifyPointsAsCubicCurveControlPoints(\n points: Vector2[],\n error?: number\n) {\n const flatPoints = points.map(({ x, y }) => [x, y]);\n const curves = fitCurve(flatPoints, error ?? 50);\n const controlPoints: Vector2[] = [];\n curves.forEach((curve, index) => {\n const points = curve.map(([x, y]) => ({ x, y }));\n if (index != 0) {\n points.shift();\n }\n controlPoints.push(...points);\n });\n return controlPoints;\n}\n","import { createConsole } from \"./Console.ts\";\nimport {\n DisplayContextCommand,\n trimContextCommands,\n} from \"./DisplayContextCommand.ts\";\nimport { INode, parseSync } from \"svgson\";\nimport { SVGPathData } from \"svg-pathdata\";\nimport { DisplayBezierCurve, DisplaySize } from \"../DisplayManager.ts\";\nimport { pointInPolygon, Vector2 } from \"./MathUtils.ts\";\nimport {\n contourArea,\n DisplaySprite,\n DisplaySpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\nimport { simplifyCurves } from \"./PathUtils.ts\";\nimport { DisplayBoundingBox } from \"./DisplayCanvasHelper.ts\";\nimport RangeHelper from \"./RangeHelper.ts\";\nimport { kMeansColors, mapToClosestPaletteIndex } from \"./ColorUtils.ts\";\n\nconst _console = createConsole(\"SvgUtils\", { log: false });\n\ntype FillRule = \"nonzero\" | \"evenodd\";\ntype CanvasCommand =\n | { type: \"lineWidth\"; lineWidth: number }\n | { type: \"fillStyle\"; fillStyle: string }\n | { type: \"strokeStyle\"; strokeStyle: string }\n | { type: \"fillRule\"; fillRule: FillRule }\n | { type: \"pathStart\" | \"pathEnd\" }\n | { type: \"moveTo\" | \"lineTo\"; x: number; y: number }\n | { type: \"line\"; x1: number; y1: number; x2: number; y2: number }\n | { type: \"quadraticCurveTo\"; cpx: number; cpy: number; x: number; y: number }\n | {\n type: \"bezierCurveTo\";\n cp1x: number;\n cp1y: number;\n cp2x: number;\n cp2y: number;\n x: number;\n y: number;\n }\n | { type: \"closePath\"; checkIfHole?: boolean }\n | {\n type: \"rect\";\n x: number;\n y: number;\n width: number;\n height: number;\n rotation: number;\n }\n | {\n type: \"roundRect\";\n x: number;\n y: number;\n width: number;\n height: number;\n r: number;\n rotation: number;\n }\n | { type: \"circle\"; x: number; y: number; r: number }\n | {\n type: \"ellipse\";\n x: number;\n y: number;\n rx: number;\n ry: number;\n rotation: number;\n };\n\ninterface Transform {\n a: number;\n b: number;\n c: number;\n d: number;\n e: number;\n f: number;\n}\n\ninterface DecomposedTransform {\n translation: { x: number; y: number };\n rotation: number; // in radians\n scale: { x: number; y: number };\n skew: { x: number; y: number }; // skewX/Y in radians\n isScaleUniform: boolean; // true if scaleX ≈ scaleY\n}\n\n/** Fully decompose a 2D affine transform */\nfunction decomposeTransform(\n t: Transform,\n tolerance = 1e-6\n): DecomposedTransform {\n // Translation\n const tx = t.e;\n const ty = t.f;\n\n // Compute scale\n const scaleX = Math.sqrt(t.a * t.a + t.b * t.b);\n const scaleY = Math.sqrt(t.c * t.c + t.d * t.d);\n\n // Compute rotation (from X-axis)\n let rotation = 0;\n if (scaleX !== 0) {\n rotation = Math.atan2(t.b / scaleX, t.a / scaleX);\n }\n\n // Compute skew (skewX = angle between x and y axes)\n let skewX = 0;\n let skewY = 0;\n if (scaleX !== 0 && scaleY !== 0) {\n skewX = Math.atan2(t.a * t.c + t.b * t.d, scaleX * scaleX);\n skewY = 0; // rarely needed, can be calculated similarly if desired\n }\n\n // Uniform scale check\n const isScaleUniform = Math.abs(scaleX - scaleY) < tolerance;\n\n return {\n translation: { x: tx, y: ty },\n rotation,\n scale: { x: scaleX, y: scaleY },\n skew: { x: skewX, y: skewY },\n isScaleUniform,\n };\n}\n\nconst identity: Transform = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };\n\nfunction multiply(t1: Transform, t2: Transform): Transform {\n //_console.log(\"multiplying matrices\", t1, t2);\n return {\n a: t1.a * t2.a + t1.c * t2.b,\n b: t1.b * t2.a + t1.d * t2.b,\n c: t1.a * t2.c + t1.c * t2.d,\n d: t1.b * t2.c + t1.d * t2.d,\n e: t1.a * t2.e + t1.c * t2.f + t1.e,\n f: t1.b * t2.e + t1.d * t2.f + t1.f,\n };\n}\n\nfunction parseTransform(transformStr: string): Transform {\n // Very basic parser, handles translate, scale, rotate, matrix\n if (!transformStr) return identity;\n\n const t = transformStr.match(/(\\w+)\\(([^)]+)\\)/g);\n if (!t) return identity;\n\n let matrix = structuredClone(identity);\n\n for (const part of t) {\n const [, fn, argsStr] = /(\\w+)\\(([^)]+)\\)/.exec(part)!;\n const args = argsStr.split(/[\\s,]+/).map(Number);\n let m: Transform = structuredClone(identity);\n\n switch (fn) {\n case \"translate\":\n //_console.log(\"translate\", { x: args[0], y: args[1] });\n m.e = args[0];\n m.f = args[1] || 0;\n break;\n case \"scale\":\n //_console.log(\"scale\", { x: args[0], y: args[1] });\n m.a = args[0];\n m.d = args[1] !== undefined ? args[1] : args[0];\n break;\n case \"rotate\":\n const angle = (args[0] * Math.PI) / 180;\n //_console.log(\"rotate\", { angle });\n const cos = Math.cos(angle),\n sin = Math.sin(angle);\n if (args[1] !== undefined && args[2] !== undefined) {\n const [cx, cy] = [args[1], args[2]];\n m = {\n a: cos,\n b: sin,\n c: -sin,\n d: cos,\n e: cx - cos * cx + sin * cy,\n f: cy - sin * cx - cos * cy,\n };\n } else {\n m.a = cos;\n m.b = sin;\n m.c = -sin;\n m.d = cos;\n }\n break;\n case \"matrix\":\n //_console.log(\"matrix\", args);\n [m.a, m.b, m.c, m.d, m.e, m.f] = args;\n break;\n }\n\n matrix = multiply(matrix, m);\n }\n\n //_console.log(\"parsedTransform\", matrix);\n return matrix;\n}\n\nfunction applyTransform(x: number, y: number, t: Transform) {\n //_console.log(\"applying transform\", { x, y, t });\n const value: Vector2 = {\n x: t.a * x + t.c * y + t.e,\n y: t.b * x + t.d * y + t.f,\n };\n //_console.log(\"transformed value\", value);\n return value;\n}\nfunction parseStyle(styleStr: string | undefined): Record<string, string> {\n const style: Record<string, string> = {};\n if (!styleStr) return style;\n\n styleStr.split(\";\").forEach((item) => {\n const [key, value] = item.split(\":\").map((s) => s.trim());\n if (key && value) style[key] = value;\n });\n return style;\n}\n\nconst circleBezierConstant = 0.5522847498307936;\nfunction svgJsonToCanvasCommands(svgJson: INode): CanvasCommand[] {\n const commands: CanvasCommand[] = [];\n\n function traverse(node: any, parentTransform: Transform) {\n //_console.log(\"traversing node\", node, parentTransform);\n const transform = parseTransform(node.attributes.transform);\n //_console.log(\"transform\", transform);\n const nodeTransform = multiply(parentTransform, transform);\n //_console.log(\"nodeTransform\", nodeTransform);\n\n const { scale, translation, rotation, isScaleUniform } =\n decomposeTransform(nodeTransform);\n //_console.log({ scale, translation, rotation, isScaleUniform });\n const uniformScale = scale.x;\n\n // Handle styles\n const style = parseStyle(node.attributes.style);\n // Fill\n if (style.fill) commands.push({ type: \"fillStyle\", fillStyle: style.fill });\n if (node.attributes.fill)\n commands.push({ type: \"fillStyle\", fillStyle: node.attributes.fill });\n\n // Stroke\n if (style.stroke)\n commands.push({ type: \"strokeStyle\", strokeStyle: style.stroke });\n if (node.attributes.stroke)\n commands.push({\n type: \"strokeStyle\",\n strokeStyle: node.attributes.stroke,\n });\n\n // Stroke width\n let strokeWidth = 0;\n if (style[\"stroke-width\"])\n strokeWidth = parseLength(style[\"stroke-width\"]) ?? 0;\n if (node.attributes[\"stroke-width\"])\n strokeWidth = parseLength(node.attributes[\"stroke-width\"]) ?? strokeWidth;\n if (strokeWidth)\n commands.push({\n type: \"lineWidth\",\n lineWidth: strokeWidth * nodeTransform.a, // scale to pixels\n });\n\n // Fill rule\n let fillRule = style[\"fill-rule\"];\n if (node.attributes[\"fill-rule\"]) fillRule = node.attributes[\"fill-rule\"];\n if (fillRule)\n commands.push({ type: \"fillRule\", fillRule: fillRule as FillRule });\n\n switch (node.name) {\n case \"path\":\n const d = node.attributes.d;\n if (!d) break;\n const pathData = new SVGPathData(d)\n .toAbs()\n .aToC()\n .normalizeHVZ(false)\n .normalizeST()\n .removeCollinear()\n .sanitize();\n //_console.log(\"pathData\", d, pathData);\n commands.push({ type: \"pathStart\" });\n for (const cmd of pathData.commands) {\n switch (cmd.type) {\n case SVGPathData.MOVE_TO:\n commands.push({ type: \"closePath\" });\n const m = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({ type: \"moveTo\", x: m.x, y: m.y });\n break;\n\n case SVGPathData.LINE_TO:\n const l = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({ type: \"lineTo\", x: l.x, y: l.y });\n break;\n case SVGPathData.CURVE_TO:\n const c1 = applyTransform(cmd.x1!, cmd.y1!, nodeTransform);\n const c2 = applyTransform(cmd.x2!, cmd.y2!, nodeTransform);\n const ce = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: c1.x,\n cp1y: c1.y,\n cp2x: c2.x,\n cp2y: c2.y,\n x: ce.x,\n y: ce.y,\n });\n break;\n case SVGPathData.QUAD_TO:\n const qcp = applyTransform(cmd.x1!, cmd.y1!, nodeTransform);\n const qe = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({\n type: \"quadraticCurveTo\",\n cpx: qcp.x,\n cpy: qcp.y,\n x: qe.x,\n y: qe.y,\n });\n break;\n case SVGPathData.CLOSE_PATH:\n commands.push({ type: \"closePath\" });\n break;\n default:\n _console.warn(\"uncaught command\", cmd);\n break;\n }\n }\n if (commands.at(-1)?.type != \"closePath\") {\n commands.push({ type: \"closePath\" });\n }\n commands.push({ type: \"pathEnd\" });\n\n break;\n\n case \"rect\": {\n const x = parseFloat(node.attributes.x || \"0\");\n const y = parseFloat(node.attributes.y || \"0\");\n const width = parseFloat(node.attributes.width || \"0\");\n const height = parseFloat(node.attributes.height || \"0\");\n\n let rx = parseFloat(node.attributes.rx || \"0\");\n let ry = parseFloat(node.attributes.ry || \"0\");\n if (!node.attributes.ry && rx) ry = rx;\n\n rx = Math.min(rx, width / 2);\n ry = Math.min(ry, height / 2);\n\n if (rx === 0 && ry === 0) {\n // sharp rect\n if (isScaleUniform) {\n const center = applyTransform(\n x + width / 2,\n y + height / 2,\n nodeTransform\n );\n commands.push({\n type: \"rect\",\n x: center.x,\n y: center.y,\n width: width * uniformScale,\n height: height * uniformScale,\n rotation,\n });\n } else {\n const tl = applyTransform(x, y, nodeTransform);\n const tr = applyTransform(x + width, y, nodeTransform);\n const br = applyTransform(x + width, y + height, nodeTransform);\n const bl = applyTransform(x, y + height, nodeTransform);\n\n commands.push({ type: \"moveTo\", x: tl.x, y: tl.y });\n commands.push({ type: \"lineTo\", x: tr.x, y: tr.y });\n commands.push({ type: \"lineTo\", x: br.x, y: br.y });\n commands.push({ type: \"lineTo\", x: bl.x, y: bl.y });\n commands.push({ type: \"closePath\" });\n }\n } else {\n // rounded rect\n if (rx == ry && isScaleUniform) {\n const center = applyTransform(\n x + width / 2,\n y + height / 2,\n nodeTransform\n );\n commands.push({\n type: \"roundRect\",\n x: center.x,\n y: center.y,\n width: width * uniformScale,\n height: height * uniformScale,\n rotation,\n r: rx * uniformScale,\n });\n } else {\n const ox = rx * circleBezierConstant; // x offset for control points\n const oy = ry * circleBezierConstant; // y offset for control points\n\n // Corners before transform\n const p1 = { x: x + rx, y: y };\n const p2 = { x: x + width - rx, y: y };\n const p3 = { x: x + width, y: y + ry };\n const p4 = { x: x + width, y: y + height - ry };\n const p5 = { x: x + width - rx, y: y + height };\n const p6 = { x: x + rx, y: y + height };\n const p7 = { x: x, y: y + height - ry };\n const p8 = { x: x, y: y + ry };\n\n // Move to start\n const start = applyTransform(p1.x, p1.y, nodeTransform);\n commands.push({ type: \"moveTo\", x: start.x, y: start.y });\n\n // Top edge + top-right corner\n let cp1 = applyTransform(p2.x + ox, p2.y, nodeTransform);\n let cp2 = applyTransform(p3.x, p3.y - oy, nodeTransform);\n let end = applyTransform(p3.x, p3.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p2.x, p2.y, nodeTransform).x,\n y: applyTransform(p2.x, p2.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Right edge + bottom-right corner\n cp1 = applyTransform(p4.x, p4.y + oy, nodeTransform);\n cp2 = applyTransform(p5.x + ox, p5.y, nodeTransform);\n end = applyTransform(p5.x, p5.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p4.x, p4.y, nodeTransform).x,\n y: applyTransform(p4.x, p4.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Bottom edge + bottom-left corner\n cp1 = applyTransform(p6.x - ox, p6.y, nodeTransform);\n cp2 = applyTransform(p7.x, p7.y + oy, nodeTransform);\n end = applyTransform(p7.x, p7.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p6.x, p6.y, nodeTransform).x,\n y: applyTransform(p6.x, p6.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Left edge + top-left corner\n cp1 = applyTransform(p8.x, p8.y - oy, nodeTransform);\n cp2 = applyTransform(p1.x - ox, p1.y, nodeTransform);\n end = applyTransform(p1.x, p1.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p8.x, p8.y, nodeTransform).x,\n y: applyTransform(p8.x, p8.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n }\n break;\n }\n\n case \"circle\": {\n const cx = parseFloat(node.attributes.cx || \"0\");\n const cy = parseFloat(node.attributes.cy || \"0\");\n const r = parseFloat(node.attributes.r || \"0\");\n\n if (r === 0) break;\n\n if (isScaleUniform) {\n //_console.log({ cx, cy, r, uniformScale });\n const center = applyTransform(cx, cy, nodeTransform);\n commands.push({\n type: \"circle\",\n x: center.x,\n y: center.y,\n r: r * uniformScale,\n });\n } else {\n const ox = r * circleBezierConstant; // control point offset\n\n // Points around the circle\n const pTop = applyTransform(cx, cy - r, nodeTransform);\n const pRight = applyTransform(cx + r, cy, nodeTransform);\n const pBottom = applyTransform(cx, cy + r, nodeTransform);\n const pLeft = applyTransform(cx - r, cy, nodeTransform);\n //_console.log({ pTop, pRight, pBottom, pLeft });\n\n const cpTopRight = applyTransform(cx + ox, cy - r, nodeTransform);\n const cpRightTop = applyTransform(cx + r, cy - ox, nodeTransform);\n\n const cpRightBottom = applyTransform(cx + r, cy + ox, nodeTransform);\n const cpBottomRight = applyTransform(cx + ox, cy + r, nodeTransform);\n\n const cpBottomLeft = applyTransform(cx - ox, cy + r, nodeTransform);\n const cpLeftBottom = applyTransform(cx - r, cy + ox, nodeTransform);\n\n const cpLeftTop = applyTransform(cx - r, cy - ox, nodeTransform);\n const cpTopLeft = applyTransform(cx - ox, cy - r, nodeTransform);\n\n commands.push({ type: \"moveTo\", x: pTop.x, y: pTop.y });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpTopRight.x,\n cp1y: cpTopRight.y,\n cp2x: cpRightTop.x,\n cp2y: cpRightTop.y,\n x: pRight.x,\n y: pRight.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpRightBottom.x,\n cp1y: cpRightBottom.y,\n cp2x: cpBottomRight.x,\n cp2y: cpBottomRight.y,\n x: pBottom.x,\n y: pBottom.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpBottomLeft.x,\n cp1y: cpBottomLeft.y,\n cp2x: cpLeftBottom.x,\n cp2y: cpLeftBottom.y,\n x: pLeft.x,\n y: pLeft.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpLeftTop.x,\n cp1y: cpLeftTop.y,\n cp2x: cpTopLeft.x,\n cp2y: cpTopLeft.y,\n x: pTop.x,\n y: pTop.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n break;\n }\n\n case \"ellipse\": {\n const cx = parseFloat(node.attributes.cx || \"0\");\n const cy = parseFloat(node.attributes.cy || \"0\");\n const rx = parseFloat(node.attributes.rx || \"0\");\n const ry = parseFloat(node.attributes.ry || \"0\");\n\n if (rx === 0 || ry === 0) break;\n\n if (isScaleUniform) {\n const center = applyTransform(cx, cy, nodeTransform);\n if (rx == ry) {\n commands.push({\n type: \"circle\",\n x: center.x,\n y: center.y,\n r: rx * uniformScale,\n });\n } else {\n commands.push({\n type: \"ellipse\",\n x: center.x,\n y: center.y,\n rx: rx * uniformScale,\n ry: ry * uniformScale,\n rotation,\n });\n }\n } else {\n const ox = rx * circleBezierConstant;\n const oy = ry * circleBezierConstant;\n\n // Key points\n const pTop = applyTransform(cx, cy - ry, nodeTransform);\n const pRight = applyTransform(cx + rx, cy, nodeTransform);\n const pBottom = applyTransform(cx, cy + ry, nodeTransform);\n const pLeft = applyTransform(cx - rx, cy, nodeTransform);\n\n // Control points\n const cpTopRight = applyTransform(cx + ox, cy - ry, nodeTransform);\n const cpRightTop = applyTransform(cx + rx, cy - oy, nodeTransform);\n\n const cpRightBottom = applyTransform(cx + rx, cy + oy, nodeTransform);\n const cpBottomRight = applyTransform(cx + ox, cy + ry, nodeTransform);\n\n const cpBottomLeft = applyTransform(cx - ox, cy + ry, nodeTransform);\n const cpLeftBottom = applyTransform(cx - rx, cy + oy, nodeTransform);\n\n const cpLeftTop = applyTransform(cx - rx, cy - oy, nodeTransform);\n const cpTopLeft = applyTransform(cx - ox, cy - ry, nodeTransform);\n\n // Draw ellipse using cubic Beziers\n commands.push({ type: \"moveTo\", x: pTop.x, y: pTop.y });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpTopRight.x,\n cp1y: cpTopRight.y,\n cp2x: cpRightTop.x,\n cp2y: cpRightTop.y,\n x: pRight.x,\n y: pRight.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpRightBottom.x,\n cp1y: cpRightBottom.y,\n cp2x: cpBottomRight.x,\n cp2y: cpBottomRight.y,\n x: pBottom.x,\n y: pBottom.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpBottomLeft.x,\n cp1y: cpBottomLeft.y,\n cp2x: cpLeftBottom.x,\n cp2y: cpLeftBottom.y,\n x: pLeft.x,\n y: pLeft.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpLeftTop.x,\n cp1y: cpLeftTop.y,\n cp2x: cpTopLeft.x,\n cp2y: cpTopLeft.y,\n x: pTop.x,\n y: pTop.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n break;\n }\n\n case \"polyline\":\n case \"polygon\": {\n const pointsStr: string = node.attributes.points || \"\";\n const points: { x: number; y: number }[] = pointsStr\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n .reduce<{ x?: number; y?: number }[]>((acc, val, idx) => {\n if (idx % 2 === 0) acc.push({ x: val, y: 0 });\n else acc[acc.length - 1].y = val;\n return acc;\n }, [])\n .map((p) => ({ x: p.x!, y: p.y! }));\n\n if (points.length === 0) break;\n\n // Move to first point\n const start = applyTransform(points[0].x, points[0].y, nodeTransform);\n commands.push({ type: \"moveTo\", x: start.x, y: start.y });\n\n // Draw lines to remaining points\n for (let i = 1; i < points.length; i++) {\n const p = applyTransform(points[i].x, points[i].y, nodeTransform);\n commands.push({ type: \"lineTo\", x: p.x, y: p.y });\n }\n\n // close path, even if polyline\n commands.push({ type: \"closePath\" });\n break;\n }\n\n case \"line\": {\n const x1 = parseFloat(node.attributes.x1 || \"0\");\n const y1 = parseFloat(node.attributes.y1 || \"0\");\n const x2 = parseFloat(node.attributes.x2 || \"0\");\n const y2 = parseFloat(node.attributes.y2 || \"0\");\n\n const p1 = applyTransform(x1, y1, nodeTransform);\n const p2 = applyTransform(x2, y2, nodeTransform);\n\n commands.push({ type: \"line\", x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y });\n\n break;\n }\n case \"svg\":\n break;\n default:\n _console.log(\"uncaught node\", node);\n break;\n }\n\n if (node.children) {\n for (const child of node.children) traverse(child, nodeTransform);\n }\n }\n\n traverse(svgJson, getSvgTransformToPixels(svgJson));\n return commands;\n}\n\nfunction parseLength(\n str: string | undefined,\n relativeTo?: number\n): number | undefined {\n if (!str) return undefined;\n const match = /^([0-9.]+)([a-z%]*)$/.exec(str.trim());\n if (!match) return undefined;\n\n const value = parseFloat(match[1]);\n const unit = match[2] || \"px\";\n\n switch (unit) {\n case \"px\":\n return value;\n case \"pt\":\n return value * (96 / 72); // 1pt = 1/72in, 96dpi\n case \"in\":\n return value * 96; // 1in = 96px\n case \"cm\":\n return value * (96 / 2.54); // 1cm = 96/2.54 px\n case \"mm\":\n return value * (96 / 25.4); // 1mm = 96/25.4 px\n case \"%\":\n if (relativeTo === undefined) return undefined;\n return (value / 100) * relativeTo;\n case \"\":\n return value; // unitless → px\n default:\n return value; // unknown unit → assume px\n }\n}\n\nfunction getSvgJsonSize(svgJson: INode) {\n const attrs = svgJson.attributes || {};\n let width = parseLength(attrs.width);\n let height = parseLength(attrs.height);\n\n // Fallback to viewBox dimensions\n if ((width == null || height == null) && attrs.viewBox) {\n const [, , vbWidth, vbHeight] = attrs.viewBox\n .split(/[\\s,]+/)\n .map(parseFloat);\n width ??= vbWidth;\n height ??= vbHeight;\n }\n\n const size: DisplaySize = {\n width: width ?? 300,\n height: height ?? 150,\n };\n //_console.log(\"size\", size);\n return size;\n}\n\nfunction getSvgJsonViewBox(svgJson: INode): DisplayBoundingBox {\n const attrs = svgJson.attributes || {};\n let x = 0,\n y = 0,\n width: number | undefined,\n height: number | undefined;\n\n if (attrs.viewBox) {\n [x, y, width, height] = attrs.viewBox.split(/[\\s,]+/).map(parseFloat);\n }\n\n // Fallback to size if no viewBox\n if (width == null || height == null) {\n const size = getSvgJsonSize(svgJson);\n width ??= size.width;\n height ??= size.height;\n }\n\n const viewBox: DisplayBoundingBox = {\n x,\n y,\n width: width!,\n height: height!,\n };\n //_console.log(\"viewBox\", viewBox);\n return viewBox;\n}\n\nfunction getSvgJsonBoundingBox(svgJson: INode): DisplayBoundingBox {\n const { width, height } = getSvgJsonSize(svgJson);\n const viewBox = getSvgJsonViewBox(svgJson);\n\n if (width !== undefined && height !== undefined) {\n return { x: 0, y: 0, width, height };\n } else if (viewBox.width !== undefined && viewBox.height !== undefined) {\n return viewBox;\n } else {\n return { x: 0, y: 0, width: 300, height: 150 };\n }\n}\n\nfunction getSvgTransformToPixels(svgJson: INode): Transform {\n const attrs = svgJson.attributes || {};\n const { width, height } = getSvgJsonSize(svgJson); // in px\n const viewBox = getSvgJsonViewBox(svgJson); // { x, y, width, height }\n\n //_console.log({ width, height, viewBox });\n\n // Base scales\n let scaleX = width / viewBox.width;\n let scaleY = height / viewBox.height;\n let offsetX = 0;\n let offsetY = 0;\n\n // Handle preserveAspectRatio=\"xMidYMid meet\"\n if (attrs.preserveAspectRatio?.includes(\"meet\")) {\n const s = Math.min(scaleX, scaleY);\n offsetX = (width - viewBox.width * s) / 2;\n offsetY = (height - viewBox.height * s) / 2;\n scaleX = scaleY = s;\n }\n\n // Return the affine transform matrix\n return {\n a: scaleX,\n b: 0,\n c: 0,\n d: scaleY,\n e: -viewBox.x * scaleX + offsetX,\n f: -viewBox.y * scaleY + offsetY,\n };\n}\n\nexport type ParseSvgOptions = {\n fit?: boolean; // removes extra empty space around the shapes\n width?: number; // scale output to this width\n height?: number; // scale output to this height\n aspectRatio?: number; // width / height, used if only one of width/height is provided\n offsetX?: number;\n offsetY?: number;\n centered?: boolean;\n};\nconst defaultParseSvgOptions: ParseSvgOptions = {\n fit: false,\n centered: true,\n};\n\nfunction transformCanvasCommands(\n canvasCommands: CanvasCommand[],\n xCallback: (x: number) => number,\n yCallback: (y: number) => number,\n type: \"offset\" | \"scale\"\n): CanvasCommand[] {\n return canvasCommands.map((command) => {\n switch (command.type) {\n case \"moveTo\":\n case \"lineTo\": {\n let { x, y } = command;\n x = xCallback(x);\n y = yCallback(y);\n return { type: command.type, x, y };\n break;\n }\n case \"quadraticCurveTo\": {\n let { x, y, cpx, cpy } = command;\n x = xCallback(x);\n y = yCallback(y);\n cpx = xCallback(cpx);\n cpy = yCallback(cpy);\n return { type: command.type, x, y, cpx, cpy };\n break;\n }\n case \"bezierCurveTo\": {\n let { x, y, cp1x, cp1y, cp2x, cp2y } = command;\n x = xCallback(x);\n y = yCallback(y);\n cp1x = xCallback(cp1x);\n cp1y = yCallback(cp1y);\n cp2x = xCallback(cp2x);\n cp2y = yCallback(cp2y);\n return { type: command.type, x, y, cp1x, cp1y, cp2x, cp2y };\n break;\n }\n case \"lineWidth\": {\n if (type == \"scale\") {\n let { lineWidth } = command;\n lineWidth = xCallback(lineWidth);\n return { type: command.type, lineWidth };\n }\n break;\n }\n case \"rect\":\n case \"roundRect\": {\n let { x, y, width, height, rotation } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n width = xCallback(width);\n height = yCallback(height);\n }\n if (command.type == \"roundRect\") {\n let { r } = command;\n if (type == \"scale\") {\n r = xCallback(r);\n }\n return { type: command.type, x, y, width, height, rotation, r };\n }\n return { type: command.type, x, y, width, height, rotation };\n break;\n }\n case \"circle\":\n {\n let { x, y, r } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n r = xCallback(r);\n }\n return { type: command.type, x, y, r };\n }\n break;\n case \"ellipse\":\n {\n let { x, y, rx, ry, rotation } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n rx = xCallback(rx);\n ry = xCallback(ry);\n }\n return { type: command.type, x, y, rx, ry, rotation };\n }\n break;\n default:\n return command;\n }\n return command;\n });\n}\nfunction forEachCanvasCommandVector2(\n canvasCommands: CanvasCommand[],\n vectorCallback: (x: number, y: number) => void\n) {\n canvasCommands.forEach((command) => {\n switch (command.type) {\n case \"moveTo\":\n case \"lineTo\":\n {\n let { x, y } = command;\n vectorCallback(x, y);\n }\n break;\n case \"quadraticCurveTo\":\n {\n let { x, y, cpx, cpy } = command;\n vectorCallback(x, y);\n vectorCallback(cpx, cpy);\n }\n break;\n case \"bezierCurveTo\": {\n let { x, y, cp1x, cp1y, cp2x, cp2y } = command;\n vectorCallback(x, y);\n vectorCallback(cp1x, cp1y);\n vectorCallback(cp2x, cp2y);\n }\n default:\n break;\n }\n });\n}\nfunction offsetCanvasCommands(\n canvasCommands: CanvasCommand[],\n offsetX = 0,\n offsetY = 0\n) {\n return transformCanvasCommands(\n canvasCommands,\n (x) => x + offsetX,\n (y) => y + offsetY,\n \"offset\"\n );\n}\nfunction scaleCanvasCommands(\n canvasCommands: CanvasCommand[],\n scaleX: number,\n scaleY: number\n) {\n return transformCanvasCommands(\n canvasCommands,\n (x) => x * scaleX,\n (y) => y * scaleY,\n \"scale\"\n );\n}\n\nfunction getBoundingBox(path: Vector2[]) {\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const p of path) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\nfunction bboxContains(\n a: ReturnType<typeof getBoundingBox>,\n b: ReturnType<typeof getBoundingBox>\n) {\n return (\n a.minX <= b.minX && a.minY <= b.minY && a.maxX >= b.maxX && a.maxY >= b.maxY\n );\n}\n\nexport function classifySubpath(\n subpath: Vector2[],\n previous: { path: Vector2[]; isHole: boolean }[],\n fillRule: FillRule\n): boolean {\n const centroid = subpath.reduce(\n (acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),\n { x: 0, y: 0 }\n );\n centroid.x /= subpath.length;\n centroid.y /= subpath.length;\n\n const subBBox = getBoundingBox(subpath);\n\n let insideCount = 0;\n\n for (const other of previous) {\n const otherBBox = getBoundingBox(other.path);\n\n // must be fully inside bbox\n if (!bboxContains(otherBBox, subBBox)) continue;\n\n // require *most* points to be inside\n const insidePoints = subpath.filter((p) =>\n pointInPolygon(p, other.path)\n ).length;\n const allInside = insidePoints > subpath.length * 0.8;\n if (!allInside) continue;\n\n insideCount++;\n }\n\n if (fillRule === \"evenodd\") {\n return insideCount % 2 === 1; // odd count = hole\n } else {\n // non-zero winding rule\n let winding = 0;\n for (const other of previous) {\n const otherBBox = getBoundingBox(other.path);\n if (!bboxContains(otherBBox, subBBox)) continue;\n if (pointInPolygon(centroid, other.path)) {\n winding += contourArea(other.path) > 0 ? 1 : -1;\n }\n }\n return winding !== 0; // nonzero = inside → hole\n }\n}\n\nexport function svgToDisplayContextCommands(\n svgString: string,\n numberOfColors: number,\n paletteOffset: number,\n colors?: string[],\n options?: ParseSvgOptions\n) {\n _console.assertWithError(\n numberOfColors > 1,\n \"numberOfColors must be greater than 1\"\n );\n options = { ...defaultParseSvgOptions, ...options };\n _console.log(\"options\", options);\n\n const svgJson = parseSync(svgString);\n\n let canvasCommands = svgJsonToCanvasCommands(svgJson);\n _console.log(\"canvasCommands\", canvasCommands);\n\n const boundingBox = getSvgJsonBoundingBox(svgJson);\n //_console.log(\"boundingBox\", boundingBox);\n\n let intrinsicWidth = boundingBox.width;\n let intrinsicHeight = boundingBox.height;\n\n _console.log({ intrinsicWidth, intrinsicHeight });\n\n let scaleX = 1,\n scaleY = 1;\n if (options.width && options.height) {\n scaleX = options.width / intrinsicWidth;\n scaleY = options.height / intrinsicHeight;\n } else if (options.width) {\n scaleX = scaleY = options.width / intrinsicWidth;\n if (options.aspectRatio) scaleY = scaleX / options.aspectRatio;\n } else if (options.height) {\n scaleX = scaleY = options.height / intrinsicHeight;\n if (options.aspectRatio) scaleX = scaleY * options.aspectRatio;\n }\n\n _console.log({ scaleX, scaleY });\n\n let width = intrinsicWidth * scaleX;\n let height = intrinsicWidth * scaleX;\n\n _console.log({ width, height });\n\n if (scaleX !== 1 || scaleY !== 1) {\n canvasCommands = scaleCanvasCommands(canvasCommands, scaleX, scaleY);\n }\n\n if (options.fit) {\n const rangeHelper = {\n x: new RangeHelper(),\n y: new RangeHelper(),\n };\n forEachCanvasCommandVector2(canvasCommands, (x, y) => {\n rangeHelper.x.update(x);\n rangeHelper.y.update(y);\n });\n\n // _console.log(\"xRange\", rangeHelper.x.min, rangeHelper.x.max);\n // _console.log(\"yRange\", rangeHelper.y.min, rangeHelper.y.max);\n\n width = rangeHelper.x.span;\n height = rangeHelper.y.span;\n\n const offsetX = -rangeHelper.x.min;\n const offsetY = -rangeHelper.y.min;\n\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n if (options.offsetX || options.offsetY) {\n const offsetX = options.offsetX || 0;\n const offsetY = options.offsetY || 0;\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n if (options.centered) {\n const offsetX = -width / 2;\n const offsetY = -height / 2;\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n let svgColors: string[] = [];\n canvasCommands.forEach((canvasCommand) => {\n let color: string | undefined;\n switch (canvasCommand.type) {\n case \"fillStyle\":\n color = canvasCommand.fillStyle;\n break;\n case \"strokeStyle\":\n color = canvasCommand.strokeStyle;\n break;\n default:\n return;\n }\n if (color && color != \"none\" && !svgColors.includes(color)) {\n svgColors.push(color);\n }\n });\n if (svgColors.length == 0) {\n svgColors.push(\"black\");\n }\n if (svgColors.length == 1) {\n svgColors.push(\"white\");\n }\n _console.log(\"colors\", svgColors);\n\n const colorToIndex: Record<string, number> = {};\n if (colors) {\n colors = colors.slice(0, numberOfColors);\n const mapping = mapToClosestPaletteIndex(svgColors, colors.slice(1));\n _console.log(\"mapping\", mapping, colors);\n svgColors.forEach((color) => {\n colorToIndex[color] = mapping[color] + 1;\n });\n } else {\n // FIX - annoying when an svg has a black fill\n const { palette, mapping } = kMeansColors(svgColors, numberOfColors);\n _console.log(\"mapping\", mapping);\n _console.log(\"palette\", palette);\n\n svgColors.forEach((color) => {\n colorToIndex[color] = mapping[color];\n });\n colors = palette;\n }\n _console.log(\"colorToIndex\", colorToIndex);\n\n _console.log(\"transformed canvasCommands\", canvasCommands);\n\n let curves: DisplayBezierCurve[] = [];\n let startPoint: Vector2 = { x: 0, y: 0 };\n let fillRule: FillRule = \"nonzero\";\n let fillStyle: string | undefined;\n let strokeStyle = \"none\";\n let lineWidth = 1;\n let segmentRadius = 1;\n let wasHole = false;\n let ignoreFill = false;\n let ignoreLine = true;\n let fillColorIndex = 1;\n let lineColorIndex = 1;\n const getFillColorIndex = () => fillColorIndex + paletteOffset;\n const getLineColorIndex = () => lineColorIndex + paletteOffset;\n let isDrawingPath = false;\n const parsedPaths: { path: Vector2[]; isHole: boolean }[] = [];\n\n let displayCommands: DisplayContextCommand[] = [];\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"selectLineColor\",\n lineColorIndex: getLineColorIndex(),\n });\n displayCommands.push({ type: \"setIgnoreLine\", ignoreLine: true });\n displayCommands.push({ type: \"setLineWidth\", lineWidth });\n displayCommands.push({\n type: \"setSegmentRadius\",\n segmentRadius,\n });\n\n canvasCommands.forEach((canvasCommand) => {\n switch (canvasCommand.type) {\n case \"moveTo\":\n {\n const { x, y } = canvasCommand;\n startPoint.x = x;\n startPoint.y = y;\n }\n break;\n case \"lineTo\":\n {\n const { x, y } = canvasCommand;\n const controlPoints: Vector2[] = [{ x, y }];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"segment\", controlPoints });\n }\n break;\n case \"quadraticCurveTo\":\n {\n const { x, y, cpx, cpy } = canvasCommand;\n const controlPoints: Vector2[] = [\n { x: cpx, y: cpy },\n { x, y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"quadratic\", controlPoints });\n }\n break;\n case \"bezierCurveTo\":\n {\n const { x, y, cp1x, cp1y, cp2x, cp2y } = canvasCommand;\n const controlPoints: Vector2[] = [\n { x: cp1x, y: cp1y },\n { x: cp2x, y: cp2y },\n { x, y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"cubic\", controlPoints });\n }\n break;\n case \"closePath\":\n if (curves.length === 0) break;\n\n curves = simplifyCurves(curves);\n\n // Flatten all control points\n const controlPoints = curves.flatMap((c) => c.controlPoints);\n\n if (isDrawingPath) {\n const isHole = classifySubpath(controlPoints, parsedPaths, fillRule);\n parsedPaths.push({ path: controlPoints, isHole });\n\n // _console.log({\n // pathIndex: parsedPaths.length - 1,\n // isHole,\n // fillStyle,\n // strokeStyle,\n // fillRule,\n // lineWidth,\n // });\n\n if (isHole != wasHole) {\n wasHole = isHole;\n if (isHole) {\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: 0,\n });\n } else {\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n }\n }\n }\n\n if (ignoreFill) {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth: 0,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getLineColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill: false,\n });\n }\n\n const isSegments = curves.every((c) => c.type === \"segment\");\n if (isSegments) {\n if (ignoreFill) {\n displayCommands.push({\n type: \"drawSegments\",\n points: controlPoints,\n });\n } else {\n displayCommands.push({\n type: \"drawPolygon\",\n points: controlPoints,\n });\n }\n } else {\n if (ignoreFill) {\n displayCommands.push({ type: \"drawPath\", curves });\n } else {\n displayCommands.push({ type: \"drawClosedPath\", curves });\n }\n }\n\n if (ignoreFill) {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill,\n });\n }\n\n // Reset curves\n curves = [];\n break;\n case \"pathStart\":\n parsedPaths.length = 0;\n if (wasHole) {\n displayCommands.push({ type: \"selectFillColor\", fillColorIndex });\n }\n wasHole = false;\n isDrawingPath = true;\n break;\n case \"pathEnd\":\n isDrawingPath = false;\n break;\n case \"line\":\n if (strokeStyle != \"none\") {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth: 0,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getLineColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill: false,\n });\n\n const { x1, y1, x2, y2 } = canvasCommand;\n displayCommands.push({\n type: \"drawSegment\",\n startX: x1,\n startY: y1,\n endX: x2,\n endY: y2,\n });\n\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill,\n });\n }\n\n break;\n case \"fillStyle\":\n _console.log(\"fillStyle\", canvasCommand.fillStyle);\n if (fillStyle != canvasCommand.fillStyle) {\n const newIgnoreFill = canvasCommand.fillStyle == \"none\";\n if (ignoreFill != newIgnoreFill) {\n ignoreFill = newIgnoreFill;\n _console.log({ ignoreFill });\n displayCommands.push({ type: \"setIgnoreFill\", ignoreFill });\n }\n if (!ignoreFill) {\n if (fillStyle != canvasCommand.fillStyle) {\n fillStyle = canvasCommand.fillStyle;\n if (fillColorIndex != colorToIndex[fillStyle]) {\n _console.log({ fillColorIndex });\n fillColorIndex = colorToIndex[fillStyle];\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n }\n }\n }\n }\n break;\n case \"strokeStyle\":\n _console.log(\"strokeStyle\", canvasCommand.strokeStyle);\n if (strokeStyle != canvasCommand.strokeStyle) {\n const newIgnoreLine = canvasCommand.strokeStyle == \"none\";\n if (ignoreLine != newIgnoreLine) {\n ignoreLine = newIgnoreLine;\n _console.log({ ignoreLine });\n displayCommands.push({ type: \"setIgnoreLine\", ignoreLine });\n }\n if (!ignoreLine) {\n if (strokeStyle != canvasCommand.strokeStyle) {\n strokeStyle = canvasCommand.strokeStyle;\n if (lineColorIndex != colorToIndex[strokeStyle]) {\n _console.log({ lineColorIndex });\n lineColorIndex = colorToIndex[strokeStyle];\n displayCommands.push({\n type: \"selectLineColor\",\n lineColorIndex: getLineColorIndex(),\n });\n }\n }\n }\n }\n break;\n case \"lineWidth\":\n if (lineWidth != canvasCommand.lineWidth) {\n lineWidth = canvasCommand.lineWidth;\n displayCommands.push({ type: \"setLineWidth\", lineWidth });\n segmentRadius = lineWidth / 2;\n displayCommands.push({\n type: \"setSegmentRadius\",\n segmentRadius,\n });\n }\n break;\n case \"fillRule\":\n fillRule = canvasCommand.fillRule;\n break;\n case \"rect\":\n {\n const { x, y, width, height, rotation } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawRect\",\n offsetX: x,\n offsetY: y,\n width: width,\n height: height,\n });\n }\n break;\n case \"roundRect\":\n {\n const { x, y, width, height, rotation, r } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawRoundRect\",\n offsetX: x,\n offsetY: y,\n width: width,\n height: height,\n borderRadius: r,\n });\n }\n break;\n case \"circle\":\n {\n const { x, y, r } = canvasCommand;\n displayCommands.push({\n type: \"drawCircle\",\n offsetX: x,\n offsetY: y,\n radius: r,\n });\n }\n break;\n case \"ellipse\":\n {\n const { x, y, rx, ry, rotation } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawEllipse\",\n offsetX: x,\n offsetY: y,\n radiusX: rx,\n radiusY: ry,\n });\n }\n break;\n default:\n _console.warn(\"uncaught canvasCommand\", canvasCommand);\n break;\n }\n });\n\n displayCommands = trimContextCommands(displayCommands);\n\n _console.log(\"displayCommands\", displayCommands);\n _console.log(\"colors\", colors);\n return { commands: displayCommands, colors, width, height };\n}\n\nexport function svgToSprite(\n svgString: string,\n spriteName: string,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0,\n options?: ParseSvgOptions\n) {\n options = { ...defaultParseSvgOptions, ...options };\n _console.log(\"options\", options, { overridePalette });\n\n let palette = spriteSheet.palettes?.find(\n (palette) => palette.name == paletteName\n );\n if (!palette) {\n palette = {\n name: paletteName,\n numberOfColors,\n colors: new Array(numberOfColors).fill(\"#000000\"),\n };\n spriteSheet.palettes = spriteSheet.palettes || [];\n spriteSheet.palettes?.push(palette);\n }\n _console.log(\"pallete\", palette);\n\n const { commands, colors, width, height } = svgToDisplayContextCommands(\n svgString,\n numberOfColors,\n paletteOffset,\n !overridePalette ? palette.colors : undefined,\n options\n );\n\n const sprite: DisplaySprite = {\n name: spriteName,\n width,\n height,\n paletteSwaps: [],\n commands,\n };\n if (overridePalette) {\n _console.log(\"overriding palette\", colors);\n colors.forEach((color, index) => {\n palette.colors[index + paletteOffset] = color;\n });\n }\n\n const spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n if (spriteIndex == -1) {\n spriteSheet.sprites.push(sprite);\n } else {\n _console.log(`overwriting spriteInde ${spriteIndex}`);\n spriteSheet.sprites[spriteIndex] = sprite;\n }\n\n return sprite;\n}\n\nexport function svgToSpriteSheet(\n svgString: string,\n spriteSheetName: string,\n numberOfColors: number,\n paletteName: string,\n options?: ParseSvgOptions\n) {\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n palettes: [],\n paletteSwaps: [],\n sprites: [],\n };\n\n svgToSprite(\n svgString,\n \"svg\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet,\n 0,\n options\n );\n\n return spriteSheet;\n}\n\nexport function getSvgStringFromDataUrl(string: string) {\n if (!string.startsWith(\"data:image/svg+xml\"))\n throw new Error(\"Not a data URL\");\n\n // Data URL might be base64 or URI encoded\n const data = string.split(\",\")[1];\n if (string.includes(\"base64\")) {\n return atob(data);\n } else {\n return decodeURIComponent(data);\n }\n}\n\nexport function isValidSVG(svgString: string) {\n if (typeof svgString !== \"string\") return false;\n const parser = new DOMParser();\n const doc = parser.parseFromString(svgString, \"image/svg+xml\");\n\n // Different browsers may put parser errors in different places; check several ways:\n if (\n doc.querySelector(\"parsererror\") ||\n doc.getElementsByTagName(\"parsererror\").length > 0\n ) {\n return false;\n }\n\n const root = doc.documentElement;\n return (\n !!root &&\n root.nodeName.toLowerCase() === \"svg\" &&\n root.namespaceURI === \"http://www.w3.org/2000/svg\"\n );\n}\n","import { removeRedundancies } from \"./ObjectUtils.ts\";\n\nexport function spacesToPascalCase(string: string) {\n return string\n .replace(/(?:^\\w|\\b\\w)/g, function (match) {\n return match.toUpperCase();\n })\n .replace(/\\s+/g, \"\");\n}\n\nexport function capitalizeFirstCharacter(string: string) {\n return string[0].toUpperCase() + string.slice(1);\n}\n\nexport function removeRedundantCharacters(string: string) {\n return removeRedundancies(Array.from(string)).join(\"\");\n}\n\nexport function removeSubstrings(string: string, substrings: string[]): string {\n let result = string;\n for (const sub of substrings) {\n result = result.split(sub).join(\"\");\n }\n return result;\n}\n","import {\n DisplayBezierCurve,\n DisplayBitmap,\n DisplaySize,\n} from \"../DisplayManager.ts\";\nimport { concatenateArrayBuffers } from \"./ArrayBufferUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { quantizeCanvas } from \"./DisplayBitmapUtils.ts\";\nimport {\n DisplayContextCommand,\n serializeContextCommands,\n} from \"./DisplayContextCommand.ts\";\nimport { DisplayManagerInterface } from \"./DisplayManagerInterface.ts\";\nimport opentype, { Glyph, Font } from \"opentype.js\";\nimport { decompress } from \"woff2-encoder\";\nimport RangeHelper from \"./RangeHelper.ts\";\nimport { Vector2 } from \"./MathUtils.ts\";\nimport { simplifyCurves } from \"./PathUtils.ts\";\nimport {\n DisplayContextState,\n isDirectionHorizontal,\n} from \"./DisplayContextState.ts\";\nimport { classifySubpath } from \"./SvgUtils.ts\";\nimport { removeRedundantCharacters, removeSubstrings } from \"./stringUtils.ts\";\n\nconst _console = createConsole(\"DisplaySpriteSheetUtils\", { log: false });\n\nexport type DisplaySpriteSubLine = {\n spriteSheetName: string;\n spriteNames: string[];\n};\nexport type DisplaySpriteLine = DisplaySpriteSubLine[];\nexport type DisplaySpriteLines = DisplaySpriteLine[];\n\nexport type DisplaySpriteSerializedSubLine = {\n spriteSheetIndex: number;\n spriteIndices: number[];\n use2Bytes: boolean;\n};\nexport type DisplaySpriteSerializedLine = DisplaySpriteSerializedSubLine[];\nexport type DisplaySpriteSerializedLines = DisplaySpriteSerializedLine[];\n\nexport type DisplaySpritePaletteSwap = {\n name: string;\n numberOfColors: number;\n spriteColorIndices: number[];\n};\nexport type DisplaySprite = {\n name: string;\n width: number;\n height: number;\n paletteSwaps?: DisplaySpritePaletteSwap[];\n commands: DisplayContextCommand[];\n};\nexport type DisplaySpriteSheetPaletteSwap = {\n name: string;\n numberOfColors: number;\n spriteColorIndices: number[];\n};\nexport type DisplaySpriteSheetPalette = {\n name: string;\n numberOfColors: number;\n colors: string[];\n opacities?: number[];\n};\nexport type DisplaySpriteSheet = {\n name: string;\n palettes?: DisplaySpriteSheetPalette[];\n paletteSwaps?: DisplaySpriteSheetPaletteSwap[];\n sprites: DisplaySprite[];\n};\n\nexport const spriteHeaderLength = 3 * 2; // width, height, commandsLength\nexport function calculateSpriteSheetHeaderLength(numberOfSprites: number) {\n // numberOfSprites, spriteOffsets, spriteHeader\n return 2 + numberOfSprites * 2 + numberOfSprites * spriteHeaderLength;\n}\nexport function getCurvesPoints(curves: DisplayBezierCurve[]) {\n const curvePoints: Vector2[] = [];\n curves.forEach((curve, index) => {\n if (index == 0) {\n curvePoints.push(curve.controlPoints[0]);\n }\n curvePoints.push(curve.controlPoints.at(-1)!);\n });\n return curvePoints;\n}\nexport function serializeSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheet: DisplaySpriteSheet\n) {\n const { name, sprites } = spriteSheet;\n _console.log(`serializing ${name} spriteSheet`, spriteSheet);\n\n const numberOfSprites = sprites.length;\n const numberOfSpritesDataView = new DataView(new ArrayBuffer(2));\n numberOfSpritesDataView.setUint16(0, numberOfSprites, true);\n\n const spritePayloads = sprites.map((sprite, index) => {\n const commandsData = serializeContextCommands(\n displayManager,\n sprite.commands\n );\n const dataView = new DataView(new ArrayBuffer(spriteHeaderLength));\n dataView.setUint16(0, sprite.width, true);\n dataView.setUint16(2, sprite.height, true);\n dataView.setUint16(4, commandsData.byteLength, true);\n const serializedSprite = concatenateArrayBuffers(dataView, commandsData);\n _console.log(\"serializedSprite\", sprite, serializedSprite);\n return serializedSprite;\n });\n const spriteOffsetsDataView = new DataView(\n new ArrayBuffer(sprites.length * 2)\n );\n let offset =\n numberOfSpritesDataView.byteLength + spriteOffsetsDataView.byteLength;\n spritePayloads.forEach((spritePayload, index) => {\n //_console.log(\"spritePayloads\", index, offset, spritePayload);\n spriteOffsetsDataView.setUint16(index * 2, offset, true);\n offset += spritePayload.byteLength;\n });\n\n // [numberOfSprites, ...spriteOffsets, ...[width, height, commands]]\n const serializedSpriteSheet = concatenateArrayBuffers(\n numberOfSpritesDataView,\n spriteOffsetsDataView,\n spritePayloads\n );\n _console.log(\"serializedSpriteSheet\", serializedSpriteSheet);\n\n return serializedSpriteSheet;\n}\n\nexport function parseSpriteSheet(dataView: DataView) {\n // FILL\n}\n\nexport type FontToSpriteSheetOptions = {\n stroke?: boolean;\n strokeWidth?: number;\n unicodeOnly?: boolean;\n englishOnly?: boolean;\n usePath?: boolean;\n script?: string;\n string?: string;\n minSpriteY?: number;\n maxSpriteY?: number;\n maxSpriteheight?: number;\n};\nexport const defaultFontToSpriteSheetOptions: FontToSpriteSheetOptions = {\n stroke: false,\n strokeWidth: 1,\n unicodeOnly: true,\n englishOnly: true,\n usePath: false,\n};\n\nfunction isWoff2(arrayBuffer: ArrayBuffer) {\n if (arrayBuffer.byteLength < 4) return false;\n\n const header = new Uint8Array(arrayBuffer, 0, 4);\n return (\n header[0] === 0x77 && // 'w'\n header[1] === 0x4f && // 'O'\n header[2] === 0x46 && // 'F'\n header[3] === 0x32 // '2'\n );\n}\nexport async function parseFont(arrayBuffer: ArrayBuffer) {\n if (isWoff2(arrayBuffer)) {\n const result = await decompress(arrayBuffer);\n // @ts-expect-error\n arrayBuffer = result.buffer;\n }\n const font = opentype.parse(arrayBuffer);\n //_console.log(\"font\", font);\n return font;\n}\n\nexport function getFontUnicodeRange(font: Font) {\n const rangeHelper = new RangeHelper();\n\n for (let i = 0; i < font.glyphs.length; i++) {\n const glyph = font.glyphs.get(i);\n if (!glyph.unicodes || glyph.unicodes.length === 0) continue;\n\n glyph.unicodes\n .filter((unicode) => {\n const char = String.fromCodePoint(unicode);\n // Keep only letters (any language)\n return /\\p{Letter}/u.test(char);\n })\n .forEach((unicode) => rangeHelper.update(unicode));\n }\n\n //_console.log(\"range\", rangeHelper.range);\n return rangeHelper.span > 0 ? rangeHelper.range : undefined;\n}\n\nexport const englishRegex = /^[A-Za-z0-9 !\"#$%&'()*+,\\-./:;?@[\\]^_`{|}~\\\\]+$/;\n\nexport function contourArea(points: Vector2[]) {\n let area = 0;\n for (let i = 0, j = points.length - 1; i < points.length; j = i++) {\n area += (points[j].x - points[i].x) * (points[j].y + points[i].y);\n }\n return area;\n}\n\nexport function getFontMetrics(\n font: Font | Font[],\n fontSize: number,\n options?: FontToSpriteSheetOptions\n) {\n _console.assertTypeWithError(fontSize, \"number\");\n\n options = options\n ? { ...defaultFontToSpriteSheetOptions, ...options }\n : defaultFontToSpriteSheetOptions;\n\n const fonts = Array.isArray(font) ? font : [font];\n\n let minSpriteY = Infinity;\n let maxSpriteY = -Infinity;\n\n const strokeWidth = options.stroke ? options.strokeWidth || 1 : 0;\n\n let string = options.string;\n if (string) {\n string = removeRedundantCharacters(string);\n console.log(\"filtered string\", string);\n }\n\n for (let font of fonts) {\n const fontScale = (1 / font.unitsPerEm) * fontSize;\n\n const glyphs: Glyph[] = [];\n let filteredGlyphs: Glyph[] | undefined;\n if (string != undefined) {\n filteredGlyphs = font\n .stringToGlyphs(string)\n .filter((glyph) => glyph.unicode != undefined);\n string = removeSubstrings(\n string,\n filteredGlyphs.map((glyph) => String.fromCharCode(glyph.unicode!))\n );\n }\n\n for (let index = 0; index < font.glyphs.length; index++) {\n const glyph = font.glyphs.get(index);\n const hasUnicode = glyph.unicode != undefined;\n if (hasUnicode) {\n //_console.log(String.fromCharCode(glyph.unicode!), glyph);\n } else {\n //_console.log(\"no unicode\", glyph);\n }\n\n if (filteredGlyphs) {\n if (!filteredGlyphs.includes(glyph)) {\n continue;\n }\n }\n\n if (options.unicodeOnly || options.englishOnly) {\n if (!hasUnicode) {\n continue;\n }\n }\n if (options.script && hasUnicode) {\n const regex = new RegExp(`\\\\p{Script=${options.script}}`, \"u\");\n if (!regex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n if (options.englishOnly) {\n if (!englishRegex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n\n const bbox = glyph.getBoundingBox();\n minSpriteY = Math.min(minSpriteY, bbox.y1 * fontScale);\n maxSpriteY = Math.max(maxSpriteY, bbox.y2 * fontScale);\n\n glyphs.push(glyph);\n }\n\n // _console.log({\n // fontName: font.getEnglishName(\"fullName\"),\n // minSpriteY,\n // maxSpriteY,\n // });\n }\n\n minSpriteY = options.minSpriteY ?? minSpriteY;\n maxSpriteY = options.maxSpriteY ?? maxSpriteY;\n\n const maxSpriteHeight =\n options.maxSpriteheight ?? maxSpriteY - minSpriteY + strokeWidth;\n return { maxSpriteHeight, maxSpriteY, minSpriteY };\n}\n\nexport async function fontToSpriteSheet(\n font: Font | Font[],\n fontSize: number,\n spriteSheetName?: string,\n options?: FontToSpriteSheetOptions\n) {\n _console.assertTypeWithError(fontSize, \"number\");\n\n options = options\n ? { ...defaultFontToSpriteSheetOptions, ...options }\n : defaultFontToSpriteSheetOptions;\n\n const fonts = Array.isArray(font) ? font : [font];\n font = fonts[0];\n spriteSheetName = spriteSheetName || font.getEnglishName(\"fullName\");\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n sprites: [],\n };\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\")!;\n\n const { maxSpriteHeight, maxSpriteY, minSpriteY } = getFontMetrics(\n fonts,\n fontSize,\n options\n );\n const strokeWidth = options.stroke ? options.strokeWidth || 1 : 0;\n\n let string = options.string;\n if (string) {\n string = removeRedundantCharacters(string);\n _console.log(\"filtered string\", string);\n }\n\n for (let font of fonts) {\n const fontScale = (1 / font.unitsPerEm) * fontSize;\n\n const glyphs: Glyph[] = [];\n let filteredGlyphs: Glyph[] | undefined;\n if (string != undefined) {\n filteredGlyphs = font\n .stringToGlyphs(string)\n .filter((glyph) => glyph.unicode != undefined);\n string = removeSubstrings(\n string,\n filteredGlyphs.map((glyph) => String.fromCharCode(glyph.unicode!))\n );\n //_console.log(\"filteredString\", string);\n //_console.log(\"filteredGlyphs\", filteredGlyphs);\n }\n\n for (let index = 0; index < font.glyphs.length; index++) {\n const glyph = font.glyphs.get(index);\n const hasUnicode = glyph.unicode != undefined;\n if (hasUnicode) {\n //_console.log(String.fromCharCode(glyph.unicode!), glyph);\n } else {\n //_console.log(\"no unicode\", glyph);\n }\n\n if (filteredGlyphs) {\n if (!filteredGlyphs.includes(glyph)) {\n continue;\n }\n }\n\n if (options.unicodeOnly || options.englishOnly) {\n if (!hasUnicode) {\n continue;\n }\n }\n if (options.script && hasUnicode) {\n const regex = new RegExp(`\\\\p{Script=${options.script}}`, \"u\");\n if (!regex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n if (options.englishOnly) {\n if (!englishRegex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n\n glyphs.push(glyph);\n }\n\n for (let i = 0; i < glyphs.length; i++) {\n const glyph = glyphs[i];\n\n let name = glyph.name;\n if (glyph.unicode != undefined) {\n name = String.fromCharCode(glyph.unicode);\n }\n //_console.log(name, glyph);\n if (typeof name != \"string\") {\n continue;\n }\n\n const bbox = glyph.getBoundingBox();\n\n const spriteWidth =\n Math.max(\n Math.max(bbox.x2, bbox.x2 - bbox.x1),\n glyph.advanceWidth || 0\n ) *\n fontScale +\n strokeWidth;\n const spriteHeight = maxSpriteHeight;\n\n const commands: DisplayContextCommand[] = [];\n\n const path = glyph.getPath(\n -bbox.x1 * fontScale,\n bbox.y2 * fontScale,\n fontSize\n );\n if (options.stroke) {\n path.stroke = \"white\";\n path.strokeWidth = strokeWidth;\n commands.push({ type: \"setLineWidth\", lineWidth: strokeWidth });\n commands.push({ type: \"setIgnoreFill\", ignoreFill: true });\n } else {\n path.fill = \"white\";\n }\n\n const bitmapWidth = (bbox.x2 - bbox.x1) * fontScale + strokeWidth;\n const bitmapHeight = (bbox.y2 - bbox.y1) * fontScale + strokeWidth;\n\n const bitmapX = (spriteWidth - bitmapWidth) / 2;\n const bitmapY =\n (spriteHeight - bitmapHeight) / 2 - (bbox.y1 * fontScale - minSpriteY);\n if (options.usePath) {\n const pathOffset: Vector2 = {\n x: -bitmapWidth / 2 + bitmapX,\n y: -bitmapHeight / 2 + bitmapY,\n };\n //_console.log(`${name} path.commands`, path.commands);\n let curves: DisplayBezierCurve[] = [];\n let startPoint: Vector2 = { x: 0, y: 0 };\n\n const allCurves: DisplayBezierCurve[][] = [];\n const parsedPaths: { path: Vector2[]; isHole: boolean }[] = [];\n let wasHole = false;\n\n let pathCommands = path.commands;\n pathCommands.forEach((cmd) => {\n switch (cmd.type) {\n case \"M\": // moveTo\n {\n startPoint.x = cmd.x;\n startPoint.y = cmd.y;\n }\n break;\n\n case \"L\": // lineTo\n {\n const controlPoints: Vector2[] = [{ x: cmd.x, y: cmd.y }];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"segment\", controlPoints });\n }\n break;\n\n case \"Q\": // quadratic Bezier\n {\n const controlPoints: Vector2[] = [\n { x: cmd.x1, y: cmd.y1 },\n { x: cmd.x, y: cmd.y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"quadratic\", controlPoints });\n }\n break;\n\n case \"C\": // cubic Bezier\n {\n const controlPoints: Vector2[] = [\n { x: cmd.x1, y: cmd.y1 },\n { x: cmd.x2, y: cmd.y2 },\n { x: cmd.x, y: cmd.y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"cubic\", controlPoints });\n }\n break;\n\n case \"Z\": // closePath\n {\n if (curves.length === 0) {\n break;\n }\n\n curves = simplifyCurves(curves);\n\n // Flatten all control points\n const controlPoints = curves.flatMap((c) => c.controlPoints);\n\n // Apply path offset\n controlPoints.forEach((pt) => {\n pt.x = pt.x + pathOffset.x;\n pt.y = pt.y + pathOffset.y;\n });\n\n allCurves.push(curves);\n\n // Reset curves\n curves = [];\n }\n break;\n }\n });\n\n allCurves.sort((a, b) => {\n const aPoints = getCurvesPoints(a);\n const bPoints = getCurvesPoints(b);\n return contourArea(bPoints) - contourArea(aPoints);\n });\n\n allCurves.forEach((curve) => {\n const controlPoints = curve.flatMap((c) => c.controlPoints);\n const isHole = classifySubpath(controlPoints, parsedPaths, \"nonzero\");\n parsedPaths.push({ path: controlPoints, isHole });\n if (isHole != wasHole) {\n wasHole = isHole;\n if (isHole) {\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 0,\n });\n } else {\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 1,\n });\n }\n }\n\n const isSegments = curves.every((c) => c.type === \"segment\");\n if (isSegments) {\n commands.push({\n type: \"drawPolygon\",\n points: controlPoints,\n });\n } else {\n commands.push({ type: \"drawClosedPath\", curves });\n }\n });\n } else {\n if (bitmapWidth > 0 && bitmapHeight > 0) {\n canvas.width = bitmapWidth;\n canvas.height = bitmapHeight;\n ctx.imageSmoothingEnabled = false;\n\n ctx.fillStyle = \"black\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n path.draw(ctx);\n const { colorIndices } = await quantizeCanvas(canvas, 2, [\n \"#000000\",\n \"#ffffff\",\n ]);\n const bitmap: DisplayBitmap = {\n width: bitmapWidth,\n height: bitmapHeight,\n numberOfColors: 2,\n pixels: colorIndices,\n };\n\n commands.push({\n type: \"selectBitmapColor\",\n bitmapColorIndex: 1,\n colorIndex: 1,\n });\n if (false) {\n // debugging\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 2,\n });\n commands.push({\n type: \"drawRect\",\n offsetX: 0,\n offsetY: 0,\n width: spriteWidth,\n height: spriteHeight,\n });\n }\n\n commands.push({\n type: \"drawBitmap\",\n offsetX: bitmapX,\n offsetY: bitmapY,\n bitmap,\n });\n }\n }\n\n const sprite: DisplaySprite = {\n name,\n commands,\n width: spriteWidth,\n height: spriteHeight,\n };\n\n spriteSheet.sprites.push(sprite);\n }\n\n if (string != undefined && string.length == 0) {\n break;\n }\n }\n\n return spriteSheet;\n}\n\nexport function stringToSprites(\n string: string,\n spriteSheet: DisplaySpriteSheet,\n requireAll = false\n) {\n const sprites: DisplaySprite[] = [];\n let substring = string;\n while (substring.length > 0) {\n let longestSprite: DisplaySprite | undefined;\n\n spriteSheet.sprites.forEach((sprite) => {\n if (substring.startsWith(sprite.name)) {\n if (!longestSprite || sprite.name.length > longestSprite.name.length) {\n longestSprite = sprite;\n }\n }\n });\n\n // _console.log(\"longestSprite\", longestSprite);\n if (requireAll) {\n _console.assertWithError(\n longestSprite,\n `couldn't find sprite with name prefixing \"${substring}\"`\n );\n }\n\n if (longestSprite) {\n sprites.push(longestSprite);\n substring = substring.substring(longestSprite!.name.length);\n } else {\n substring = substring.substring(1);\n }\n //_console.log(\"new substring\", substring);\n }\n\n //_console.log(`string \"${string}\" to sprites`, sprites);\n return sprites;\n}\n\nexport function getReferencedSprites(\n sprite: DisplaySprite,\n spriteSheet: DisplaySpriteSheet\n) {\n const sprites: DisplaySprite[] = [];\n sprite.commands\n .filter((command) => command.type == \"drawSprite\")\n .map((command) => command.spriteIndex)\n .map((spriteIndex) => spriteSheet.sprites[spriteIndex])\n .forEach((_sprite) => {\n if (!sprites.includes(_sprite)) {\n sprites.push(_sprite);\n sprites.push(...getReferencedSprites(_sprite, spriteSheet));\n }\n });\n _console.log(\"referencedSprites\", sprite, sprites);\n return sprites;\n}\nexport function reduceSpriteSheet(\n spriteSheet: DisplaySpriteSheet,\n spriteNames: string | string[],\n requireAll = false\n) {\n const reducedSpriteSheet = Object.assign({}, spriteSheet);\n if (!(spriteNames instanceof Array)) {\n spriteNames = stringToSprites(spriteNames, spriteSheet, requireAll).map(\n (sprite) => sprite.name\n );\n }\n _console.log(\"reducingSpriteSheet\", spriteSheet, spriteNames);\n reducedSpriteSheet.sprites = [];\n spriteSheet.sprites.forEach((sprite) => {\n if (spriteNames.includes(sprite.name)) {\n reducedSpriteSheet.sprites.push(sprite);\n reducedSpriteSheet.sprites.push(\n ...getReferencedSprites(sprite, spriteSheet)\n );\n }\n });\n _console.log(\"reducedSpriteSheet\", reducedSpriteSheet);\n return reducedSpriteSheet;\n}\n\nexport function stringToSpriteLines(\n string: string,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState,\n requireAll = false,\n maxLineBreadth = Infinity,\n separators = [\" \"]\n): DisplaySpriteLines {\n _console.log(\"stringToSpriteLines\", string);\n const isSpritesDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesDirection\n );\n const isSpritesLineDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesLineDirection\n );\n const areSpritesDirectionsOrthogonal =\n isSpritesDirectionHorizontal != isSpritesLineDirectionHorizontal;\n\n const lineStrings = string.split(\"\\n\");\n let lineBreadth = 0;\n\n if (isSpritesLineDirectionHorizontal) {\n maxLineBreadth /= contextState.spriteScaleX;\n } else {\n maxLineBreadth /= contextState.spriteScaleY;\n }\n\n const sprites: {\n sprite: DisplaySprite;\n spriteSheet: DisplaySpriteSheet;\n }[][] = [];\n let latestSeparatorIndex = -1;\n let latestSeparator: string | undefined;\n let latestSeparatorLineBreadth: number | undefined;\n let latestSeparatorBreadth: number | undefined;\n const spritesLineIndices: number[][] = [];\n\n lineStrings.forEach((lineString) => {\n sprites.push([]);\n spritesLineIndices.push([]);\n const i = sprites.length - 1;\n if (areSpritesDirectionsOrthogonal) {\n lineBreadth = 0;\n } else {\n lineBreadth += contextState.spritesLineSpacing;\n }\n\n let lineSubstring = lineString;\n while (lineSubstring.length > 0) {\n let longestSprite: DisplaySprite | undefined;\n let longestSpriteSheet: DisplaySpriteSheet | undefined;\n for (let spriteSheetName in spriteSheets) {\n const spriteSheet = spriteSheets[spriteSheetName];\n spriteSheet.sprites.forEach((sprite) => {\n if (lineSubstring.startsWith(sprite.name)) {\n if (\n !longestSprite ||\n sprite.name.length > longestSprite.name.length\n ) {\n longestSprite = sprite;\n longestSpriteSheet = spriteSheet;\n }\n }\n });\n }\n //_console.log(\"longestSprite\", longestSprite);\n if (requireAll) {\n _console.assertWithError(\n longestSprite,\n `couldn't find sprite with name prefixing \"${lineSubstring}\"`\n );\n }\n\n if (longestSprite && longestSpriteSheet) {\n const isSeparator =\n separators.length > 0\n ? separators.includes(longestSprite.name)\n : true;\n\n sprites[i].push({\n sprite: longestSprite,\n spriteSheet: longestSpriteSheet,\n });\n\n // _console.log({\n // name: longestSprite!.name,\n // isSeparator,\n // lineBreadth,\n // latestSeparatorIndex,\n // latestSeparatorLineBreadth,\n // latestSeparator,\n // index: sprites[i].length - 1,\n // });\n\n let newLineBreadth = lineBreadth;\n const longestSpriteBreadth = isSpritesDirectionHorizontal\n ? longestSprite.width\n : longestSprite.height;\n newLineBreadth += longestSpriteBreadth;\n newLineBreadth += contextState.spritesSpacing;\n if (newLineBreadth >= maxLineBreadth) {\n if (isSeparator) {\n if (longestSprite.name.trim().length == 0) {\n sprites[i].pop();\n }\n spritesLineIndices[i].push(sprites[i].length);\n lineBreadth = 0;\n } else {\n if (latestSeparatorIndex != -1) {\n if (latestSeparator!.trim().length == 0) {\n sprites[i].splice(latestSeparatorIndex, 1);\n lineBreadth -= latestSeparatorBreadth!;\n latestSeparatorIndex;\n }\n spritesLineIndices[i].push(latestSeparatorIndex);\n lineBreadth = newLineBreadth - latestSeparatorLineBreadth!;\n } else {\n spritesLineIndices[i].push(sprites[i].length - 1);\n lineBreadth = 0;\n }\n }\n latestSeparatorIndex = -1;\n latestSeparator = undefined;\n } else {\n lineBreadth = newLineBreadth;\n\n if (isSeparator) {\n latestSeparator = longestSprite.name;\n latestSeparatorIndex = sprites[i].length - 1;\n //_console.log({ latestSeparatorIndex });\n latestSeparatorLineBreadth = lineBreadth;\n latestSeparatorBreadth = longestSpriteBreadth;\n }\n }\n\n lineSubstring = lineSubstring.substring(longestSprite!.name.length);\n } else {\n lineSubstring = lineSubstring.substring(1);\n }\n }\n });\n\n const spriteLines: DisplaySpriteLine[] = [];\n sprites.forEach((_sprites, i) => {\n let spriteLine: DisplaySpriteLine = [];\n spriteLines.push(spriteLine);\n\n let spriteSubLine: DisplaySpriteSubLine | undefined;\n\n _sprites.forEach(({ sprite, spriteSheet }, index) => {\n if (spritesLineIndices[i].includes(index)) {\n spriteLine = [];\n spriteLines.push(spriteLine);\n spriteSubLine = undefined;\n }\n\n if (!spriteSubLine || spriteSubLine.spriteSheetName != spriteSheet.name) {\n spriteSubLine = {\n spriteSheetName: spriteSheet.name,\n spriteNames: [],\n };\n spriteLine.push(spriteSubLine);\n }\n spriteSubLine.spriteNames.push(sprite.name);\n });\n });\n _console.log(`spriteLines for \"${string}\"`, spriteLines);\n return spriteLines;\n}\n\nexport function getFontMaxHeight(font: Font, fontSize: number) {\n const scale = (1 / font.unitsPerEm) * fontSize;\n const maxHeight = (font.ascender - font.descender) * scale;\n return maxHeight;\n}\nexport function getMaxSpriteSheetSize(spriteSheet: DisplaySpriteSheet) {\n const size: DisplaySize = { width: 0, height: 0 };\n spriteSheet.sprites.forEach((sprite) => {\n size.width = Math.max(size.width, sprite.width);\n size.height = Math.max(size.height, sprite.height);\n });\n return size;\n}\n\nexport function assertValidSpriteLines(\n displayManager: DisplayManagerInterface,\n spriteLines: DisplaySpriteLines\n) {\n spriteLines.forEach((spriteLine) => {\n spriteLine.forEach((spriteSubLine) => {\n const { spriteSheetName, spriteNames } = spriteSubLine;\n displayManager.assertLoadedSpriteSheet(spriteSheetName);\n const spriteSheet = displayManager.spriteSheets[spriteSheetName];\n spriteNames.forEach((spriteName) => {\n const sprite = spriteSheet.sprites.find(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n sprite,\n `no sprite with name \"${spriteName}\" found in spriteSheet \"${spriteSheetName}\"`\n );\n });\n });\n });\n}\n\nexport function getExpandedSpriteLines(\n spriteLines: DisplaySpriteLines,\n spriteSheets: Record<string, DisplaySpriteSheet>\n) {\n const expandedSpritesLines: DisplaySprite[][] = [];\n\n spriteLines.forEach((spriteLine) => {\n const _spritesLine: DisplaySprite[] = [];\n\n spriteLine.forEach(({ spriteSheetName, spriteNames }) => {\n const spriteSheet = spriteSheets[spriteSheetName];\n _console.assertWithError(\n spriteSheet,\n `no spriteSheet found with name \"${spriteSheetName}\"`\n );\n\n spriteNames.forEach((spriteName) => {\n const sprite = spriteSheet.sprites.find(\n (sprite) => sprite.name == spriteName\n )!;\n _console.assertWithError(\n sprite,\n `no sprite found with name \"${spriteName} in \"${spriteSheetName}\" spriteSheet`\n );\n _spritesLine.push(sprite);\n });\n });\n expandedSpritesLines.push(_spritesLine);\n });\n return expandedSpritesLines;\n}\n\nexport function getExpandedSpriteLinesSize(\n expandedSpritesLines: DisplaySprite[][],\n contextState: DisplayContextState\n) {\n const localSize = { width: 0, height: 0 };\n\n const isSpritesDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesDirection\n );\n const isSpritesLineDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesLineDirection\n );\n\n const areSpritesDirectionsOrthogonal =\n isSpritesDirectionHorizontal != isSpritesLineDirectionHorizontal;\n\n const breadthSizeKey = isSpritesDirectionHorizontal ? \"width\" : \"height\";\n const depthSizeKey = isSpritesLineDirectionHorizontal ? \"width\" : \"height\";\n\n if (!areSpritesDirectionsOrthogonal) {\n if (isSpritesDirectionHorizontal) {\n localSize.height += contextState.spritesLineHeight;\n } else {\n localSize.width += contextState.spritesLineHeight;\n }\n }\n\n const lineBreadths: number[] = [];\n\n expandedSpritesLines.forEach((expandedSpriteLine, lineIndex) => {\n let spritesLineBreadth = 0;\n\n expandedSpriteLine.forEach((sprite) => {\n spritesLineBreadth += isSpritesDirectionHorizontal\n ? sprite.width\n : sprite.height;\n spritesLineBreadth += contextState.spritesSpacing;\n });\n spritesLineBreadth -= contextState.spritesSpacing;\n\n if (areSpritesDirectionsOrthogonal) {\n localSize[breadthSizeKey] = Math.max(\n localSize[breadthSizeKey],\n spritesLineBreadth\n );\n\n localSize[depthSizeKey] += contextState.spritesLineHeight;\n } else {\n localSize[breadthSizeKey] += spritesLineBreadth;\n }\n\n localSize[depthSizeKey] += contextState.spritesLineSpacing;\n\n // _console.log({\n // lineIndex,\n // spritesBreadth: spritesSize[breadthSizeKey],\n // spritesDepth: spritesSize[depthSizeKey],\n // });\n\n lineBreadths.push(spritesLineBreadth);\n });\n localSize[depthSizeKey] -= contextState.spritesLineSpacing;\n\n // _console.log({\n // spritesWidth: spritesSize.width,\n // spritesHeight: spritesSize.height,\n // });\n\n const spritesScaledWidth =\n localSize.width * Math.abs(contextState.spriteScaleX);\n const spritesScaledHeight =\n localSize.height * Math.abs(contextState.spriteScaleY);\n\n const size: DisplaySize = {\n width: spritesScaledWidth,\n height: spritesScaledHeight,\n };\n\n return { localSize, size, lineBreadths };\n}\n\nexport function getSpriteLinesMetrics(\n spriteLines: DisplaySpriteLines,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState\n) {\n const expandedSpritesLines = getExpandedSpriteLines(\n spriteLines,\n spriteSheets\n );\n return {\n expandedSpritesLines,\n numberOfLines: expandedSpritesLines.length,\n ...getExpandedSpriteLinesSize(expandedSpritesLines, contextState),\n };\n}\n\nexport type DisplaySpriteLinesMetrics = {\n localSize: {\n width: number;\n height: number;\n };\n size: DisplaySize;\n lineBreadths: number[];\n expandedSpritesLines: DisplaySprite[][];\n numberOfLines: number;\n};\nexport function stringToSpriteLinesMetrics(\n string: string,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n): DisplaySpriteLinesMetrics {\n return getSpriteLinesMetrics(\n stringToSpriteLines(\n string,\n spriteSheets,\n contextState,\n requireAll,\n maxLineBreadth,\n separators\n ),\n spriteSheets,\n contextState\n );\n}\n","// @ts-expect-error\nimport RGBQuant from \"rgbquant\";\nimport { createConsole } from \"./Console.ts\";\nimport { hexToRGB, rgbToHex } from \"./ColorUtils.ts\";\nimport { getVector3Length, Vector3 } from \"./MathUtils.ts\";\nimport {\n DisplayColorRGB,\n numberOfColorsToPixelDepth,\n pixelDepthToNumberOfColors,\n pixelDepthToPixelBitWidth,\n pixelDepthToPixelsPerByte,\n} from \"./DisplayUtils.ts\";\nimport { DisplayBitmap, DisplayPixelDepths } from \"../DisplayManager.ts\";\nimport {\n calculateSpriteSheetHeaderLength,\n DisplaySprite,\n DisplaySpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\n\nconst _console = createConsole(\"DisplayBitmapUtils\", { log: false });\n\nexport const drawBitmapHeaderLength = 2 + 2 + 2 + 4 + 1 + 2; // x, y, width, numberOfPixels, numberOfColors, dataLength\n\nexport function getBitmapData(bitmap: DisplayBitmap) {\n const pixelDataLength = getBitmapNumberOfBytes(bitmap);\n const dataView = new DataView(new ArrayBuffer(pixelDataLength));\n const pixelDepth = numberOfColorsToPixelDepth(bitmap.numberOfColors)!;\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n bitmap.pixels.forEach((bitmapColorIndex, pixelIndex) => {\n const byteIndex = Math.floor(pixelIndex / pixelsPerByte);\n const byteSlot = pixelIndex % pixelsPerByte;\n const pixelBitWidth = pixelDepthToPixelBitWidth(pixelDepth);\n const bitOffset = pixelBitWidth * byteSlot;\n const shift = 8 - pixelBitWidth - bitOffset;\n let value = dataView.getUint8(byteIndex);\n value |= bitmapColorIndex << shift;\n dataView.setUint8(byteIndex, value);\n });\n _console.log(\"getBitmapData\", bitmap, dataView);\n return dataView;\n}\n\nexport async function quantizeCanvas(\n canvas: HTMLCanvasElement,\n numberOfColors: number,\n colors?: string[]\n) {\n _console.assertWithError(\n numberOfColors > 1,\n \"numberOfColors must be greater than 1\"\n );\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n removeAlphaFromCanvas(canvas);\n\n const isSmall = canvas.width * canvas.height < 4;\n\n const quantOptions = {\n method: isSmall ? 1 : 2,\n colors: numberOfColors,\n dithKern: null, // Disable dithering\n useCache: false, // Disable color caching to force exact matches\n reIndex: true, // Ensure strict re-indexing to the palette\n orDist: \"manhattan\",\n };\n\n if (colors) {\n // @ts-ignore\n quantOptions.palette = colors.map((color) => {\n const rgb = hexToRGB(color);\n if (rgb) {\n const { r, g, b } = rgb;\n return [r, g, b];\n } else {\n _console.error(`invalid rgb hex \"${color}\"`);\n }\n });\n }\n //_console.log(\"quantizeImage options\", quantOptions);\n const quantizer = new RGBQuant(quantOptions);\n const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n quantizer.sample(imageData);\n\n const quantizedPixels = quantizer.reduce(imageData.data);\n const quantizedImageData = new ImageData(\n new Uint8ClampedArray(quantizedPixels.buffer),\n canvas.width,\n canvas.height\n );\n ctx.putImageData(quantizedImageData, 0, 0);\n\n const pixels = quantizedImageData.data;\n\n const quantizedPaletteData: Uint8Array = quantizer.palette();\n const numberOfQuantizedPaletteColors = quantizedPaletteData.byteLength / 4;\n //_console.log(\"quantizedPaletteData\", quantizedPaletteData);\n const quantizedPaletteColors: DisplayColorRGB[] = [];\n let closestColorIndexToBlack = 0;\n let closestColorDistanceToBlack = Infinity;\n const vector3: Vector3 = { x: 0, y: 0, z: 0 };\n for (\n let colorIndex = 0;\n colorIndex < numberOfQuantizedPaletteColors;\n colorIndex++\n ) {\n const rgb: DisplayColorRGB = {\n r: quantizedPaletteData[colorIndex * 4],\n g: quantizedPaletteData[colorIndex * 4 + 1],\n b: quantizedPaletteData[colorIndex * 4 + 2],\n };\n quantizedPaletteColors.push(rgb);\n vector3.x = rgb.r;\n vector3.y = rgb.g;\n vector3.z = rgb.b;\n\n const distanceToBlack = getVector3Length(vector3);\n if (distanceToBlack < closestColorDistanceToBlack) {\n closestColorDistanceToBlack = distanceToBlack;\n closestColorIndexToBlack = colorIndex;\n }\n }\n //_console.log({ closestColorIndexToBlack, closestColorDistanceToBlack });\n if (closestColorIndexToBlack != 0) {\n const [currentBlack, newBlack] = [\n quantizedPaletteColors[0],\n quantizedPaletteColors[closestColorIndexToBlack],\n ];\n quantizedPaletteColors[0] = newBlack;\n quantizedPaletteColors[closestColorIndexToBlack] = currentBlack;\n }\n //_console.log(\"quantizedPaletteColors\", quantizedPaletteColors);\n const quantizedColors = quantizedPaletteColors.map((rgb, index) => {\n const hex = rgbToHex(rgb);\n return hex;\n });\n //_console.log(\"quantizedColors\", quantizedColors);\n\n const quantizedColorIndices: number[] = [];\n for (let i = 0; i < pixels.length; i += 4) {\n const r = pixels[i];\n const g = pixels[i + 1];\n const b = pixels[i + 2];\n const a = pixels[i + 3];\n\n const hex = rgbToHex({ r, g, b });\n quantizedColorIndices.push(quantizedColors.indexOf(hex));\n }\n //_console.log(\"quantizedColorIndices\", quantizedColorIndices);\n\n const promise = new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject();\n }\n }, \"image/png\");\n });\n\n const blob = await promise;\n return {\n blob,\n colors: quantizedColors,\n colorIndices: quantizedColorIndices,\n };\n}\n\nexport async function quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n let { naturalWidth: imageWidth, naturalHeight: imageHeight } = image;\n _console.log({ imageWidth, imageHeight });\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n\n ctx.drawImage(image, 0, 0, width, height);\n\n return quantizeCanvas(canvas, numberOfColors, colors);\n}\n\nexport function resizeImage(\n image: CanvasImageSource,\n width: number,\n height: number,\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n\n ctx.drawImage(image, 0, 0, width, height);\n\n return canvas;\n}\nexport function cropCanvas(\n canvas: HTMLCanvasElement,\n x: number,\n y: number,\n width: number,\n height: number,\n targetCanvas?: HTMLCanvasElement\n) {\n targetCanvas = targetCanvas || document.createElement(\"canvas\");\n const ctx = targetCanvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n targetCanvas.width = width;\n targetCanvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n ctx.drawImage(canvas, x, y, width, height, 0, 0, width, height);\n\n return targetCanvas;\n}\nexport function removeAlphaFromCanvas(canvas: HTMLCanvasElement) {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n const data = imageData.data;\n\n // turn any non-opaque pixel to black\n for (let i = 0; i < data.length; i += 4) {\n const alpha = data[i + 3];\n\n if (alpha < 255) {\n data[i] = 0;\n data[i + 1] = 0;\n data[i + 2] = 0;\n data[i + 3] = 255;\n }\n }\n\n ctx.putImageData(imageData, 0, 0);\n\n return canvas;\n}\n\nexport async function canvasToBlob(\n canvas: HTMLCanvasElement,\n type: \"image/png\" | \"image/jpeg\" = \"image/jpeg\",\n quality: number = 1\n) {\n const promise = new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject();\n }\n },\n type,\n quality\n );\n });\n const blob = await promise;\n return blob;\n}\n\nexport async function resizeAndQuantizeImage(\n image: CanvasImageSource,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n resizeImage(image, width, height, canvas);\n removeAlphaFromCanvas(canvas);\n return quantizeCanvas(canvas, numberOfColors, colors);\n}\n\nexport async function imageToBitmap(\n image: CanvasImageSource,\n width: number,\n height: number,\n colors: string[],\n bitmapColorIndices: number[],\n numberOfColors?: number\n) {\n if (numberOfColors == undefined) {\n numberOfColors = colors.length;\n }\n const bitmapColors = bitmapColorIndices\n .map((bitmapColorIndex) => colors[bitmapColorIndex])\n .slice(0, numberOfColors);\n const { blob, colorIndices } = await resizeAndQuantizeImage(\n image,\n width,\n height,\n numberOfColors,\n bitmapColors\n );\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n return { blob, bitmap };\n}\n\nconst drawSpriteBitmapCommandHeaderLength = 1 + 2 + 2 + 2 + 2 + 1 + 2; // command, offetXY, width, numberOfPixels, numberOfColors, pixelDataLength\nexport async function canvasToBitmaps(\n canvas: HTMLCanvasElement,\n numberOfColors: number,\n mtu: number\n) {\n const { blob, colors, colorIndices } = await quantizeCanvas(\n canvas,\n numberOfColors\n );\n const bitmapRows: DisplayBitmap[][] = [];\n\n const { width, height } = canvas;\n\n const numberOfPixels = width * height;\n const pixelDepth = DisplayPixelDepths.find(\n (pixelDepth) => pixelDepthToNumberOfColors(pixelDepth) >= numberOfColors\n )!;\n _console.assertWithError(\n pixelDepth,\n `no pixelDepth found that covers ${numberOfColors} colors`\n );\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfBytes = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n width,\n height,\n numberOfPixels,\n pixelDepth,\n pixelsPerByte,\n numberOfBytes,\n mtu,\n });\n\n const maxPixelDataLength = mtu - (drawSpriteBitmapCommandHeaderLength + 5);\n const maxPixels = Math.floor(maxPixelDataLength / pixelsPerByte);\n const maxBitmapWidth = Math.min(maxPixels, width);\n let maxBitmapHeight = 1;\n if (maxBitmapWidth == width) {\n const bitmapRowPixelDataLength = Math.ceil(width / pixelsPerByte);\n maxBitmapHeight = Math.floor(maxPixelDataLength / bitmapRowPixelDataLength);\n }\n _console.log({\n maxPixelDataLength,\n maxPixels,\n maxBitmapHeight,\n maxBitmapWidth,\n });\n\n if (maxBitmapHeight >= height) {\n _console.log(\"image is small enough for a single bitmap\");\n\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n bitmapRows.push([bitmap]);\n } else {\n let offsetX = 0;\n let offsetY = 0;\n const bitmapCanvas: HTMLCanvasElement = document.createElement(\"canvas\");\n const bitmapColorIndices: number[] = new Array(numberOfColors)\n .fill(0)\n .map((_, i) => i);\n while (offsetY < height) {\n const bitmapHeight = Math.min(maxBitmapHeight, height - offsetY);\n offsetX = 0;\n const bitmapRow: DisplayBitmap[] = [];\n bitmapRows.push(bitmapRow);\n\n while (offsetX < width) {\n const bitmapWidth = Math.min(maxBitmapWidth, width - offsetX);\n cropCanvas(\n canvas,\n offsetX,\n offsetY,\n bitmapWidth,\n bitmapHeight,\n bitmapCanvas\n );\n // _console.log(`cropping bitmap`, {\n // bitmapWidth,\n // bitmapHeight,\n // offsetX,\n // offsetY,\n // });\n const { bitmap } = await imageToBitmap(\n bitmapCanvas,\n bitmapWidth,\n bitmapHeight,\n colors,\n bitmapColorIndices,\n numberOfColors\n );\n // _console.log(\"bitmap\", bitmap);\n bitmapRow.push(bitmap);\n offsetX += bitmapWidth;\n }\n offsetY += bitmapHeight;\n }\n }\n\n return { bitmapRows, colors };\n}\nexport async function imageToBitmaps(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n mtu: number\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToBitmaps(canvas, numberOfColors, mtu);\n}\n\nexport function getBitmapNumberOfBytes(bitmap: DisplayBitmap) {\n const pixelDepth = numberOfColorsToPixelDepth(bitmap.numberOfColors)!;\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfPixels = bitmap.pixels.length;\n const pixelDataLength = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n pixelDepth,\n pixelsPerByte,\n numberOfPixels,\n pixelDataLength,\n });\n return pixelDataLength;\n}\nexport function assertValidBitmapPixels(bitmap: DisplayBitmap) {\n _console.assertRangeWithError(\n \"bitmap.pixels.length\",\n bitmap.pixels.length,\n bitmap.width * (bitmap.height - 1) + 1,\n bitmap.width * bitmap.height\n );\n bitmap.pixels.forEach((pixel, index) => {\n _console.assertRangeWithError(\n `bitmap.pixels[${index}]`,\n pixel,\n 0,\n bitmap.numberOfColors - 1\n );\n });\n}\n\nexport async function canvasToSprite(\n canvas: HTMLCanvasElement,\n spriteName: string,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0\n) {\n const { width, height } = canvas;\n\n let palette = spriteSheet.palettes?.find(\n (palette) => palette.name == paletteName\n );\n if (!palette) {\n palette = {\n name: paletteName,\n numberOfColors,\n colors: new Array(numberOfColors).fill(\"#000000\"),\n };\n spriteSheet.palettes = spriteSheet.palettes || [];\n spriteSheet.palettes?.push(palette);\n }\n _console.log(\"pallete\", palette);\n\n // _console.assertWithError(\n // numberOfColors + paletteOffset <= palette.numberOfColors,\n // `invalid numberOfColors ${numberOfColors} + offset ${paletteOffset} (max ${palette.numberOfColors})`\n // );\n\n const sprite: DisplaySprite = {\n name: spriteName,\n width,\n height,\n paletteSwaps: [],\n commands: [],\n };\n\n const results = await quantizeCanvas(\n canvas,\n numberOfColors,\n !overridePalette ? palette.colors : undefined\n );\n const blob = results.blob;\n const colorIndices = results.colorIndices;\n if (overridePalette) {\n results.colors.forEach((color, index) => {\n palette.colors[index + paletteOffset] = color;\n });\n }\n\n sprite.commands.push({\n type: \"selectBitmapColors\",\n bitmapColorPairs: new Array(numberOfColors).fill(0).map((_, index) => ({\n bitmapColorIndex: index,\n colorIndex: index + paletteOffset,\n })),\n });\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n sprite.commands.push({ type: \"drawBitmap\", offsetX: 0, offsetY: 0, bitmap });\n\n const spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n if (spriteIndex == -1) {\n spriteSheet.sprites.push(sprite);\n } else {\n _console.log(`overwriting spriteIndex ${spriteIndex}`);\n spriteSheet.sprites[spriteIndex] = sprite;\n }\n\n return { sprite, blob };\n}\nexport async function imageToSprite(\n image: HTMLImageElement,\n spriteName: string,\n width: number,\n height: number,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToSprite(\n canvas,\n spriteName,\n numberOfColors,\n paletteName,\n overridePalette,\n spriteSheet,\n paletteOffset\n );\n}\n\nconst spriteSheetWithSingleBitmapCommandLength =\n calculateSpriteSheetHeaderLength(1) + drawSpriteBitmapCommandHeaderLength;\nfunction spriteSheetWithBitmapCommandAndSelectBitmapColorsLength(\n numberOfColors: number\n) {\n return (\n spriteSheetWithSingleBitmapCommandLength + (1 + 1 + numberOfColors * 2)\n ); // command, numberOfPairs, ...pairs\n}\n\nexport async function canvasToSpriteSheet(\n canvas: HTMLCanvasElement,\n spriteSheetName: string,\n numberOfColors: number,\n paletteName: string,\n maxFileLength?: number\n) {\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n palettes: [],\n paletteSwaps: [],\n sprites: [],\n };\n\n if (maxFileLength == undefined) {\n await canvasToSprite(\n canvas,\n \"image\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet\n );\n } else {\n const { width, height } = canvas;\n const numberOfPixels = width * height;\n const pixelDepth = DisplayPixelDepths.find(\n (pixelDepth) => pixelDepthToNumberOfColors(pixelDepth) >= numberOfColors\n )!;\n _console.assertWithError(\n pixelDepth,\n `no pixelDepth found that covers ${numberOfColors} colors`\n );\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfBytes = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n width,\n height,\n numberOfPixels,\n pixelDepth,\n pixelsPerByte,\n numberOfBytes,\n maxFileLength,\n });\n\n const maxPixelDataLength =\n maxFileLength -\n (spriteSheetWithBitmapCommandAndSelectBitmapColorsLength(numberOfColors) +\n 5);\n const imageRowPixelDataLength = Math.ceil(width / pixelsPerByte);\n const maxSpriteHeight = Math.floor(\n maxPixelDataLength / imageRowPixelDataLength\n );\n // _console.log({\n // maxPixelDataLength,\n // imageRowPixelDataLength,\n // maxSpriteHeight,\n // });\n\n if (maxSpriteHeight >= height) {\n _console.log(\"image is small enough for a single sprite\");\n await canvasToSprite(\n canvas,\n \"image\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet\n );\n } else {\n const { colors } = await quantizeCanvas(canvas, numberOfColors);\n spriteSheet.palettes?.push({ name: paletteName, numberOfColors, colors });\n\n let offsetY = 0;\n let imageIndex = 0;\n const spriteCanvas: HTMLCanvasElement = document.createElement(\"canvas\");\n\n while (offsetY < height) {\n const spriteHeight = Math.min(maxSpriteHeight, height - offsetY);\n cropCanvas(canvas, 0, offsetY, width, spriteHeight, spriteCanvas);\n offsetY += spriteHeight;\n _console.log(`cropping sprite ${imageIndex}`, {\n offsetY,\n width,\n spriteHeight,\n });\n await canvasToSprite(\n spriteCanvas,\n `image${imageIndex}`,\n numberOfColors,\n paletteName,\n false,\n spriteSheet\n );\n imageIndex++;\n }\n }\n }\n\n return spriteSheet;\n}\n\nexport async function imageToSpriteSheet(\n image: HTMLImageElement,\n spriteSheetName: string,\n width: number,\n height: number,\n numberOfColors: number,\n paletteName: string,\n maxFileLength?: number\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToSpriteSheet(\n canvas,\n spriteSheetName,\n numberOfColors,\n paletteName,\n maxFileLength\n );\n}\n","import {\n DisplayBitmapColorPair,\n DisplayBrightness,\n DisplaySpriteColorPair,\n DisplayBitmap,\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayWireframe,\n DisplaySize,\n} from \"../DisplayManager.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { DisplayContextCommand } from \"./DisplayContextCommand.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayContextState,\n DisplayDirection,\n DisplaySegmentCap,\n} from \"./DisplayContextState.ts\";\nimport {\n DisplaySprite,\n DisplaySpriteLines,\n DisplaySpriteLinesMetrics,\n DisplaySpritePaletteSwap,\n DisplaySpriteSheet,\n DisplaySpriteSheetPalette,\n DisplaySpriteSheetPaletteSwap,\n reduceSpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\nimport {\n DisplayScaleDirection,\n DisplayColorRGB,\n DisplayCropDirection,\n} from \"./DisplayUtils.ts\";\nimport { degToRad, Vector2 } from \"./MathUtils.ts\";\n\nconst _console = createConsole(\"DisplayManagerInterface\", { log: false });\n\nexport interface DisplayManagerInterface {\n get isReady(): boolean;\n\n get contextState(): DisplayContextState;\n\n flushContextCommands(): Promise<void>;\n\n get brightness(): DisplayBrightness;\n setBrightness(\n newDisplayBrightness: DisplayBrightness,\n sendImmediately?: boolean\n ): Promise<void>;\n\n show(sendImmediately?: boolean): Promise<void>;\n clear(sendImmediately?: boolean): Promise<void>;\n\n get colors(): string[];\n get numberOfColors(): number;\n setColor(\n colorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n\n assertValidColorIndex(colorIndex: number): void;\n assertValidLineWidth(lineWidth: number): void;\n assertValidNumberOfColors(numberOfColors: number): void;\n assertValidBitmap(bitmap: DisplayBitmap, checkSize?: boolean): void;\n\n get opacities(): number[];\n setColorOpacity(\n colorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setOpacity(opacity: number, sendImmediately?: boolean): Promise<void>;\n\n saveContext(sendImmediately?: boolean): Promise<void>;\n restoreContext(sendImmediately?: boolean): Promise<void>;\n\n selectFillColor(\n fillColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectBackgroundColor(\n backgroundColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectLineColor(\n lineColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setLineWidth(lineWidth: number, sendImmediately?: boolean): Promise<void>;\n\n setIgnoreFill(ignoreFill: boolean, sendImmediately?: boolean): Promise<void>;\n setIgnoreLine(ignoreLine: boolean, sendImmediately?: boolean): Promise<void>;\n setFillBackground(\n fillBackground: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setAlignment(\n alignmentDirection: DisplayAlignmentDirection,\n alignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setHorizontalAlignment(\n horizontalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setVerticalAlignment(\n verticalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n resetAlignment(sendImmediately?: boolean): Promise<void>;\n\n setRotation(\n rotation: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n clearRotation(sendImmediately?: boolean): Promise<void>;\n\n setSegmentStartCap(\n segmentStartCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentEndCap(\n segmentEndCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentCap(\n segmentCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSegmentStartRadius(\n segmentStartRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentEndRadius(\n segmentEndRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentRadius(\n segmentRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setCropTop(cropTop: number, sendImmediately?: boolean): Promise<void>;\n setCropRight(cropRight: number, sendImmediately?: boolean): Promise<void>;\n setCropBottom(cropBottom: number, sendImmediately?: boolean): Promise<void>;\n setCropLeft(cropLeft: number, sendImmediately?: boolean): Promise<void>;\n clearCrop(sendImmediately?: boolean): Promise<void>;\n\n setRotationCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropTop(\n rotationCropTop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropRight(\n rotationCropRight: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropBottom(\n rotationCropBottom: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropLeft(\n rotationCropLeft: number,\n sendImmediately?: boolean\n ): Promise<void>;\n clearRotationCrop(sendImmediately?: boolean): Promise<void>;\n\n selectBitmapColor(\n bitmapColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get bitmapColorIndices(): number[];\n get bitmapColors(): string[];\n selectBitmapColors(\n bitmapColorPairs: DisplayBitmapColorPair[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n setBitmapColor(\n bitmapColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapColorOpacity(\n bitmapColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setBitmapScaleDirection(\n direction: DisplayScaleDirection,\n bitmapScale: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScaleX(\n bitmapScaleX: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScaleY(\n bitmapScaleY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScale(bitmapScale: number, sendImmediately?: boolean): Promise<void>;\n resetBitmapScale(sendImmediately?: boolean): Promise<void>;\n\n selectSpriteColor(\n spriteColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get spriteColorIndices(): number[];\n get spriteColors(): string[];\n selectSpriteColors(\n spriteColorPairs: DisplaySpriteColorPair[],\n sendImmediately?: boolean\n ): Promise<void>;\n resetSpriteColors(sendImmediately?: boolean): Promise<void>;\n\n setSpriteColor(\n spriteColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteColorOpacity(\n spriteColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpriteScaleDirection(\n direction: DisplayScaleDirection,\n spriteScale: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScaleX(\n spriteScaleX: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScaleY(\n spriteScaleY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScale(spriteScale: number, sendImmediately?: boolean): Promise<void>;\n resetSpriteScale(sendImmediately?: boolean): Promise<void>;\n\n setSpritesLineHeight(\n spritesLineHeight: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesDirectionGeneric(\n direction: DisplayDirection,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesDirection(\n spritesDirection: DisplayDirection,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineDirection(\n spritesLineDirection: DisplayDirection,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesSpacingGeneric(\n spacing: number,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesAlignmentGeneric(\n alignment: DisplayAlignment,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesAlignment(\n spritesAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineAlignment(\n spritesLineAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n\n clearRect(\n x: number,\n y: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRoundRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n borderRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawCircle(\n offsetX: number,\n offsetY: number,\n radius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRegularPolygon(\n offsetX: number,\n offsetY: number,\n radius: number,\n numberOfSides: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawPolygon(points: Vector2[], sendImmediately?: boolean): Promise<void>;\n\n drawWireframe(\n wireframe: DisplayWireframe,\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawCurve(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawCurves(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawQuadraticBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawQuadraticBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawCubicBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawCubicBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n _drawPath(\n isClosed: boolean,\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawClosedPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawSegment(\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSegments(points: Vector2[], sendImmediately?: boolean): Promise<void>;\n\n drawArc(\n offsetX: number,\n offsetY: number,\n radius: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n drawArcEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawBitmap(\n offsetX: number,\n offsetY: number,\n bitmap: DisplayBitmap,\n sendImmediately?: boolean\n ): Promise<void>;\n\n runContextCommand(\n command: DisplayContextCommand,\n sendImmediately?: boolean\n ): Promise<void>;\n\n runContextCommands(\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n imageToBitmap(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors?: number\n ): Promise<{\n blob: Blob;\n bitmap: DisplayBitmap;\n }>;\n quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n ): Promise<{\n blob: Blob;\n colors: string[];\n colorIndices: number[];\n }>;\n resizeAndQuantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n ): Promise<{\n blob: Blob;\n colors: string[];\n colorIndices: number[];\n }>;\n\n uploadSpriteSheet(spriteSheet: DisplaySpriteSheet): Promise<void>;\n uploadSpriteSheets(spriteSheets: DisplaySpriteSheet[]): Promise<void>;\n selectSpriteSheet(\n spriteSheetName: string,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSprite(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n sendImmediately?: boolean\n ): Promise<void>;\n stringToSpriteLines(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLines;\n stringToSpriteLinesMetrics(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLinesMetrics;\n drawSprites(\n offsetX: number,\n offsetY: number,\n spriteLines: DisplaySpriteLines,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSpritesString(\n offsetX: number,\n offsetY: number,\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[],\n sendImmediately?: boolean\n ): Promise<void>;\n assertLoadedSpriteSheet(spriteSheetName: string): void;\n assertSelectedSpriteSheet(spriteSheetName: string): void;\n assertAnySelectedSpriteSheet(): void;\n assertSprite(spriteName: string): void;\n getSprite(spriteName: string): DisplaySprite | undefined;\n getSpriteSheetPalette(\n paletteName: string\n ): DisplaySpriteSheetPalette | undefined;\n getSpriteSheetPaletteSwap(\n paletteSwapName: string\n ): DisplaySpriteSheetPaletteSwap | undefined;\n getSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string\n ): DisplaySpritePaletteSwap | undefined;\n\n drawSpriteFromSpriteSheet(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get selectedSpriteSheet(): DisplaySpriteSheet | undefined;\n get selectedSpriteSheetName(): string | undefined;\n\n spriteSheets: Record<string, DisplaySpriteSheet>;\n spriteSheetIndices: Record<string, number>;\n\n assertSpriteSheetPalette(paletteName: string): void;\n assertSpriteSheetPaletteSwap(paletteSwapName: string): void;\n assertSpritePaletteSwap(spriteName: string, paletteSwapName: string): void;\n selectSpriteSheetPalette(\n paletteName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectSpriteSheetPaletteSwap(\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n serializeSpriteSheet(spriteSheet: DisplaySpriteSheet): ArrayBuffer;\n\n startSprite(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n endSprite(sendImmediately?: boolean): Promise<void>;\n}\n\nexport async function runDisplayContextCommand(\n displayManager: DisplayManagerInterface,\n command: DisplayContextCommand,\n sendImmediately?: boolean\n) {\n if (command.hide) {\n return;\n }\n switch (command.type) {\n case \"show\":\n await displayManager.show(sendImmediately);\n break;\n case \"clear\":\n await displayManager.clear(sendImmediately);\n break;\n case \"saveContext\":\n //await displayManager.saveContext(sendImmediately);\n break;\n case \"restoreContext\":\n //await displayManager.restoreContext(sendImmediately);\n break;\n case \"clearRotation\":\n await displayManager.clearRotation(sendImmediately);\n break;\n case \"clearCrop\":\n await displayManager.clearCrop(sendImmediately);\n break;\n case \"clearRotationCrop\":\n await displayManager.clearRotationCrop(sendImmediately);\n break;\n case \"resetBitmapScale\":\n await displayManager.resetBitmapScale(sendImmediately);\n break;\n case \"resetSpriteScale\":\n await displayManager.resetSpriteScale(sendImmediately);\n break;\n case \"setColor\":\n {\n const { colorIndex, color } = command;\n await displayManager.setColor(colorIndex, color, sendImmediately);\n }\n break;\n case \"setColorOpacity\":\n {\n const { colorIndex, opacity } = command;\n await displayManager.setColorOpacity(\n colorIndex,\n opacity,\n sendImmediately\n );\n }\n break;\n case \"setOpacity\":\n {\n const { opacity } = command;\n await displayManager.setOpacity(opacity, sendImmediately);\n }\n break;\n case \"selectBackgroundColor\":\n {\n const { backgroundColorIndex } = command;\n await displayManager.selectBackgroundColor(\n backgroundColorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectFillColor\":\n {\n const { fillColorIndex } = command;\n await displayManager.selectFillColor(fillColorIndex, sendImmediately);\n }\n break;\n case \"selectLineColor\":\n {\n const { lineColorIndex } = command;\n await displayManager.selectLineColor(lineColorIndex, sendImmediately);\n }\n break;\n case \"setIgnoreFill\":\n {\n const { ignoreFill } = command;\n await displayManager.setIgnoreFill(ignoreFill, sendImmediately);\n }\n break;\n case \"setIgnoreLine\":\n {\n const { ignoreLine } = command;\n await displayManager.setIgnoreLine(ignoreLine, sendImmediately);\n }\n break;\n case \"setFillBackground\":\n {\n const { fillBackground } = command;\n await displayManager.setFillBackground(fillBackground, sendImmediately);\n }\n break;\n case \"setLineWidth\":\n {\n const { lineWidth } = command;\n await displayManager.setLineWidth(lineWidth, sendImmediately);\n }\n break;\n case \"setRotation\":\n {\n let { rotation, isRadians } = command;\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation;\n await displayManager.setRotation(rotation, true, sendImmediately);\n }\n break;\n case \"setSegmentStartCap\":\n {\n const { segmentStartCap } = command;\n await displayManager.setSegmentStartCap(\n segmentStartCap,\n sendImmediately\n );\n }\n break;\n case \"setSegmentEndCap\":\n {\n const { segmentEndCap } = command;\n await displayManager.setSegmentEndCap(segmentEndCap, sendImmediately);\n }\n break;\n case \"setSegmentCap\":\n {\n const { segmentCap } = command;\n await displayManager.setSegmentCap(segmentCap, sendImmediately);\n }\n break;\n case \"setSegmentStartRadius\":\n {\n const { segmentStartRadius } = command;\n await displayManager.setSegmentStartRadius(\n segmentStartRadius,\n sendImmediately\n );\n }\n break;\n case \"setSegmentEndRadius\":\n {\n const { segmentEndRadius } = command;\n await displayManager.setSegmentEndRadius(\n segmentEndRadius,\n sendImmediately\n );\n }\n break;\n case \"setSegmentRadius\":\n {\n const { segmentRadius } = command;\n await displayManager.setSegmentRadius(segmentRadius, sendImmediately);\n }\n break;\n case \"setHorizontalAlignment\":\n {\n const { horizontalAlignment } = command;\n await displayManager.setHorizontalAlignment(\n horizontalAlignment,\n sendImmediately\n );\n }\n break;\n case \"setVerticalAlignment\":\n {\n const { verticalAlignment } = command;\n await displayManager.setVerticalAlignment(\n verticalAlignment,\n sendImmediately\n );\n }\n break;\n case \"resetAlignment\":\n {\n await displayManager.resetAlignment(sendImmediately);\n }\n break;\n case \"setCropTop\":\n {\n const { cropTop } = command;\n await displayManager.setCropTop(cropTop, sendImmediately);\n }\n break;\n case \"setCropRight\":\n {\n const { cropRight } = command;\n await displayManager.setCropRight(cropRight, sendImmediately);\n }\n break;\n case \"setCropBottom\":\n {\n const { cropBottom } = command;\n await displayManager.setCropBottom(cropBottom, sendImmediately);\n }\n break;\n case \"setCropLeft\":\n {\n const { cropLeft } = command;\n await displayManager.setCropLeft(cropLeft, sendImmediately);\n }\n break;\n case \"setRotationCropTop\":\n {\n const { rotationCropTop } = command;\n await displayManager.setRotationCropTop(\n rotationCropTop,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropRight\":\n {\n const { rotationCropRight } = command;\n await displayManager.setRotationCropRight(\n rotationCropRight,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropBottom\":\n {\n const { rotationCropBottom } = command;\n await displayManager.setRotationCropBottom(\n rotationCropBottom,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropLeft\":\n {\n const { rotationCropLeft } = command;\n await displayManager.setRotationCropLeft(\n rotationCropLeft,\n sendImmediately\n );\n }\n break;\n case \"selectBitmapColor\":\n {\n const { bitmapColorIndex, colorIndex } = command;\n await displayManager.selectBitmapColor(\n bitmapColorIndex,\n colorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectBitmapColors\":\n {\n const { bitmapColorPairs } = command;\n await displayManager.selectBitmapColors(\n bitmapColorPairs,\n sendImmediately\n );\n }\n break;\n case \"setBitmapScaleX\":\n {\n const { bitmapScaleX } = command;\n await displayManager.setBitmapScaleX(bitmapScaleX, sendImmediately);\n }\n break;\n case \"setBitmapScaleY\":\n {\n const { bitmapScaleY } = command;\n await displayManager.setBitmapScaleY(bitmapScaleY, sendImmediately);\n }\n break;\n case \"setBitmapScale\":\n {\n const { bitmapScale } = command;\n await displayManager.setBitmapScale(bitmapScale, sendImmediately);\n }\n break;\n case \"selectSpriteColor\":\n {\n const { spriteColorIndex, colorIndex } = command;\n await displayManager.selectSpriteColor(\n spriteColorIndex,\n colorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectSpriteColors\":\n {\n const { spriteColorPairs } = command;\n await displayManager.selectSpriteColors(\n spriteColorPairs,\n sendImmediately\n );\n }\n break;\n case \"setSpriteScaleX\":\n {\n const { spriteScaleX } = command;\n await displayManager.setSpriteScaleX(spriteScaleX, sendImmediately);\n }\n break;\n case \"setSpriteScaleY\":\n {\n const { spriteScaleY } = command;\n await displayManager.setSpriteScaleY(spriteScaleY, sendImmediately);\n }\n break;\n case \"setSpriteScale\":\n {\n const { spriteScale } = command;\n await displayManager.setSpriteScale(spriteScale, sendImmediately);\n }\n break;\n\n case \"clearRect\":\n {\n const { x, y, width, height } = command;\n await displayManager.clearRect(x, y, width, height, sendImmediately);\n }\n break;\n case \"drawRect\":\n {\n const { offsetX, offsetY, width, height } = command;\n await displayManager.drawRect(\n offsetX,\n offsetY,\n width,\n height,\n sendImmediately\n );\n }\n break;\n case \"drawRoundRect\":\n {\n const { offsetX, offsetY, width, height, borderRadius } = command;\n await displayManager.drawRoundRect(\n offsetX,\n offsetY,\n width,\n height,\n borderRadius,\n sendImmediately\n );\n }\n break;\n case \"drawCircle\":\n {\n const { offsetX, offsetY, radius } = command;\n await displayManager.drawCircle(\n offsetX,\n offsetY,\n radius,\n sendImmediately\n );\n }\n break;\n case \"drawEllipse\":\n {\n const { offsetX, offsetY, radiusX, radiusY } = command;\n await displayManager.drawEllipse(\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n sendImmediately\n );\n }\n break;\n case \"drawPolygon\":\n {\n const { points } = command;\n await displayManager.drawPolygon(points, sendImmediately);\n }\n break;\n case \"drawRegularPolygon\":\n {\n const { offsetX, offsetY, radius, numberOfSides } = command;\n await displayManager.drawRegularPolygon(\n offsetX,\n offsetY,\n radius,\n numberOfSides,\n sendImmediately\n );\n }\n break;\n case \"drawWireframe\":\n {\n const { wireframe } = command;\n await displayManager.drawWireframe(wireframe, sendImmediately);\n }\n break;\n case \"drawSegment\":\n {\n const { startX, startY, endX, endY } = command;\n await displayManager.drawSegment(\n startX,\n startY,\n endX,\n endY,\n sendImmediately\n );\n }\n break;\n case \"drawSegments\":\n {\n const { points } = command;\n await displayManager.drawSegments(\n points.map(({ x, y }) => ({ x: x, y: y })),\n sendImmediately\n );\n }\n break;\n case \"drawArc\":\n {\n let { offsetX, offsetY, radius, startAngle, angleOffset, isRadians } =\n command;\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n\n await displayManager.drawArc(\n offsetX,\n offsetY,\n radius,\n startAngle,\n angleOffset,\n true,\n sendImmediately\n );\n }\n break;\n case \"drawArcEllipse\":\n {\n let {\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n isRadians,\n } = command;\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n\n await displayManager.drawArcEllipse(\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n true,\n sendImmediately\n );\n }\n break;\n case \"drawBitmap\":\n {\n const { offsetX, offsetY, bitmap } = command;\n await displayManager.drawBitmap(\n offsetX,\n offsetY,\n bitmap,\n sendImmediately\n );\n }\n break;\n case \"drawSprite\":\n {\n const { offsetX, offsetY, spriteIndex } = command;\n const spriteName =\n displayManager.selectedSpriteSheet?.sprites[spriteIndex].name!;\n await displayManager.drawSprite(\n offsetX,\n offsetY,\n spriteName,\n sendImmediately\n );\n }\n break;\n case \"selectSpriteSheet\":\n {\n const { spriteSheetIndex } = command;\n const spriteSheetName = Object.entries(\n displayManager.spriteSheetIndices\n ).find((entry) => entry[1] == spriteSheetIndex)?.[0];\n await displayManager.selectSpriteSheet(\n spriteSheetName!,\n sendImmediately\n );\n }\n break;\n case \"resetSpriteColors\":\n await displayManager.resetSpriteColors(sendImmediately);\n break;\n\n case \"drawQuadraticBezierCurve\":\n {\n const { controlPoints } = command;\n await displayManager.drawQuadraticBezierCurve(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawQuadraticBezierCurves\":\n {\n const { controlPoints } = command;\n await displayManager.drawQuadraticBezierCurves(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawCubicBezierCurve\":\n {\n const { controlPoints } = command;\n await displayManager.drawCubicBezierCurve(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawCubicBezierCurves\":\n {\n const { controlPoints } = command;\n await displayManager.drawCubicBezierCurves(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawClosedPath\":\n {\n const { curves } = command;\n await displayManager.drawClosedPath(curves, sendImmediately);\n }\n break;\n case \"drawPath\":\n {\n const { curves } = command;\n await displayManager.drawPath(curves, sendImmediately);\n }\n break;\n case \"startSprite\":\n {\n const { offsetX, offsetY, width, height } = command;\n await displayManager.startSprite(\n offsetX,\n offsetY,\n width,\n height,\n sendImmediately\n );\n }\n break;\n case \"endSprite\":\n await displayManager.endSprite(sendImmediately);\n break;\n }\n}\n\nexport async function runDisplayContextCommands(\n displayManager: DisplayManagerInterface,\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n) {\n _console.log(\"runDisplayContextCommands\", commands);\n commands\n .filter((command) => !command.hide)\n .forEach((command) => {\n runDisplayContextCommand(displayManager, command, false);\n });\n if (sendImmediately) {\n displayManager.flushContextCommands();\n }\n}\n\nexport function assertLoadedSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheetName: string\n) {\n _console.assertWithError(\n displayManager.spriteSheets[spriteSheetName],\n `spriteSheet \"${spriteSheetName}\" not loaded`\n );\n}\nexport function assertSelectedSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheetName: string\n) {\n displayManager.assertLoadedSpriteSheet(spriteSheetName);\n _console.assertWithError(\n displayManager.selectedSpriteSheetName == spriteSheetName,\n `spriteSheet \"${spriteSheetName}\" not selected`\n );\n}\nexport function assertAnySelectedSpriteSheet(\n displayManager: DisplayManagerInterface\n) {\n _console.assertWithError(\n displayManager.selectedSpriteSheet,\n \"no spriteSheet selected\"\n );\n}\nexport function getSprite(\n displayManager: DisplayManagerInterface,\n spriteName: string\n): DisplaySprite | undefined {\n displayManager.assertAnySelectedSpriteSheet();\n return displayManager.selectedSpriteSheet!.sprites.find(\n (sprite) => sprite.name == spriteName\n );\n}\nexport function assertSprite(\n displayManager: DisplayManagerInterface,\n spriteName: string\n) {\n displayManager.assertAnySelectedSpriteSheet();\n const sprite = displayManager.getSprite(spriteName);\n _console.assertWithError(sprite, `no sprite found with name \"${spriteName}\"`);\n}\nexport function getSpriteSheetPalette(\n displayManager: DisplayManagerInterface,\n paletteName: string\n): DisplaySpriteSheetPalette | undefined {\n return displayManager.selectedSpriteSheet?.palettes?.find(\n (palette) => palette.name == paletteName\n );\n}\nexport function getSpriteSheetPaletteSwap(\n displayManager: DisplayManagerInterface,\n paletteSwapName: string\n): DisplaySpriteSheetPaletteSwap | undefined {\n return displayManager.selectedSpriteSheet?.paletteSwaps?.find(\n (paletteSwap) => paletteSwap.name == paletteSwapName\n );\n}\nexport function getSpritePaletteSwap(\n displayManager: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string\n): DisplaySpritePaletteSwap | undefined {\n return displayManager\n .getSprite(spriteName)\n ?.paletteSwaps?.find((paletteSwap) => paletteSwap.name == paletteSwapName);\n}\n\nexport function assertSpriteSheetPalette(\n displayManagerInterface: DisplayManagerInterface,\n paletteName: string\n) {\n const spriteSheetPalette =\n displayManagerInterface.getSpriteSheetPalette(paletteName);\n _console.assertWithError(\n spriteSheetPalette,\n `no spriteSheetPalette found with name \"${paletteName}\"`\n );\n}\nexport function assertSpriteSheetPaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n paletteSwapName: string\n) {\n const spriteSheetPaletteSwap =\n displayManagerInterface.getSpriteSheetPaletteSwap(paletteSwapName);\n _console.assertWithError(\n spriteSheetPaletteSwap,\n `no paletteSwapName found with name \"${paletteSwapName}\"`\n );\n}\nexport function assertSpritePaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string\n) {\n const spritePaletteSwap = displayManagerInterface.getSpritePaletteSwap(\n spriteName,\n paletteSwapName\n );\n _console.assertWithError(\n spritePaletteSwap,\n `no spritePaletteSwap found for sprite \"${spriteName}\" name \"${paletteSwapName}\"`\n );\n}\nexport async function selectSpriteSheetPalette(\n displayManagerInterface: DisplayManagerInterface,\n paletteName: string,\n offset?: number,\n indicesOnly?: boolean,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n\n displayManagerInterface.assertAnySelectedSpriteSheet();\n displayManagerInterface.assertSpriteSheetPalette(paletteName);\n const palette = displayManagerInterface.getSpriteSheetPalette(paletteName)!;\n\n _console.assertWithError(\n palette.numberOfColors + offset <= displayManagerInterface.numberOfColors,\n `invalid offset ${offset} and palette.numberOfColors ${palette.numberOfColors} (max ${displayManagerInterface.numberOfColors})`\n );\n\n //_console.log({ indicesOnly });\n for (let index = 0; index < palette.numberOfColors; index++) {\n if (!indicesOnly) {\n const color = palette.colors[index];\n let opacity = palette.opacities?.[index];\n if (opacity == undefined) {\n opacity = 1;\n }\n //_console.log({ index, offset, color });\n displayManagerInterface.setColor(index + offset, color, false);\n displayManagerInterface.setColorOpacity(index + offset, opacity, false);\n }\n displayManagerInterface.selectSpriteColor(index, index + offset);\n }\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\nexport async function selectSpriteSheetPaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n displayManagerInterface.assertAnySelectedSpriteSheet();\n displayManagerInterface.assertSpriteSheetPaletteSwap(paletteSwapName);\n\n const paletteSwap =\n displayManagerInterface.getSpriteSheetPaletteSwap(paletteSwapName)!;\n\n const spriteColorPairs: DisplaySpriteColorPair[] = [];\n for (\n let spriteColorIndex = 0;\n spriteColorIndex < paletteSwap.numberOfColors;\n spriteColorIndex++\n ) {\n const colorIndex = paletteSwap.spriteColorIndices[spriteColorIndex];\n spriteColorPairs.push({\n spriteColorIndex: spriteColorIndex + offset,\n colorIndex,\n });\n }\n displayManagerInterface.selectSpriteColors(spriteColorPairs, false);\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\nexport async function selectSpritePaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n displayManagerInterface.assertAnySelectedSpriteSheet();\n\n const paletteSwap = displayManagerInterface.getSpritePaletteSwap(\n spriteName,\n paletteSwapName\n )!;\n\n const spriteColorPairs: DisplaySpriteColorPair[] = [];\n for (\n let spriteColorIndex = 0;\n spriteColorIndex < paletteSwap.numberOfColors;\n spriteColorIndex++\n ) {\n const colorIndex = paletteSwap.spriteColorIndices[spriteColorIndex];\n spriteColorPairs.push({\n spriteColorIndex: spriteColorIndex + offset,\n colorIndex,\n });\n }\n displayManagerInterface.selectSpriteColors(spriteColorPairs, false);\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\n\nexport async function drawSpriteFromSpriteSheet(\n displayManagerInterface: DisplayManagerInterface,\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n) {\n const reducedSpriteSheet = reduceSpriteSheet(spriteSheet, [spriteName]);\n await displayManagerInterface.uploadSpriteSheet(reducedSpriteSheet);\n await displayManagerInterface.selectSpriteSheet(spriteSheet.name);\n await displayManagerInterface.drawSprite(\n offsetX,\n offsetY,\n spriteName,\n sendImmediately\n );\n if (paletteName != undefined) {\n await displayManagerInterface.selectSpriteSheetPalette(paletteName);\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport {\n clamp,\n degToRad,\n normalizeRadians,\n Vector2,\n} from \"./utils/MathUtils.ts\";\nimport { rgbToHex, stringToRGB } from \"./utils/ColorUtils.ts\";\nimport DisplayContextStateHelper from \"./utils/DisplayContextStateHelper.ts\";\nimport {\n assertValidColor,\n assertValidDisplayBrightness,\n assertValidSegmentCap,\n DisplayScaleDirection,\n DisplayBitmapScaleDirectionToCommandType,\n DisplayColorRGB,\n DisplayCropDirection,\n DisplayCropDirections,\n DisplayCropDirectionToCommandType,\n DisplayCropDirectionToStateKey,\n DisplayRotationCropDirectionToCommandType,\n DisplayRotationCropDirectionToStateKey,\n maxDisplayScale,\n roundScale,\n DisplaySpriteScaleDirectionToCommandType,\n minDisplayScale,\n assertValidAlignment,\n DisplayAlignmentDirectionToCommandType,\n DisplayAlignmentDirectionToStateKey,\n assertValidDirection,\n assertValidAlignmentDirection,\n assertValidWireframe,\n trimWireframe,\n assertValidNumberOfControlPoints,\n assertValidPathNumberOfControlPoints,\n assertValidPath,\n isWireframePolygon,\n} from \"./utils/DisplayUtils.ts\";\nimport {\n assertValidBitmapPixels,\n drawBitmapHeaderLength,\n getBitmapNumberOfBytes,\n imageToBitmap,\n quantizeImage,\n resizeAndQuantizeImage,\n} from \"./utils/DisplayBitmapUtils.ts\";\nimport {\n DefaultDisplayContextState,\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayContextState,\n DisplayContextStateKey,\n DisplayDirection,\n DisplaySegmentCap,\n PartialDisplayContextState,\n} from \"./utils/DisplayContextState.ts\";\nimport {\n DisplayContextCommand,\n DisplayContextCommandType,\n DisplayContextCommandTypes,\n serializeContextCommand,\n} from \"./utils/DisplayContextCommand.ts\";\nimport {\n assertAnySelectedSpriteSheet,\n assertLoadedSpriteSheet,\n assertSelectedSpriteSheet,\n assertSprite,\n assertSpritePaletteSwap,\n assertSpriteSheetPalette,\n assertSpriteSheetPaletteSwap,\n DisplayManagerInterface,\n drawSpriteFromSpriteSheet,\n getSprite,\n getSpritePaletteSwap,\n getSpriteSheetPalette,\n getSpriteSheetPaletteSwap,\n runDisplayContextCommand,\n runDisplayContextCommands,\n selectSpritePaletteSwap,\n selectSpriteSheetPalette,\n selectSpriteSheetPaletteSwap,\n} from \"./utils/DisplayManagerInterface.ts\";\nimport { SendFileCallback } from \"./FileTransferManager.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport {\n DisplaySprite,\n DisplaySpritePaletteSwap,\n DisplaySpriteSheetPalette,\n DisplaySpriteSheetPaletteSwap,\n serializeSpriteSheet,\n DisplaySpriteSheet,\n DisplaySpriteLines,\n stringToSpriteLines,\n DisplaySpriteSerializedSubLine,\n DisplaySpriteSerializedLine,\n DisplaySpriteSerializedLines,\n stringToSpriteLinesMetrics,\n} from \"./utils/DisplaySpriteSheetUtils.ts\";\nimport { wait } from \"./utils/Timer.ts\";\n\nconst _console = createConsole(\"DisplayManager\", { log: false });\n\nexport const DefaultNumberOfDisplayColors = 16;\n\nexport const DisplayCommands = [\"sleep\", \"wake\"] as const;\nexport type DisplayCommand = (typeof DisplayCommands)[number];\n\nexport const DisplayStatuses = [\"awake\", \"asleep\"] as const;\nexport type DisplayStatus = (typeof DisplayStatuses)[number];\n\nexport const DisplayInformationTypes = [\n \"type\",\n \"width\",\n \"height\",\n \"pixelDepth\",\n] as const;\nexport type DisplayInformationType = (typeof DisplayInformationTypes)[number];\n\nexport const DisplayTypes = [\n \"none\",\n \"generic\",\n \"monocularLeft\",\n \"monocularRight\",\n \"binocular\",\n] as const;\nexport type DisplayType = (typeof DisplayTypes)[number];\n\nexport const DisplayPixelDepths = [\"1\", \"2\", \"4\"] as const;\nexport type DisplayPixelDepth = (typeof DisplayPixelDepths)[number];\n\nexport const DisplayBrightnesses = [\n \"veryLow\",\n \"low\",\n \"medium\",\n \"high\",\n \"veryHigh\",\n] as const;\nexport type DisplayBrightness = (typeof DisplayBrightnesses)[number];\n\nexport const DisplayMessageTypes = [\n \"isDisplayAvailable\",\n \"displayStatus\",\n \"displayInformation\",\n \"displayCommand\",\n \"getDisplayBrightness\",\n \"setDisplayBrightness\",\n \"displayContextCommands\",\n \"displayReady\",\n \"getSpriteSheetName\",\n \"setSpriteSheetName\",\n \"spriteSheetIndex\",\n] as const;\nexport type DisplayMessageType = (typeof DisplayMessageTypes)[number];\n\nexport type DisplaySize = {\n width: number;\n height: number;\n};\nexport type DisplayInformation = {\n type: DisplayType;\n width: number;\n height: number;\n pixelDepth: DisplayPixelDepth;\n};\n\nexport type DisplayBitmapColorPair = {\n bitmapColorIndex: number;\n colorIndex: number;\n};\n\nexport type DisplaySpriteColorPair = {\n spriteColorIndex: number;\n colorIndex: number;\n};\n\nexport type DisplayWireframeEdge = {\n startIndex: number;\n endIndex: number;\n};\nexport type DisplaySegment = {\n start: Vector2;\n end: Vector2;\n};\nexport type DisplayWireframe = {\n points: Vector2[];\n edges: DisplayWireframeEdge[];\n};\n\nexport const DisplayBezierCurveTypes = [\n \"segment\",\n \"quadratic\",\n \"cubic\",\n] as const;\nexport type DisplayBezierCurveType = (typeof DisplayBezierCurveTypes)[number];\nexport type DisplayBezierCurve = {\n type: DisplayBezierCurveType;\n controlPoints: Vector2[];\n};\n\nexport const displayCurveTypeBitWidth = 2;\nexport const displayCurveTypesPerByte = 8 / displayCurveTypeBitWidth;\n\nexport const DisplayPointDataTypes = [\"int8\", \"int16\", \"float\"] as const;\nexport type DisplayPointDataType = (typeof DisplayPointDataTypes)[number];\nexport const displayPointDataTypeToSize: Record<DisplayPointDataType, number> =\n {\n int8: 1 * 2,\n int16: 2 * 2,\n float: 4 * 2,\n };\nexport const displayPointDataTypeToRange: Record<\n DisplayPointDataType,\n { min: number; max: number }\n> = {\n int8: { min: -(2 ** 7), max: 2 ** 7 - 1 },\n int16: { min: -(2 ** 15), max: 2 ** 15 - 1 },\n float: { min: -Infinity, max: Infinity },\n};\n\nexport const DisplayInformationValues = {\n type: DisplayTypes,\n pixelDepth: DisplayPixelDepths,\n};\n\nexport const RequiredDisplayMessageTypes: DisplayMessageType[] = [\n \"isDisplayAvailable\",\n \"displayInformation\",\n \"displayStatus\",\n \"getDisplayBrightness\",\n] as const;\n\nexport const DisplayEventTypes = [\n ...DisplayMessageTypes,\n \"displayContextState\",\n \"displayColor\",\n \"displayColorOpacity\",\n \"displayOpacity\",\n \"displaySpriteSheetUploadStart\",\n \"displaySpriteSheetUploadProgress\",\n \"displaySpriteSheetUploadComplete\",\n] as const;\nexport type DisplayEventType = (typeof DisplayEventTypes)[number];\n\nexport interface DisplayEventMessages {\n isDisplayAvailable: { isDisplayAvailable: boolean };\n displayStatus: {\n displayStatus: DisplayStatus;\n previousDisplayStatus: DisplayStatus;\n };\n displayInformation: {\n displayInformation: DisplayInformation;\n };\n getDisplayBrightness: {\n displayBrightness: DisplayBrightness;\n };\n displayContextState: {\n displayContextState: DisplayContextState;\n differences: DisplayContextStateKey[];\n };\n displayColor: {\n colorIndex: number;\n colorRGB: DisplayColorRGB;\n colorHex: string;\n };\n displayColorOpacity: {\n opacity: number;\n colorIndex: number;\n };\n displayOpacity: {\n opacity: number;\n };\n displayReady: {};\n getSpriteSheetName: {\n spriteSheetName: string;\n };\n\n displaySpriteSheetUploadStart: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n };\n displaySpriteSheetUploadProgress: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n progress: number;\n };\n displaySpriteSheetUploadComplete: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n };\n displayContextCommands: {};\n}\n\nexport type DisplayEventDispatcher = EventDispatcher<\n Device,\n DisplayEventType,\n DisplayEventMessages\n>;\nexport type SendDisplayMessageCallback =\n SendMessageCallback<DisplayMessageType>;\n\nexport const MinSpriteSheetNameLength = 1;\nexport const MaxSpriteSheetNameLength = 30;\n\nexport type DisplayBitmap = {\n width: number;\n height: number;\n numberOfColors: number;\n pixels: number[];\n};\n\nclass DisplayManager implements DisplayManagerInterface {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendDisplayMessageCallback;\n\n eventDispatcher!: DisplayEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required display information\");\n const messages = RequiredDisplayMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // IS DISPLAY AVAILABLE\n #isAvailable = false;\n get isAvailable() {\n return this.#isAvailable;\n }\n\n #assertDisplayIsAvailable() {\n _console.assertWithError(this.#isAvailable, \"display is not available\");\n }\n\n #parseIsDisplayAvailable(dataView: DataView) {\n const newIsDisplayAvailable = dataView.getUint8(0) == 1;\n this.#isAvailable = newIsDisplayAvailable;\n _console.log({ isDisplayAvailable: this.#isAvailable });\n this.#dispatchEvent(\"isDisplayAvailable\", {\n isDisplayAvailable: this.#isAvailable,\n });\n }\n\n // DISPLAY CONTEXT STATE\n #contextStateHelper = new DisplayContextStateHelper();\n get contextState() {\n return this.#contextStateHelper.state;\n }\n #onContextStateUpdate(differences: DisplayContextStateKey[]) {\n this.#dispatchEvent(\"displayContextState\", {\n displayContextState: structuredClone(this.contextState),\n differences,\n });\n }\n async setContextState(\n newState: PartialDisplayContextState,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.diff(newState);\n if (differences.length == 0) {\n return;\n }\n differences.forEach((difference) => {\n switch (difference) {\n case \"backgroundColorIndex\":\n this.selectBackgroundColor(newState.backgroundColorIndex!);\n break;\n case \"fillBackground\":\n this.setFillBackground(newState.fillBackground!);\n break;\n case \"ignoreFill\":\n this.setIgnoreFill(newState.ignoreFill!);\n break;\n case \"ignoreLine\":\n this.setIgnoreLine(newState.ignoreLine!);\n break;\n case \"fillColorIndex\":\n this.selectFillColor(newState.fillColorIndex!);\n break;\n case \"lineColorIndex\":\n this.selectLineColor(newState.lineColorIndex!);\n break;\n case \"lineWidth\":\n this.setLineWidth(newState.lineWidth!);\n break;\n case \"horizontalAlignment\":\n this.setHorizontalAlignment(newState.horizontalAlignment!);\n break;\n case \"verticalAlignment\":\n this.setVerticalAlignment(newState.verticalAlignment!);\n break;\n case \"rotation\":\n this.setRotation(newState.rotation!, true);\n break;\n case \"segmentStartCap\":\n this.setSegmentStartCap(newState.segmentStartCap!);\n break;\n case \"segmentEndCap\":\n this.setSegmentEndCap(newState.segmentEndCap!);\n break;\n case \"segmentStartRadius\":\n this.setSegmentStartRadius(newState.segmentStartRadius!);\n break;\n case \"segmentEndRadius\":\n this.setSegmentEndRadius(newState.segmentEndRadius!);\n break;\n case \"cropTop\":\n this.setCropTop(newState.cropTop!);\n break;\n case \"cropRight\":\n this.setCropRight(newState.cropRight!);\n break;\n case \"cropBottom\":\n this.setCropBottom(newState.cropBottom!);\n break;\n case \"cropLeft\":\n this.setCropLeft(newState.cropLeft!);\n break;\n case \"rotationCropTop\":\n this.setRotationCropTop(newState.rotationCropTop!);\n break;\n case \"rotationCropRight\":\n this.setRotationCropRight(newState.rotationCropRight!);\n break;\n case \"rotationCropBottom\":\n this.setRotationCropBottom(newState.rotationCropBottom!);\n break;\n case \"rotationCropLeft\":\n this.setRotationCropLeft(newState.rotationCropLeft!);\n break;\n case \"bitmapColorIndices\":\n const bitmapColors: DisplayBitmapColorPair[] = [];\n newState.bitmapColorIndices!.forEach(\n (colorIndex, bitmapColorIndex) => {\n bitmapColors.push({ bitmapColorIndex, colorIndex });\n }\n );\n this.selectBitmapColors(bitmapColors);\n break;\n case \"bitmapScaleX\":\n this.setBitmapScaleX(newState.bitmapScaleX!);\n break;\n case \"bitmapScaleY\":\n this.setBitmapScaleY(newState.bitmapScaleY!);\n break;\n case \"spriteColorIndices\":\n const spriteColors: DisplaySpriteColorPair[] = [];\n newState.spriteColorIndices!.forEach(\n (colorIndex, spriteColorIndex) => {\n spriteColors.push({ spriteColorIndex, colorIndex });\n }\n );\n this.selectSpriteColors(spriteColors);\n break;\n case \"spriteScaleX\":\n this.setSpriteScaleX(newState.spriteScaleX!);\n break;\n case \"spriteScaleY\":\n this.setSpriteScaleY(newState.spriteScaleY!);\n break;\n case \"spritesLineHeight\":\n this.setSpritesLineHeight(newState.spritesLineHeight!);\n break;\n case \"spritesDirection\":\n this.setSpritesDirection(newState.spritesDirection!);\n break;\n case \"spritesLineDirection\":\n this.setSpritesLineDirection(newState.spritesLineDirection!);\n break;\n case \"spritesSpacing\":\n this.setSpritesSpacing(newState.spritesSpacing!);\n break;\n case \"spritesLineSpacing\":\n this.setSpritesLineSpacing(newState.spritesLineSpacing!);\n break;\n case \"spritesAlignment\":\n this.setSpritesAlignment(newState.spritesAlignment!);\n break;\n case \"spritesLineAlignment\":\n this.setSpritesLineAlignment(newState.spritesLineAlignment!);\n break;\n }\n });\n if (sendImmediately) {\n await this.#sendContextCommands();\n }\n }\n\n // DISPLAY STATUS\n #displayStatus!: DisplayStatus;\n get displayStatus() {\n return this.#displayStatus;\n }\n get isDisplayAwake() {\n return this.#displayStatus == \"awake\";\n }\n #parseDisplayStatus(dataView: DataView) {\n const displayStatusIndex = dataView.getUint8(0);\n const newDisplayStatus = DisplayStatuses[displayStatusIndex];\n this.#updateDisplayStatus(newDisplayStatus);\n }\n #updateDisplayStatus(newDisplayStatus: DisplayStatus) {\n _console.assertEnumWithError(newDisplayStatus, DisplayStatuses);\n if (newDisplayStatus == this.#displayStatus) {\n _console.log(`redundant displayStatus ${newDisplayStatus}`);\n return;\n }\n const previousDisplayStatus = this.#displayStatus;\n this.#displayStatus = newDisplayStatus;\n _console.log(`updated displayStatus to \"${this.displayStatus}\"`);\n this.#dispatchEvent(\"displayStatus\", {\n displayStatus: this.displayStatus,\n previousDisplayStatus,\n });\n }\n\n // DISPLAY COMMAND\n async #sendDisplayCommand(\n command: DisplayCommand,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(command, DisplayCommands);\n _console.log(`sending display command \"${command}\"`);\n\n const promise = this.waitForEvent(\"displayStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = DisplayCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"displayCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsAwake() {\n _console.assertWithError(\n this.#displayStatus == \"awake\",\n `display is not awake - currently ${this.#displayStatus}`\n );\n }\n #assertIsNotAwake() {\n _console.assertWithError(\n this.#displayStatus != \"awake\",\n `display is awake`\n );\n }\n\n async wake() {\n this.#assertIsNotAwake();\n await this.#sendDisplayCommand(\"wake\");\n }\n async sleep() {\n this.#assertIsAwake();\n await this.#sendDisplayCommand(\"sleep\");\n }\n async toggle() {\n switch (this.displayStatus) {\n case \"asleep\":\n this.wake();\n break;\n case \"awake\":\n this.sleep();\n break;\n }\n }\n\n get numberOfColors() {\n return 2 ** Number(this.pixelDepth!);\n }\n\n // INFORMATION\n #displayInformation?: DisplayInformation;\n get displayInformation() {\n return this.#displayInformation!;\n }\n\n get pixelDepth() {\n return this.#displayInformation?.pixelDepth!;\n }\n get width() {\n return this.#displayInformation?.width!;\n }\n get height() {\n return this.#displayInformation?.width!;\n }\n get size() {\n return {\n width: this.width!,\n height: this.height!,\n };\n }\n get type() {\n return this.#displayInformation?.type!;\n }\n\n #parseDisplayInformation(dataView: DataView) {\n // @ts-expect-error\n const parsedDisplayInformation: DisplayInformation = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const displayInformationTypeIndex = dataView.getUint8(byteOffset++);\n const displayInformationType =\n DisplayInformationTypes[displayInformationTypeIndex];\n _console.assertWithError(\n displayInformationType,\n `invalid displayInformationTypeIndex ${displayInformationType}`\n );\n _console.log({ displayInformationType });\n\n switch (displayInformationType) {\n case \"width\":\n case \"height\":\n {\n const value = dataView.getUint16(byteOffset, true);\n parsedDisplayInformation[displayInformationType] = value;\n byteOffset += 2;\n }\n break;\n case \"pixelDepth\":\n case \"type\":\n {\n const values = DisplayInformationValues[displayInformationType];\n let rawValue = dataView.getUint8(byteOffset++);\n const value = values[rawValue];\n _console.assertEnumWithError(value, values);\n // @ts-expect-error\n parsedDisplayInformation[displayInformationType] = value;\n }\n break;\n }\n }\n\n _console.log({ parsedDisplayInformation });\n const missingDisplayInformationType = DisplayInformationTypes.find(\n (type) => !(type in parsedDisplayInformation)\n );\n _console.assertWithError(\n !missingDisplayInformationType,\n `missingDisplayInformationType ${missingDisplayInformationType}`\n );\n this.#displayInformation = parsedDisplayInformation;\n this.#colors = new Array(this.numberOfColors).fill(\"#000000\");\n this.#opacities = new Array(this.numberOfColors).fill(1);\n this.contextState.bitmapColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.contextState.spriteColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.#dispatchEvent(\"displayInformation\", {\n displayInformation: this.#displayInformation,\n });\n }\n\n // DISPLAY BRIGHTNESS\n #brightness!: DisplayBrightness;\n get brightness() {\n return this.#brightness;\n }\n\n #parseDisplayBrightness(dataView: DataView) {\n const newDisplayBrightnessEnum = dataView.getUint8(0);\n const newDisplayBrightness = DisplayBrightnesses[newDisplayBrightnessEnum];\n assertValidDisplayBrightness(newDisplayBrightness);\n\n this.#brightness = newDisplayBrightness;\n _console.log({ displayBrightness: this.#brightness });\n this.#dispatchEvent(\"getDisplayBrightness\", {\n displayBrightness: this.#brightness,\n });\n }\n\n async setBrightness(\n newDisplayBrightness: DisplayBrightness,\n sendImmediately?: boolean\n ) {\n this.#assertDisplayIsAvailable();\n assertValidDisplayBrightness(newDisplayBrightness);\n if (this.brightness == newDisplayBrightness) {\n _console.log(`redundant displayBrightness ${newDisplayBrightness}`);\n return;\n }\n const newDisplayBrightnessEnum =\n DisplayBrightnesses.indexOf(newDisplayBrightness);\n const newDisplayBrightnessData = UInt8ByteBuffer(newDisplayBrightnessEnum);\n\n const promise = this.waitForEvent(\"getDisplayBrightness\");\n this.sendMessage(\n [{ type: \"setDisplayBrightness\", data: newDisplayBrightnessData }],\n sendImmediately\n );\n await promise;\n }\n\n // DISPLAY CONTEXT\n #assertValidDisplayContextCommandType(\n displayContextCommand: DisplayContextCommandType\n ) {\n _console.assertEnumWithError(\n displayContextCommand,\n DisplayContextCommandTypes\n );\n }\n\n get #maxCommandDataLength() {\n return this.mtu - 7;\n }\n #contextCommandBuffers: ArrayBuffer[] = [];\n async #sendContextCommand(\n contextCommandType: DisplayContextCommandType,\n arrayBuffer?: ArrayBufferLike,\n sendImmediately?: boolean\n ) {\n this.#assertValidDisplayContextCommandType(contextCommandType);\n _console.log(\n \"sendContextCommand\",\n { displayContextCommand: contextCommandType, sendImmediately },\n arrayBuffer\n );\n const displayContextCommandEnum =\n DisplayContextCommandTypes.indexOf(contextCommandType);\n const _arrayBuffer = concatenateArrayBuffers(\n UInt8ByteBuffer(displayContextCommandEnum),\n arrayBuffer\n );\n const newLength = this.#contextCommandBuffers.reduce(\n (sum, buffer) => sum + buffer.byteLength,\n _arrayBuffer.byteLength\n );\n if (newLength > this.#maxCommandDataLength) {\n _console.log(\"displayContextCommandBuffers too full - sending now\");\n await this.#sendContextCommands();\n }\n this.#contextCommandBuffers.push(_arrayBuffer);\n if (sendImmediately) {\n await this.#sendContextCommands();\n }\n }\n async #sendContextCommands() {\n if (this.#contextCommandBuffers.length == 0) {\n return;\n }\n const data = concatenateArrayBuffers(this.#contextCommandBuffers);\n _console.log(\n `sending displayContextCommands`,\n this.#contextCommandBuffers.slice(),\n data\n );\n this.#contextCommandBuffers.length = 0;\n await this.sendMessage([{ type: \"displayContextCommands\", data }], true);\n this.#dispatchEvent(\"displayContextCommands\", {});\n }\n async flushContextCommands() {\n await this.#sendContextCommands();\n }\n async show(sendImmediately = true) {\n _console.log(\"showDisplay\");\n this.#isReady = false;\n this.#lastShowRequestTime = Date.now();\n await this.#sendContextCommand(\"show\", undefined, sendImmediately);\n }\n async clear(sendImmediately = true) {\n _console.log(\"clearDisplay\");\n this.#isReady = false;\n this.#lastShowRequestTime = Date.now();\n await this.#sendContextCommand(\"clear\", undefined, sendImmediately);\n }\n\n assertValidColorIndex(colorIndex: number) {\n _console.assertRangeWithError(\n \"colorIndex\",\n colorIndex,\n 0,\n this.numberOfColors\n );\n }\n #colors: string[] = [];\n get colors() {\n return this.#colors;\n }\n async setColor(\n colorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n let colorRGB: DisplayColorRGB;\n if (typeof color == \"string\") {\n colorRGB = stringToRGB(color);\n } else {\n colorRGB = color;\n }\n const colorHex = rgbToHex(colorRGB);\n if (this.colors[colorIndex] == colorHex) {\n _console.log(`redundant color #${colorIndex} ${colorHex}`);\n return;\n }\n\n //_console.log(`setting color #${colorIndex}`, colorRGB);\n this.assertValidColorIndex(colorIndex);\n assertValidColor(colorRGB);\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, colorRGB.r);\n dataView.setUint8(2, colorRGB.g);\n dataView.setUint8(3, colorRGB.b);\n await this.#sendContextCommand(\n \"setColor\",\n dataView.buffer,\n sendImmediately\n );\n this.colors[colorIndex] = colorHex;\n this.#dispatchEvent(\"displayColor\", {\n colorIndex,\n colorRGB,\n colorHex,\n });\n }\n #opacities: number[] = [];\n get opacities() {\n return this.#opacities;\n }\n async setColorOpacity(\n colorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"setColorOpacity\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n colorIndex,\n opacity,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#opacities[colorIndex] = opacity;\n this.#dispatchEvent(\"displayColorOpacity\", { colorIndex, opacity });\n }\n async setOpacity(opacity: number, sendImmediately?: boolean) {\n const commandType: DisplayContextCommandType = \"setOpacity\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n opacity,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#opacities.fill(opacity);\n this.#dispatchEvent(\"displayOpacity\", { opacity });\n }\n\n #contextStack: DisplayContextState[] = [];\n #saveContext(sendImmediately?: boolean) {\n this.#contextStack.push(structuredClone(this.contextState));\n }\n #restoreContext(sendImmediately?: boolean) {\n const contextState = this.#contextStack.pop();\n if (!contextState) {\n _console.warn(\"#contextStack empty\");\n return;\n }\n this.setContextState(contextState, sendImmediately);\n }\n async saveContext(sendImmediately?: boolean) {\n if (true) {\n this.#saveContext(sendImmediately);\n } else {\n // const commandType: DisplayContextCommandType = \"saveContext\";\n // const dataView = serializeContextCommand(this, { type: commandType });\n // await this.#sendDisplayContextCommand(\n // commandType,\n // dataView?.buffer,\n // sendImmediately\n // );\n }\n }\n async restoreContext(sendImmediately?: boolean) {\n if (true) {\n this.#restoreContext(sendImmediately);\n } else {\n // const commandType: DisplayContextCommandType = \"restoreContext\";\n // const dataView = serializeContextCommand(this, { type: commandType });\n // await this.#sendDisplayContextCommand(\n // commandType,\n // dataView?.buffer,\n // sendImmediately\n // );\n }\n }\n\n async selectFillColor(fillColorIndex: number, sendImmediately?: boolean) {\n this.assertValidColorIndex(fillColorIndex);\n const differences = this.#contextStateHelper.update({\n fillColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectFillColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n fillColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async selectBackgroundColor(\n backgroundColorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(backgroundColorIndex);\n const differences = this.#contextStateHelper.update({\n backgroundColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBackgroundColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n backgroundColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async selectLineColor(lineColorIndex: number, sendImmediately?: boolean) {\n this.assertValidColorIndex(lineColorIndex);\n const differences = this.#contextStateHelper.update({\n lineColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectLineColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n lineColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setIgnoreFill(ignoreFill: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n ignoreFill,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setIgnoreFill\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n ignoreFill,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setIgnoreLine(ignoreLine: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n ignoreLine,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setIgnoreLine\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n ignoreLine,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setFillBackground(fillBackground: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n fillBackground,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setFillBackground\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n fillBackground,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n assertValidLineWidth(lineWidth: number) {\n _console.assertRangeWithError(\n \"lineWidth\",\n lineWidth,\n 0,\n Math.max(this.width, this.height)\n );\n }\n async setLineWidth(lineWidth: number, sendImmediately?: boolean) {\n this.assertValidLineWidth(lineWidth);\n const differences = this.#contextStateHelper.update({\n lineWidth,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setLineWidth\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n lineWidth,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setAlignment(\n alignmentDirection: DisplayAlignmentDirection,\n alignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n assertValidAlignmentDirection(alignmentDirection);\n const alignmentCommand =\n DisplayAlignmentDirectionToCommandType[alignmentDirection];\n const alignmentKey =\n DisplayAlignmentDirectionToStateKey[alignmentDirection];\n const differences = this.#contextStateHelper.update({\n [alignmentKey]: alignment,\n });\n _console.log({ alignmentKey, alignment, differences });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: alignmentCommand,\n [alignmentKey]: alignment,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n alignmentCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setHorizontalAlignment(\n horizontalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setAlignment(\"horizontal\", horizontalAlignment, sendImmediately);\n }\n async setVerticalAlignment(\n verticalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setAlignment(\"vertical\", verticalAlignment, sendImmediately);\n }\n async resetAlignment(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n verticalAlignment: DefaultDisplayContextState.verticalAlignment,\n horizontalAlignment: DefaultDisplayContextState.horizontalAlignment,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetAlignment\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setRotation(\n rotation: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation = normalizeRadians(rotation);\n isRadians = true;\n const differences = this.#contextStateHelper.update({\n rotation,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setRotation\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n rotation,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async clearRotation(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n rotation: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearRotation\";\n const dataView = serializeContextCommand(this, { type: commandType });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSegmentStartCap(\n segmentStartCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentStartCap);\n const differences = this.#contextStateHelper.update({\n segmentStartCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentStartCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentStartCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentEndCap(\n segmentEndCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentEndCap);\n const differences = this.#contextStateHelper.update({\n segmentEndCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentEndCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentEndCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentCap(\n segmentCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentCap);\n const differences = this.#contextStateHelper.update({\n segmentStartCap: segmentCap,\n segmentEndCap: segmentCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSegmentStartRadius(\n segmentStartRadius: number,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.update({\n segmentStartRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentStartRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentStartRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentEndRadius(\n segmentEndRadius: number,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.update({\n segmentEndRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentEndRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentEndRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentRadius(segmentRadius: number, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n segmentStartRadius: segmentRadius,\n segmentEndRadius: segmentRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(cropDirection, DisplayCropDirections);\n crop = Math.max(0, crop);\n const cropCommand = DisplayCropDirectionToCommandType[cropDirection];\n const cropKey = DisplayCropDirectionToStateKey[cropDirection];\n const differences = this.#contextStateHelper.update({\n [cropKey]: crop,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: cropCommand,\n [cropKey]: crop,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n cropCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setCropTop(cropTop: number, sendImmediately?: boolean) {\n await this.setCrop(\"top\", cropTop, sendImmediately);\n }\n async setCropRight(cropRight: number, sendImmediately?: boolean) {\n await this.setCrop(\"right\", cropRight, sendImmediately);\n }\n async setCropBottom(cropBottom: number, sendImmediately?: boolean) {\n await this.setCrop(\"bottom\", cropBottom, sendImmediately);\n }\n async setCropLeft(cropLeft: number, sendImmediately?: boolean) {\n await this.setCrop(\"left\", cropLeft, sendImmediately);\n }\n async clearCrop(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n cropTop: 0,\n cropRight: 0,\n cropBottom: 0,\n cropLeft: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearCrop\";\n const dataView = serializeContextCommand(this, { type: commandType });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setRotationCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(cropDirection, DisplayCropDirections);\n const cropCommand =\n DisplayRotationCropDirectionToCommandType[cropDirection];\n const cropKey = DisplayRotationCropDirectionToStateKey[cropDirection];\n const differences = this.#contextStateHelper.update({\n [cropKey]: crop,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: cropCommand,\n [cropKey]: crop,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n cropCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setRotationCropTop(rotationCropTop: number, sendImmediately?: boolean) {\n await this.setRotationCrop(\"top\", rotationCropTop, sendImmediately);\n }\n async setRotationCropRight(\n rotationCropRight: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"right\", rotationCropRight, sendImmediately);\n }\n async setRotationCropBottom(\n rotationCropBottom: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"bottom\", rotationCropBottom, sendImmediately);\n }\n async setRotationCropLeft(\n rotationCropLeft: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"left\", rotationCropLeft, sendImmediately);\n }\n async clearRotationCrop(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n rotationCropTop: 0,\n rotationCropRight: 0,\n rotationCropBottom: 0,\n rotationCropLeft: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearRotationCrop\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async selectBitmapColor(\n bitmapColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(bitmapColorIndex);\n this.assertValidColorIndex(colorIndex);\n const bitmapColorIndices = this.contextState.bitmapColorIndices.slice();\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n const differences = this.#contextStateHelper.update({\n bitmapColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBitmapColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n bitmapColorIndex,\n colorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n get bitmapColorIndices() {\n return this.contextState.bitmapColorIndices;\n }\n get bitmapColors() {\n return this.bitmapColorIndices.map((colorIndex) => this.colors[colorIndex]);\n }\n async selectBitmapColors(\n bitmapColorPairs: DisplayBitmapColorPair[],\n sendImmediately?: boolean\n ) {\n _console.assertRangeWithError(\n \"bitmapColors\",\n bitmapColorPairs.length,\n 1,\n this.numberOfColors\n );\n const bitmapColorIndices = this.contextState.bitmapColorIndices.slice();\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n this.assertValidColorIndex(bitmapColorIndex);\n this.assertValidColorIndex(colorIndex);\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n });\n\n const differences = this.#contextStateHelper.update({\n bitmapColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBitmapColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n bitmapColorPairs,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setBitmapColor(\n bitmapColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n return this.setColor(\n this.bitmapColorIndices[bitmapColorIndex],\n color,\n sendImmediately\n );\n }\n async setBitmapColorOpacity(\n bitmapColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n return this.setColorOpacity(\n this.bitmapColorIndices[bitmapColorIndex],\n opacity,\n sendImmediately\n );\n }\n async setBitmapScaleDirection(\n direction: DisplayScaleDirection,\n bitmapScale: number,\n sendImmediately?: boolean\n ) {\n bitmapScale = clamp(bitmapScale, minDisplayScale, maxDisplayScale);\n bitmapScale = roundScale(bitmapScale);\n const commandType = DisplayBitmapScaleDirectionToCommandType[direction];\n _console.log({ [commandType]: bitmapScale });\n const newState: PartialDisplayContextState = {};\n let command: DisplayContextCommand;\n switch (direction) {\n case \"all\":\n newState.bitmapScaleX = bitmapScale;\n newState.bitmapScaleY = bitmapScale;\n command = { type: \"setBitmapScale\", bitmapScale };\n break;\n case \"x\":\n newState.bitmapScaleX = bitmapScale;\n command = { type: \"setBitmapScaleX\", bitmapScaleX: bitmapScale };\n break;\n case \"y\":\n newState.bitmapScaleY = bitmapScale;\n command = { type: \"setBitmapScaleY\", bitmapScaleY: bitmapScale };\n break;\n }\n const differences = this.#contextStateHelper.update(newState);\n if (differences.length == 0) {\n return;\n }\n const dataView = serializeContextCommand(this, command);\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async setBitmapScaleX(bitmapScaleX: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"x\", bitmapScaleX, sendImmediately);\n }\n async setBitmapScaleY(bitmapScaleY: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"y\", bitmapScaleY, sendImmediately);\n }\n async setBitmapScale(bitmapScale: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"all\", bitmapScale, sendImmediately);\n }\n async resetBitmapScale(sendImmediately?: boolean) {\n //return this.setBitmapScaleDirection(\"all\", 1, sendImmediately);\n\n const differences = this.#contextStateHelper.update({\n bitmapScaleX: 1,\n bitmapScaleY: 1,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetBitmapScale\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async selectSpriteColor(\n spriteColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(spriteColorIndex);\n this.assertValidColorIndex(colorIndex);\n const spriteColorIndices = this.contextState.spriteColorIndices.slice();\n spriteColorIndices[spriteColorIndex] = colorIndex;\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectSpriteColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteColorIndex,\n colorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n get spriteColorIndices() {\n return this.contextState.spriteColorIndices;\n }\n get spriteColors() {\n return this.spriteColorIndices.map((colorIndex) => this.colors[colorIndex]);\n }\n async selectSpriteColors(\n spriteColorPairs: DisplaySpriteColorPair[],\n sendImmediately?: boolean\n ) {\n _console.assertRangeWithError(\n \"spriteColors\",\n spriteColorPairs.length,\n 1,\n this.numberOfColors\n );\n const spriteColorIndices = this.contextState.spriteColorIndices.slice();\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n this.assertValidColorIndex(spriteColorIndex);\n this.assertValidColorIndex(colorIndex);\n spriteColorIndices[spriteColorIndex] = colorIndex;\n });\n\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectSpriteColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteColorPairs,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpriteColor(\n spriteColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n return this.setColor(\n this.spriteColorIndices[spriteColorIndex],\n color,\n sendImmediately\n );\n }\n async setSpriteColorOpacity(\n spriteColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n return this.setColorOpacity(\n this.spriteColorIndices[spriteColorIndex],\n opacity,\n sendImmediately\n );\n }\n\n async resetSpriteColors(sendImmediately?: boolean) {\n const spriteColorIndices = new Array(this.numberOfColors).fill(0);\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetSpriteColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpriteScaleDirection(\n direction: DisplayScaleDirection,\n spriteScale: number,\n sendImmediately?: boolean\n ) {\n spriteScale = clamp(spriteScale, minDisplayScale, maxDisplayScale);\n spriteScale = roundScale(spriteScale);\n const commandType = DisplaySpriteScaleDirectionToCommandType[direction];\n _console.log({ [commandType]: spriteScale });\n const newState: PartialDisplayContextState = {};\n let command: DisplayContextCommand;\n switch (direction) {\n case \"all\":\n newState.spriteScaleX = spriteScale;\n newState.spriteScaleY = spriteScale;\n command = { type: \"setSpriteScale\", spriteScale };\n break;\n case \"x\":\n newState.spriteScaleX = spriteScale;\n command = { type: \"setSpriteScaleX\", spriteScaleX: spriteScale };\n break;\n case \"y\":\n newState.spriteScaleY = spriteScale;\n command = { type: \"setSpriteScaleY\", spriteScaleY: spriteScale };\n break;\n }\n const differences = this.#contextStateHelper.update(newState);\n if (differences.length == 0) {\n return;\n }\n const dataView = serializeContextCommand(this, command);\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async setSpriteScaleX(spriteScaleX: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"x\", spriteScaleX, sendImmediately);\n }\n async setSpriteScaleY(spriteScaleY: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"y\", spriteScaleY, sendImmediately);\n }\n async setSpriteScale(spriteScale: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"all\", spriteScale, sendImmediately);\n }\n async resetSpriteScale(sendImmediately?: boolean) {\n //return this.setSpriteScaleDirection(\"all\", 1, sendImmediately);\n\n const differences = this.#contextStateHelper.update({\n spriteScaleX: 1,\n spriteScaleY: 1,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetSpriteScale\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpritesLineHeight(\n spritesLineHeight: number,\n sendImmediately?: boolean\n ) {\n this.assertValidLineWidth(spritesLineHeight);\n const differences = this.#contextStateHelper.update({\n spritesLineHeight,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSpritesLineHeight\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spritesLineHeight,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpritesDirectionGeneric(\n direction: DisplayDirection,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n assertValidDirection(direction);\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineDirection\"\n : \"spritesDirection\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineDirection\"\n : \"setSpritesDirection\";\n\n const differences = this.#contextStateHelper.update({\n [stateKey]: direction,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: direction,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesDirection(\n spritesDirection: DisplayDirection,\n sendImmediately?: boolean\n ) {\n await this.setSpritesDirectionGeneric(\n spritesDirection,\n false,\n sendImmediately\n );\n }\n async setSpritesLineDirection(\n spritesLineDirection: DisplayDirection,\n sendImmediately?: boolean\n ) {\n await this.setSpritesDirectionGeneric(\n spritesLineDirection,\n true,\n sendImmediately\n );\n }\n\n async setSpritesSpacingGeneric(\n spacing: number,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineSpacing\"\n : \"spritesSpacing\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineSpacing\"\n : \"setSpritesSpacing\";\n\n const differences = this.#contextStateHelper.update({\n [stateKey]: spacing,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: spacing,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesSpacing(spritesSpacing: number, sendImmediately?: boolean) {\n await this.setSpritesSpacingGeneric(spritesSpacing, false, sendImmediately);\n }\n async setSpritesLineSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ) {\n await this.setSpritesSpacingGeneric(spritesSpacing, true, sendImmediately);\n }\n\n async setSpritesAlignmentGeneric(\n alignment: DisplayAlignment,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n assertValidAlignment(alignment);\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineAlignment\"\n : \"spritesAlignment\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineAlignment\"\n : \"setSpritesAlignment\";\n const differences = this.#contextStateHelper.update({\n [stateKey]: alignment,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: alignment,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesAlignment(\n spritesAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setSpritesAlignmentGeneric(\n spritesAlignment,\n false,\n sendImmediately\n );\n }\n async setSpritesLineAlignment(\n spritesLineAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setSpritesAlignmentGeneric(\n spritesLineAlignment,\n true,\n sendImmediately\n );\n }\n\n async clearRect(\n x: number,\n y: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"clearRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n x,\n y,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRoundRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n borderRadius: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRoundRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n borderRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawCircle(\n offsetX: number,\n offsetY: number,\n radius: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawCircle\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawEllipse\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRegularPolygon(\n offsetX: number,\n offsetY: number,\n radius: number,\n numberOfSides: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRegularPolygon\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n numberOfSides,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawPolygon(points: Vector2[], sendImmediately?: boolean) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n const commandType: DisplayContextCommandType = \"drawPolygon\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n points,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawWireframe(wireframe: DisplayWireframe, sendImmediately?: boolean) {\n wireframe = trimWireframe(wireframe);\n if (wireframe.points.length == 0) {\n return;\n }\n assertValidWireframe(wireframe);\n if (this.#contextStateHelper.isSegmentUniform) {\n const polygon = isWireframePolygon(wireframe);\n if (polygon) {\n return this.drawSegments(polygon, sendImmediately);\n }\n }\n const commandType: DisplayContextCommandType = \"drawWireframe\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n wireframe,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `wireframe data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawCurve(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n assertValidNumberOfControlPoints(curveType, controlPoints);\n const commandType: DisplayContextCommandType =\n curveType == \"cubic\"\n ? \"drawCubicBezierCurve\"\n : \"drawQuadraticBezierCurve\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n controlPoints,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawCurves(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n assertValidPathNumberOfControlPoints(curveType, controlPoints);\n const commandType: DisplayContextCommandType =\n curveType == \"cubic\"\n ? \"drawCubicBezierCurves\"\n : \"drawQuadraticBezierCurves\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n controlPoints,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `curve data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawQuadraticBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurve(\"quadratic\", controlPoints, sendImmediately);\n }\n async drawQuadraticBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurves(\"quadratic\", controlPoints, sendImmediately);\n }\n\n async drawCubicBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurve(\"cubic\", controlPoints, sendImmediately);\n }\n async drawCubicBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurves(\"cubic\", controlPoints, sendImmediately);\n }\n\n async _drawPath(\n isClosed: boolean,\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ) {\n assertValidPath(curves);\n\n const commandType: DisplayContextCommandType = isClosed\n ? \"drawClosedPath\"\n : \"drawPath\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n curves,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `path data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawPath(curves: DisplayBezierCurve[], sendImmediately?: boolean) {\n await this._drawPath(false, curves, sendImmediately);\n }\n async drawClosedPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ) {\n await this._drawPath(true, curves, sendImmediately);\n }\n\n async drawSegment(\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawSegment\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n startX,\n startY,\n endX,\n endY,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawSegments(points: Vector2[], sendImmediately?: boolean) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n const commandType: DisplayContextCommandType = \"drawSegments\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n points,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n const mid = Math.floor(points.length / 2);\n const firstHalf = points.slice(0, mid + 1);\n const secondHalf = points.slice(mid);\n _console.log({ firstHalf, secondHalf });\n _console.log(\"sending first half\", firstHalf);\n await this.drawSegments(firstHalf, false);\n _console.log(\"sending second half\", secondHalf);\n await this.drawSegments(secondHalf, sendImmediately);\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawArc(\n offsetX: number,\n offsetY: number,\n radius: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawArc\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n startAngle,\n angleOffset,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawArcEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawArcEllipse\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n assertValidNumberOfColors(numberOfColors: number) {\n _console.assertRangeWithError(\n \"numberOfColors\",\n numberOfColors,\n 2,\n this.numberOfColors\n );\n }\n\n assertValidBitmap(bitmap: DisplayBitmap, checkSize?: boolean) {\n this.assertValidNumberOfColors(bitmap.numberOfColors);\n assertValidBitmapPixels(bitmap);\n if (checkSize) {\n this.#assertValidBitmapSize(bitmap);\n }\n }\n #assertValidBitmapSize(bitmap: DisplayBitmap) {\n const pixelDataLength = getBitmapNumberOfBytes(bitmap);\n _console.assertRangeWithError(\n \"bitmap.pixels.length\",\n pixelDataLength,\n 1,\n this.#maxCommandDataLength - drawBitmapHeaderLength\n );\n }\n async drawBitmap(\n offsetX: number,\n offsetY: number,\n bitmap: DisplayBitmap,\n sendImmediately?: boolean\n ) {\n this.assertValidBitmap(bitmap, true);\n const commandType: DisplayContextCommandType = \"drawBitmap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n bitmap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async imageToBitmap(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors?: number\n ) {\n return imageToBitmap(\n image,\n width,\n height,\n this.colors,\n this.bitmapColorIndices,\n numberOfColors\n );\n }\n async quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number\n ) {\n return quantizeImage(image, width, height, numberOfColors);\n }\n async resizeAndQuantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[]\n ) {\n return resizeAndQuantizeImage(image, width, height, numberOfColors, colors);\n }\n\n // CONTEXT COMMANDS\n\n async runContextCommand(\n command: DisplayContextCommand,\n sendImmediately?: boolean\n ) {\n return runDisplayContextCommand(this, command, sendImmediately);\n }\n async runContextCommands(\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n ) {\n return runDisplayContextCommands(this, commands, sendImmediately);\n }\n\n #isReady = true;\n get isReady() {\n return this.isAvailable && this.#isReady;\n }\n #lastReadyTime = 0;\n #lastShowRequestTime = 0;\n #minReadyInterval = 60; // Forced delay due to Frame's fpga timing...\n #waitBeforeReady = true;\n async #parseDisplayReady(dataView: DataView) {\n const now = Date.now();\n const timeSinceLastDraw = now - this.#lastShowRequestTime;\n const timeSinceLastReady = now - this.#lastReadyTime;\n //_console.log(`${timeSinceLastReady}ms since last render`);\n _console.log(`${timeSinceLastDraw}ms draw time`);\n if (this.#waitBeforeReady && timeSinceLastReady < this.#minReadyInterval) {\n const timeToWait = this.#minReadyInterval - timeSinceLastReady;\n _console.log(`waiting ${timeToWait}ms`);\n await wait(timeToWait);\n }\n this.#isReady = true;\n this.#lastReadyTime = Date.now();\n this.#dispatchEvent(\"displayReady\", {});\n }\n\n // SPRITE SHEET\n #spriteSheets: Record<string, DisplaySpriteSheet> = {};\n #spriteSheetIndices: Record<string, number> = {};\n get spriteSheets() {\n return this.#spriteSheets;\n }\n get spriteSheetIndices() {\n return this.#spriteSheetIndices;\n }\n async #setSpriteSheetName(\n spriteSheetName: string,\n sendImmediately?: boolean\n ) {\n if (typeof spriteSheetName == \"number\") {\n // @ts-expect-error\n spriteSheetName = spriteSheetName.toString();\n }\n _console.assertTypeWithError(spriteSheetName, \"string\");\n _console.assertRangeWithError(\n \"newName\",\n spriteSheetName.length,\n MinSpriteSheetNameLength,\n MaxSpriteSheetNameLength\n );\n const setSpriteSheetNameData = textEncoder.encode(spriteSheetName);\n _console.log({ setSpriteSheetNameData });\n\n const promise = this.waitForEvent(\"getSpriteSheetName\");\n this.sendMessage(\n [{ type: \"setSpriteSheetName\", data: setSpriteSheetNameData.buffer }],\n sendImmediately\n );\n await promise;\n }\n #pendingSpriteSheet?: DisplaySpriteSheet;\n get pendingSpriteSheet() {\n return this.#pendingSpriteSheet;\n }\n #pendingSpriteSheetName?: string;\n get pendingSpriteSheetName() {\n return this.#pendingSpriteSheetName;\n }\n #updateSpriteSheetName(updatedSpriteSheetName: string) {\n _console.assertTypeWithError(updatedSpriteSheetName, \"string\");\n this.#pendingSpriteSheetName = updatedSpriteSheetName;\n _console.log({ updatedSpriteSheetName: this.#pendingSpriteSheetName });\n this.#dispatchEvent(\"getSpriteSheetName\", {\n spriteSheetName: this.#pendingSpriteSheetName,\n });\n }\n sendFile!: SendFileCallback;\n serializeSpriteSheet(spriteSheet: DisplaySpriteSheet): ArrayBuffer {\n return serializeSpriteSheet(this, spriteSheet);\n }\n async uploadSpriteSheet(spriteSheet: DisplaySpriteSheet) {\n if (spriteSheet.sprites.length == 0) {\n _console.log(\"no sprites in spriteSheet\");\n return;\n }\n if (this.#pendingSpriteSheet) {\n await this.waitForEvent(\"displaySpriteSheetUploadComplete\");\n await this.uploadSpriteSheet(spriteSheet);\n return;\n }\n spriteSheet = structuredClone(spriteSheet);\n this.#pendingSpriteSheet = spriteSheet;\n const buffer = this.serializeSpriteSheet(this.#pendingSpriteSheet);\n await this.#setSpriteSheetName(this.#pendingSpriteSheet.name);\n const promise = this.waitForEvent(\"displaySpriteSheetUploadComplete\");\n this.sendFile(\"spriteSheet\", buffer, true);\n await promise;\n }\n async uploadSpriteSheets(spriteSheets: DisplaySpriteSheet[]) {\n for (const spriteSheet of spriteSheets) {\n await this.uploadSpriteSheet(spriteSheet);\n }\n }\n assertLoadedSpriteSheet(spriteSheetName: string) {\n assertLoadedSpriteSheet(this, spriteSheetName);\n }\n assertSelectedSpriteSheet(spriteSheetName: string) {\n assertSelectedSpriteSheet(this, spriteSheetName);\n }\n assertAnySelectedSpriteSheet() {\n assertAnySelectedSpriteSheet(this);\n }\n assertSprite(spriteName: string) {\n return assertSprite(this, spriteName);\n }\n getSprite(spriteName: string): DisplaySprite | undefined {\n return getSprite(this, spriteName);\n }\n getSpriteSheetPalette(\n paletteName: string\n ): DisplaySpriteSheetPalette | undefined {\n return getSpriteSheetPalette(this, paletteName);\n }\n getSpriteSheetPaletteSwap(\n paletteSwapName: string\n ): DisplaySpriteSheetPaletteSwap | undefined {\n return getSpriteSheetPaletteSwap(this, paletteSwapName);\n }\n getSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string\n ): DisplaySpritePaletteSwap | undefined {\n return getSpritePaletteSwap(this, spriteName, paletteSwapName);\n }\n\n get selectedSpriteSheet() {\n if (this.contextState.spriteSheetName) {\n return this.#spriteSheets[this.contextState.spriteSheetName];\n }\n }\n get selectedSpriteSheetName() {\n return this.selectedSpriteSheet?.name;\n }\n async selectSpriteSheet(spriteSheetName: string, sendImmediately?: boolean) {\n this.assertLoadedSpriteSheet(spriteSheetName);\n const differences = this.#contextStateHelper.update({\n spriteSheetName,\n });\n if (differences.length == 0) {\n return;\n }\n const spriteSheetIndex = this.spriteSheetIndices[spriteSheetName];\n //_console.log(\"selecting\", { spriteSheetIndex, spriteSheetName });\n const commandType: DisplayContextCommandType = \"selectSpriteSheet\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteSheetIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async drawSprite(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n this.selectedSpriteSheet,\n \"no spriteSheet selected\"\n );\n _console.log(\n `drawing sprite \"${spriteName}\" in selectedSpriteSheet`,\n this.selectedSpriteSheet\n );\n let spriteIndex = this.selectedSpriteSheet!.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n spriteIndex != -1,\n `sprite \"${spriteName}\" not found in spriteSheet`\n );\n spriteIndex = spriteIndex!;\n const commandType: DisplayContextCommandType = \"drawSprite\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n spriteIndex,\n use2Bytes: this.selectedSpriteSheet!.sprites.length > 255,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawSprites(\n offsetX: number,\n offsetY: number,\n spriteLines: DisplaySpriteLines,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n this.contextState.spritesLineHeight > 0,\n `spritesLineHeight must be >0`\n );\n const spriteSerializedLines: DisplaySpriteSerializedLines = [];\n spriteLines.forEach((spriteLine) => {\n const serializedLine: DisplaySpriteSerializedLine = [];\n spriteLine.forEach((spriteSubLine) => {\n this.assertLoadedSpriteSheet(spriteSubLine.spriteSheetName);\n const spriteSheet = this.spriteSheets[spriteSubLine.spriteSheetName];\n const spriteSheetIndex = this.spriteSheetIndices[spriteSheet.name];\n const serializedSubLine: DisplaySpriteSerializedSubLine = {\n spriteSheetIndex,\n spriteIndices: [],\n use2Bytes: spriteSheet.sprites.length > 255,\n };\n spriteSubLine.spriteNames.forEach((spriteName) => {\n let spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n spriteIndex != -1,\n `sprite \"${spriteName}\" not found`\n );\n spriteIndex = spriteIndex!;\n serializedSubLine.spriteIndices.push(spriteIndex);\n });\n serializedLine.push(serializedSubLine);\n });\n spriteSerializedLines.push(serializedLine);\n });\n _console.log(\"spriteSerializedLines\", spriteSerializedLines);\n const commandType: DisplayContextCommandType = \"drawSprites\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n spriteSerializedLines: spriteSerializedLines,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawSpritesString(\n offsetX: number,\n offsetY: number,\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[],\n sendImmediately?: boolean\n ) {\n const spriteLines = this.stringToSpriteLines(\n string,\n requireAll,\n maxLineBreadth,\n separators\n );\n await this.drawSprites(offsetX, offsetY, spriteLines, sendImmediately);\n }\n stringToSpriteLines(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLines {\n return stringToSpriteLines(\n string,\n this.spriteSheets,\n this.contextState,\n requireAll,\n maxLineBreadth,\n separators\n );\n }\n stringToSpriteLinesMetrics(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ) {\n return stringToSpriteLinesMetrics(\n string,\n this.spriteSheets,\n this.contextState,\n requireAll,\n maxLineBreadth,\n separators\n );\n }\n\n async drawSpriteFromSpriteSheet(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n ) {\n return drawSpriteFromSpriteSheet(\n this,\n offsetX,\n offsetY,\n spriteName,\n spriteSheet,\n paletteName,\n sendImmediately\n );\n }\n\n #parseSpriteSheetIndex(dataView: DataView) {\n const spriteSheetIndex = dataView.getUint8(0);\n _console.log({\n pendingSpriteSheet: this.#pendingSpriteSheet,\n spriteSheetName: this.#pendingSpriteSheetName,\n spriteSheetIndex,\n });\n if (this.isServerSide) {\n return;\n }\n _console.assertWithError(\n this.#pendingSpriteSheetName != undefined,\n \"expected spriteSheetName when receiving spriteSheetIndex\"\n );\n _console.assertWithError(\n this.#pendingSpriteSheet != undefined,\n \"expected pendingSpriteSheet when receiving spriteSheetIndex\"\n );\n this.#spriteSheets[this.#pendingSpriteSheetName!] =\n this.#pendingSpriteSheet!;\n this.#spriteSheetIndices[this.#pendingSpriteSheetName!] = spriteSheetIndex;\n _console.log(\n `finished uploading \"${this.#pendingSpriteSheetName!}\" spriteSheet`\n );\n this.#dispatchEvent(\"displaySpriteSheetUploadComplete\", {\n spriteSheetName: this.#pendingSpriteSheetName!,\n spriteSheet: this.#pendingSpriteSheet!,\n });\n this.#pendingSpriteSheet = undefined;\n }\n\n // MESSAGE\n parseMessage(messageType: DisplayMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"isDisplayAvailable\":\n this.#parseIsDisplayAvailable(dataView);\n break;\n case \"displayStatus\":\n this.#parseDisplayStatus(dataView);\n break;\n case \"displayInformation\":\n this.#parseDisplayInformation(dataView);\n break;\n case \"getDisplayBrightness\":\n case \"setDisplayBrightness\":\n this.#parseDisplayBrightness(dataView);\n break;\n case \"displayReady\":\n this.#parseDisplayReady(dataView);\n break;\n case \"getSpriteSheetName\":\n case \"setSpriteSheetName\":\n const spriteSheetName = textDecoder.decode(\n dataView.buffer as ArrayBuffer\n );\n _console.log({ spriteSheetName });\n this.#updateSpriteSheetName(spriteSheetName);\n break;\n case \"spriteSheetIndex\":\n this.#parseSpriteSheetIndex(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n // SPRITE SHEET PALETTES\n\n assertSpriteSheetPalette(paletteName: string) {\n assertSpriteSheetPalette(this, paletteName);\n }\n assertSpriteSheetPaletteSwap(paletteSwapName: string) {\n assertSpriteSheetPaletteSwap(this, paletteSwapName);\n }\n assertSpritePaletteSwap(spriteName: string, paletteSwapName: string) {\n assertSpritePaletteSwap(this, spriteName, paletteSwapName);\n }\n async selectSpriteSheetPalette(\n paletteName: string,\n offset?: number,\n indicesOnly?: boolean,\n sendImmediately?: boolean\n ) {\n await selectSpriteSheetPalette(\n this,\n paletteName,\n offset,\n indicesOnly,\n sendImmediately\n );\n }\n async selectSpriteSheetPaletteSwap(\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ) {\n await selectSpriteSheetPaletteSwap(\n this,\n paletteSwapName,\n offset,\n sendImmediately\n );\n }\n async selectSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ) {\n await selectSpritePaletteSwap(\n this,\n spriteName,\n paletteSwapName,\n offset,\n sendImmediately\n );\n }\n\n #isDrawingBlankSprite = false;\n async startSprite(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n !this.#isDrawingBlankSprite,\n `already drawing blank sprite`\n );\n this.#isDrawingBlankSprite = true;\n this.#saveContext(sendImmediately);\n this.#contextStateHelper.reset();\n this.contextState.bitmapColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.contextState.spriteColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n\n const commandType: DisplayContextCommandType = \"startSprite\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async endSprite(sendImmediately?: boolean) {\n this.#restoreContext(sendImmediately);\n\n _console.assertWithError(\n this.#isDrawingBlankSprite,\n `not drawing blank sprite`\n );\n this.#isDrawingBlankSprite = false;\n\n // _console.log(\"endSprite\");\n await this.#sendContextCommand(\"endSprite\", undefined, sendImmediately);\n }\n\n reset() {\n _console.log(\"clearing displayManager\");\n // @ts-ignore\n this.#displayStatus = undefined;\n this.#isAvailable = false;\n this.#displayInformation = undefined;\n // @ts-ignore\n this.#brightness = undefined;\n this.#contextCommandBuffers = [];\n this.#isAvailable = false;\n\n this.#contextStateHelper.reset();\n this.#colors.length = 0;\n this.#opacities.length = 0;\n\n this.#isReady = true;\n this.#pendingSpriteSheet = undefined;\n this.#pendingSpriteSheetName = undefined;\n\n this.isServerSide = false;\n\n this.#isDrawingBlankSprite = false;\n\n Object.keys(this.#spriteSheetIndices).forEach(\n (spriteSheetName) => delete this.#spriteSheetIndices[spriteSheetName]\n );\n Object.keys(this.#spriteSheets).forEach(\n (spriteSheetName) => delete this.#spriteSheets[spriteSheetName]\n );\n }\n\n // MTU\n #mtu!: number;\n get mtu() {\n return this.#mtu;\n }\n set mtu(newMtu: number) {\n this.#mtu = newMtu;\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n //_console.log(\"redundant isServerSide assignment\");\n return;\n }\n this.#isServerSide = newIsServerSide;\n _console.log({ isServerSide: this.isServerSide });\n }\n}\n\nexport default DisplayManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport { Timer } from \"../utils/Timer.ts\";\nimport { FileTransferMessageTypes } from \"../FileTransferManager.ts\";\nimport { TfliteMessageTypes } from \"../TfliteManager.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport { parseMessage } from \"../utils/ParseUtils.ts\";\nimport { DeviceInformationTypes } from \"../DeviceInformationManager.ts\";\nimport { InformationMessageTypes } from \"../InformationManager.ts\";\nimport { VibrationMessageTypes } from \"../vibration/VibrationManager.ts\";\nimport { SensorConfigurationMessageTypes } from \"../sensor/SensorConfigurationManager.ts\";\nimport { SensorDataMessageTypes } from \"../sensor/SensorDataManager.ts\";\nimport { WifiMessageTypes } from \"../WifiManager.ts\";\nimport { CameraMessageTypes } from \"../CameraManager.ts\";\nimport { MicrophoneMessageTypes } from \"../MicrophoneManager.ts\";\nimport { DisplayMessageTypes } from \"../DisplayManager.ts\";\n\nconst _console = createConsole(\"BaseConnectionManager\", { log: false });\n\nexport const ConnectionTypes = [\n \"webBluetooth\",\n \"noble\",\n \"client\",\n \"webSocket\",\n \"udp\",\n] as const;\nexport type ConnectionType = (typeof ConnectionTypes)[number];\n\nexport const ClientConnectionTypes = [\"noble\", \"webSocket\", \"udp\"] as const;\nexport type ClientConnectionType = (typeof ClientConnectionTypes)[number];\n\ninterface BaseConnectOptions {\n type: \"client\" | \"webBluetooth\" | \"webSocket\" | \"udp\";\n}\nexport interface WebBluetoothConnectOptions extends BaseConnectOptions {\n type: \"webBluetooth\";\n}\ninterface BaseWifiConnectOptions extends BaseConnectOptions {\n ipAddress: string;\n}\nexport interface ClientConnectOptions extends BaseConnectOptions {\n type: \"client\";\n subType?: \"noble\" | \"webSocket\" | \"udp\";\n}\nexport interface WebSocketConnectOptions extends BaseWifiConnectOptions {\n type: \"webSocket\";\n isWifiSecure?: boolean;\n}\nexport interface UDPConnectOptions extends BaseWifiConnectOptions {\n type: \"udp\";\n //sendPort: number;\n receivePort?: number;\n}\nexport type ConnectOptions =\n | WebBluetoothConnectOptions\n | WebSocketConnectOptions\n | UDPConnectOptions\n | ClientConnectOptions;\n\nexport const ConnectionStatuses = [\n \"notConnected\",\n \"connecting\",\n \"connected\",\n \"disconnecting\",\n] as const;\nexport type ConnectionStatus = (typeof ConnectionStatuses)[number];\n\nexport const ConnectionEventTypes = [\n ...ConnectionStatuses,\n \"connectionStatus\",\n \"isConnected\",\n] as const;\nexport type ConnectionEventType = (typeof ConnectionEventTypes)[number];\n\nexport interface ConnectionStatusEventMessages {\n notConnected: any;\n connecting: any;\n connected: any;\n disconnecting: any;\n connectionStatus: { connectionStatus: ConnectionStatus };\n isConnected: { isConnected: boolean };\n}\n\nexport interface TxMessage {\n type: TxRxMessageType;\n data?: ArrayBuffer;\n}\n\nexport const TxRxMessageTypes = [\n ...InformationMessageTypes,\n ...SensorConfigurationMessageTypes,\n ...SensorDataMessageTypes,\n ...VibrationMessageTypes,\n ...FileTransferMessageTypes,\n ...TfliteMessageTypes,\n ...WifiMessageTypes,\n ...CameraMessageTypes,\n ...MicrophoneMessageTypes,\n ...DisplayMessageTypes,\n] as const;\nexport type TxRxMessageType = (typeof TxRxMessageTypes)[number];\n\nexport const SMPMessageTypes = [\"smp\"] as const;\nexport type SMPMessageType = (typeof SMPMessageTypes)[number];\n\nexport const BatteryLevelMessageTypes = [\"batteryLevel\"] as const;\nexport type BatteryLevelMessageType = (typeof BatteryLevelMessageTypes)[number];\n\nexport const MetaConnectionMessageTypes = [\"rx\", \"tx\"] as const;\nexport type MetaConnectionMessageType =\n (typeof MetaConnectionMessageTypes)[number];\n\nexport const ConnectionMessageTypes = [\n ...BatteryLevelMessageTypes,\n ...DeviceInformationTypes,\n ...MetaConnectionMessageTypes,\n ...TxRxMessageTypes,\n ...SMPMessageTypes,\n] as const;\nexport type ConnectionMessageType = (typeof ConnectionMessageTypes)[number];\n\nexport type ConnectionStatusCallback = (status: ConnectionStatus) => void;\nexport type MessageReceivedCallback = (\n messageType: ConnectionMessageType,\n dataView: DataView\n) => void;\nexport type MessagesReceivedCallback = () => void;\n\nabstract class BaseConnectionManager {\n static #AssertValidTxRxMessageType(messageType: TxRxMessageType) {\n _console.assertEnumWithError(messageType, TxRxMessageTypes);\n }\n\n abstract get bluetoothId(): string;\n\n // CALLBACKS\n onStatusUpdated?: ConnectionStatusCallback;\n onMessageReceived?: MessageReceivedCallback;\n onMessagesReceived?: MessagesReceivedCallback;\n\n protected get baseConstructor() {\n return this.constructor as typeof BaseConnectionManager;\n }\n static get isSupported() {\n return false;\n }\n get isSupported() {\n return this.baseConstructor.isSupported;\n }\n\n get canUpdateFirmware() {\n return false;\n }\n\n static type: ConnectionType;\n get type(): ConnectionType {\n return this.baseConstructor.type;\n }\n\n /** @throws {Error} if not supported */\n #assertIsSupported() {\n _console.assertWithError(this.isSupported, `${this.type} is not supported`);\n }\n\n constructor() {\n this.#assertIsSupported();\n }\n\n #status: ConnectionStatus = \"notConnected\";\n get status() {\n return this.#status;\n }\n protected set status(newConnectionStatus) {\n _console.assertEnumWithError(newConnectionStatus, ConnectionStatuses);\n if (this.#status == newConnectionStatus) {\n _console.log(\n `tried to assign same connection status \"${newConnectionStatus}\"`\n );\n return;\n }\n _console.log(`new connection status \"${newConnectionStatus}\"`);\n this.#status = newConnectionStatus;\n this.onStatusUpdated!(this.status);\n\n if (this.isConnected) {\n this.#timer.start();\n } else {\n this.#timer.stop();\n }\n\n if (this.#status == \"notConnected\") {\n this.mtu = this.defaultMtu;\n }\n }\n\n get isConnected() {\n return this.status == \"connected\";\n }\n\n get isAvailable() {\n return false;\n }\n\n /** @throws {Error} if connected */\n protected assertIsNotConnected() {\n _console.assertWithError(!this.isConnected, \"device is already connected\");\n }\n /** @throws {Error} if connecting */\n #assertIsNotConnecting() {\n _console.assertWithError(\n this.status != \"connecting\",\n \"device is already connecting\"\n );\n }\n /** @throws {Error} if not connected */\n protected assertIsConnected() {\n _console.assertWithError(this.isConnected, \"device is not connected\");\n }\n /** @throws {Error} if disconnecting */\n #assertIsNotDisconnecting() {\n _console.assertWithError(\n this.status != \"disconnecting\",\n \"device is already disconnecting\"\n );\n }\n /** @throws {Error} if not connected or is disconnecting */\n assertIsConnectedAndNotDisconnecting() {\n this.assertIsConnected();\n this.#assertIsNotDisconnecting();\n }\n\n async connect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return false;\n }\n if (this.#status == \"connecting\") {\n _console.log(\"already connecting\");\n return false;\n }\n // this.assertIsNotConnected();\n // this.#assertIsNotConnecting();\n this.status = \"connecting\";\n return true;\n }\n get canReconnect() {\n return false;\n }\n async reconnect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return false;\n }\n if (this.#status == \"connecting\") {\n _console.log(\"already connecting\");\n return false;\n }\n // this.assertIsNotConnected();\n // this.#assertIsNotConnecting();\n if (!this.canReconnect) {\n _console.warn(\"unable to reconnect\");\n return false;\n }\n // _console.assertWithError(this.canReconnect, \"unable to reconnect\");\n this.status = \"connecting\";\n _console.log(\"attempting to reconnect...\");\n return true;\n }\n async disconnect() {\n if (!this.isConnected) {\n _console.log(\"already not connected\");\n return false;\n }\n if (this.#status == \"disconnecting\") {\n _console.log(\"already disconnecting\");\n return false;\n }\n // this.assertIsConnected();\n // this.#assertIsNotDisconnecting();\n this.status = \"disconnecting\";\n _console.log(\"disconnecting from device...\");\n return true;\n }\n\n async sendSmpMessage(data: ArrayBuffer) {\n this.assertIsConnectedAndNotDisconnecting();\n _console.log(\"sending smp message\", data);\n }\n\n #pendingMessages: TxMessage[] = [];\n #isSendingMessages = false;\n async sendTxMessages(\n messages: TxMessage[] | undefined,\n sendImmediately: boolean = true\n ) {\n this.assertIsConnectedAndNotDisconnecting();\n\n if (messages) {\n this.#pendingMessages.push(...messages);\n _console.log(`appended ${messages.length} messages`);\n }\n\n if (!sendImmediately) {\n _console.log(\"not sending immediately - waiting until later\");\n return;\n }\n\n if (this.#isSendingMessages) {\n _console.log(\"already sending messages - waiting until later\");\n return;\n }\n if (this.#pendingMessages.length == 0) {\n _console.log(\"no pendingMessages\");\n return;\n }\n this.#isSendingMessages = true;\n\n _console.log(\"sendTxMessages\", this.#pendingMessages.slice());\n\n const arrayBuffers = this.#pendingMessages.map((message) => {\n BaseConnectionManager.#AssertValidTxRxMessageType(message.type);\n const messageTypeEnum = TxRxMessageTypes.indexOf(message.type);\n const dataLength = new DataView(new ArrayBuffer(2));\n dataLength.setUint16(0, message.data?.byteLength || 0, true);\n return concatenateArrayBuffers(messageTypeEnum, dataLength, message.data);\n });\n this.#pendingMessages.length = 0;\n\n if (this.mtu) {\n while (arrayBuffers.length > 0) {\n if (\n arrayBuffers.every(\n (arrayBuffer) => arrayBuffer.byteLength > this.mtu! - 3\n )\n ) {\n _console.log(\"every arrayBuffer is too big to send\");\n break;\n }\n _console.log(\"remaining arrayBuffers.length\", arrayBuffers.length);\n let arrayBufferByteLength = 0;\n let arrayBufferCount = 0;\n arrayBuffers.some((arrayBuffer) => {\n if (arrayBufferByteLength + arrayBuffer.byteLength > this.mtu! - 3) {\n _console.log(\n `stopping appending arrayBuffers ( length ${arrayBuffer.byteLength} too much)`\n );\n return true;\n }\n _console.log(\n `allowing arrayBuffer with length ${arrayBuffer.byteLength}`\n );\n arrayBufferCount++;\n arrayBufferByteLength += arrayBuffer.byteLength;\n });\n const arrayBuffersToSend = arrayBuffers.splice(0, arrayBufferCount);\n _console.log({ arrayBufferCount, arrayBuffersToSend });\n\n const arrayBuffer = concatenateArrayBuffers(...arrayBuffersToSend);\n _console.log(\"sending arrayBuffer (partitioned)\", arrayBuffer);\n await this.sendTxData(arrayBuffer);\n }\n } else {\n const arrayBuffer = concatenateArrayBuffers(...arrayBuffers);\n _console.log(\"sending arrayBuffer (all)\", arrayBuffer);\n await this.sendTxData(arrayBuffer);\n }\n\n this.#isSendingMessages = false;\n\n this.sendTxMessages(undefined, true);\n }\n\n protected defaultMtu = 23;\n //mtu?: number;\n mtu?: number = this.defaultMtu;\n\n async sendTxData(data: ArrayBuffer) {\n _console.log(\"sendTxData\", data);\n }\n\n parseRxMessage(dataView: DataView) {\n parseMessage(\n dataView,\n TxRxMessageTypes,\n this.#onRxMessage.bind(this),\n null,\n true\n );\n this.onMessagesReceived!();\n }\n\n #onRxMessage(messageType: TxRxMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n this.onMessageReceived!(messageType, dataView);\n }\n\n #timer = new Timer(this.#checkConnection.bind(this), 5000);\n #checkConnection() {\n //console.log(\"checking connection...\");\n if (!this.isConnected) {\n _console.log(\"timer detected disconnection\");\n this.status = \"notConnected\";\n }\n }\n\n clear() {\n this.#isSendingMessages = false;\n this.#pendingMessages.length = 0;\n }\n\n remove() {\n this.clear();\n\n this.onStatusUpdated = undefined;\n this.onMessageReceived = undefined;\n this.onMessagesReceived = undefined;\n }\n}\n\nexport default BaseConnectionManager;\n","import { createConsole } from \"./Console.ts\";\nimport { spacesToPascalCase } from \"./stringUtils.ts\";\n\nconst _console = createConsole(\"EventUtils\", { log: false });\n\ntype BoundEventListeners = { [eventType: string]: EventListener };\nexport type BoundGenericEventListeners = { [eventType: string]: Function };\n\nexport function bindEventListeners(\n eventTypes: readonly string[],\n boundEventListeners: BoundGenericEventListeners,\n target: any\n) {\n _console.log(\"bindEventListeners\", { eventTypes, boundEventListeners, target });\n eventTypes.forEach((eventType) => {\n const _eventType = `_on${spacesToPascalCase(eventType)}`;\n _console.assertWithError(target[_eventType], `no event \"${_eventType}\" found in target`);\n _console.log(`binding eventType \"${eventType}\" as ${_eventType} from target`, target);\n const boundEvent = target[_eventType].bind(target);\n target[_eventType] = boundEvent;\n boundEventListeners[eventType] = boundEvent;\n });\n}\n\nexport function addEventListeners(target: any, boundEventListeners: BoundGenericEventListeners) {\n let addEventListener = target.addEventListener || target.addListener || target.on || target.AddEventListener;\n _console.assertWithError(addEventListener, \"no add listener function found for target\");\n addEventListener = addEventListener.bind(target);\n Object.entries(boundEventListeners).forEach(([eventType, eventListener]) => {\n addEventListener(eventType, eventListener);\n });\n}\n\nexport function removeEventListeners(target: any, boundEventListeners: BoundGenericEventListeners) {\n let removeEventListener = target.removeEventListener || target.removeListener || target.RemoveEventListener;\n _console.assertWithError(removeEventListener, \"no remove listener function found for target\");\n removeEventListener = removeEventListener.bind(target);\n Object.entries(boundEventListeners).forEach(([eventType, eventListener]) => {\n removeEventListener(eventType, eventListener);\n });\n}\n","import {\n isInBrowser,\n isInLensStudio,\n isInNode,\n} from \"../../utils/environment.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\n\nconst _console = createConsole(\"bluetoothUUIDs\", { log: false });\n\n/** NODE_START */\nimport * as webbluetooth from \"webbluetooth\";\nvar BluetoothUUID = webbluetooth.BluetoothUUID;\n/** NODE_END */\n\n/** BROWSER_START */\nif (isInBrowser) {\n var BluetoothUUID = window.BluetoothUUID;\n}\n/** BROWSER_END */\n\n/** LS_START */\n\nvar BluetoothUUID = {\n getService: (uuid: number | string): string => toUUID(uuid),\n\n getCharacteristic: (uuid: number | string): string => toUUID(uuid),\n\n getDescriptor: (uuid: number | string): string => toUUID(uuid),\n\n getCharacteristicName: (uuid: number | string): string | null => null,\n\n getServiceName: (uuid: number | string): string | null => null,\n\n getDescriptorName: (uuid: number | string): string | null => null,\n};\n\nfunction toUUID(uuid: number | string): string {\n if (typeof uuid === \"number\") {\n uuid = uuid.toString(16).padStart(4, \"0\");\n }\n\n if (/^[0-9a-fA-F]{4,8}$/.test(uuid)) {\n return `0000${uuid.padStart(8, \"0\")}-0000-1000-8000-00805f9b34fb`;\n }\n\n return uuid.toLowerCase();\n}\n\n/** LS_END */\n\nfunction generateBluetoothUUID(value: string): BluetoothServiceUUID {\n _console.assertTypeWithError(value, \"string\");\n _console.assertWithError(\n value.length == 4,\n \"value must be 4 characters long\"\n );\n return `ea6d${value}-a725-4f9b-893d-c3913e33b39f`;\n}\n\nfunction stringToCharacteristicUUID(\n identifier: string\n): BluetoothCharacteristicUUID {\n return BluetoothUUID?.getCharacteristic?.(identifier);\n}\n\nfunction stringToServiceUUID(identifier: string): BluetoothServiceUUID {\n return BluetoothUUID?.getService?.(identifier);\n}\n\nexport type BluetoothServiceName =\n | \"deviceInformation\"\n | \"battery\"\n | \"main\"\n | \"smp\";\nimport { DeviceInformationType } from \"../../DeviceInformationManager.ts\";\nexport type BluetoothCharacteristicName =\n | DeviceInformationType\n | \"batteryLevel\"\n | \"rx\"\n | \"tx\"\n | \"smp\";\n\ninterface BluetoothCharacteristicInformation {\n uuid: BluetoothCharacteristicUUID;\n}\ninterface BluetoothServiceInformation {\n uuid: BluetoothServiceUUID;\n characteristics: {\n [characteristicName in BluetoothCharacteristicName]?: BluetoothCharacteristicInformation;\n };\n}\ninterface BluetoothServicesInformation {\n services: {\n [serviceName in BluetoothServiceName]: BluetoothServiceInformation;\n };\n}\nconst bluetoothUUIDs: BluetoothServicesInformation = Object.freeze({\n services: {\n deviceInformation: {\n uuid: stringToServiceUUID(\"device_information\"),\n characteristics: {\n manufacturerName: {\n uuid: stringToCharacteristicUUID(\"manufacturer_name_string\"),\n },\n modelNumber: {\n uuid: stringToCharacteristicUUID(\"model_number_string\"),\n },\n hardwareRevision: {\n uuid: stringToCharacteristicUUID(\"hardware_revision_string\"),\n },\n firmwareRevision: {\n uuid: stringToCharacteristicUUID(\"firmware_revision_string\"),\n },\n softwareRevision: {\n uuid: stringToCharacteristicUUID(\"software_revision_string\"),\n },\n pnpId: {\n uuid: stringToCharacteristicUUID(\"pnp_id\"),\n },\n serialNumber: {\n uuid: stringToCharacteristicUUID(\"serial_number_string\"),\n },\n },\n },\n battery: {\n uuid: stringToServiceUUID(\"battery_service\"),\n characteristics: {\n batteryLevel: {\n uuid: stringToCharacteristicUUID(\"battery_level\"),\n },\n },\n },\n main: {\n uuid: generateBluetoothUUID(\"0000\"),\n characteristics: {\n rx: { uuid: generateBluetoothUUID(\"1000\") },\n tx: { uuid: generateBluetoothUUID(\"1001\") },\n },\n },\n smp: {\n uuid: \"8d53dc1d-1db7-4cd3-868b-8a527460aa84\",\n characteristics: {\n smp: { uuid: \"da2e7828-fbce-4e01-ae9e-261174997c48\" },\n },\n },\n },\n});\n\nexport const serviceUUIDs = [bluetoothUUIDs.services.main.uuid];\nexport const optionalServiceUUIDs = [\n bluetoothUUIDs.services.deviceInformation.uuid,\n bluetoothUUIDs.services.battery.uuid,\n bluetoothUUIDs.services.smp.uuid,\n];\nexport const allServiceUUIDs = [...serviceUUIDs, ...optionalServiceUUIDs];\n\nexport function getServiceNameFromUUID(\n serviceUUID: BluetoothServiceUUID\n): BluetoothServiceName | undefined {\n serviceUUID = serviceUUID.toString().toLowerCase();\n const serviceNames = Object.keys(\n bluetoothUUIDs.services\n ) as BluetoothServiceName[];\n return serviceNames.find((serviceName) => {\n const serviceInfo = bluetoothUUIDs.services[serviceName];\n let serviceInfoUUID = serviceInfo.uuid.toString();\n if (serviceUUID.length == 4) {\n serviceInfoUUID = serviceInfoUUID.slice(4, 8);\n }\n if (!serviceUUID.includes(\"-\")) {\n serviceInfoUUID = serviceInfoUUID.replaceAll(\"-\", \"\");\n }\n return serviceUUID == serviceInfoUUID;\n });\n}\n\nexport const characteristicUUIDs: BluetoothCharacteristicUUID[] = [];\nexport const allCharacteristicUUIDs: BluetoothCharacteristicUUID[] = [];\n\nexport const characteristicNames: BluetoothCharacteristicName[] = [];\nexport const allCharacteristicNames: BluetoothCharacteristicName[] = [];\n\nObject.values(bluetoothUUIDs.services).forEach((serviceInfo) => {\n if (!serviceInfo.characteristics) {\n return;\n }\n const characteristicNames = Object.keys(\n serviceInfo.characteristics\n ) as BluetoothCharacteristicName[];\n characteristicNames.forEach((characteristicName) => {\n const characteristicInfo = serviceInfo.characteristics[characteristicName]!;\n if (serviceUUIDs.includes(serviceInfo.uuid)) {\n characteristicUUIDs.push(characteristicInfo.uuid);\n characteristicNames.push(characteristicName);\n }\n allCharacteristicUUIDs.push(characteristicInfo.uuid);\n allCharacteristicNames.push(characteristicName);\n });\n}, []);\n\n//_console.log({ characteristicUUIDs, allCharacteristicUUIDs });\n\nexport function getCharacteristicNameFromUUID(\n characteristicUUID: BluetoothCharacteristicUUID\n): BluetoothCharacteristicName | undefined {\n //_console.log({ characteristicUUID });\n characteristicUUID = characteristicUUID.toString().toLowerCase();\n var characteristicName: BluetoothCharacteristicName | undefined;\n Object.values(bluetoothUUIDs.services).some((serviceInfo) => {\n const characteristicNames = Object.keys(\n serviceInfo.characteristics\n ) as BluetoothCharacteristicName[];\n characteristicName = characteristicNames.find((_characteristicName) => {\n const characteristicInfo =\n serviceInfo.characteristics[_characteristicName]!;\n let characteristicInfoUUID = characteristicInfo.uuid.toString();\n if (characteristicUUID.length == 4) {\n characteristicInfoUUID = characteristicInfoUUID.slice(4, 8);\n }\n if (!characteristicUUID.includes(\"-\")) {\n characteristicInfoUUID = characteristicInfoUUID.replaceAll(\"-\", \"\");\n }\n return characteristicUUID == characteristicInfoUUID;\n });\n return characteristicName;\n });\n return characteristicName;\n}\n\nexport function getCharacteristicProperties(\n characteristicName: BluetoothCharacteristicName\n): BluetoothCharacteristicProperties {\n const properties = {\n broadcast: false,\n read: true,\n writeWithoutResponse: false,\n write: false,\n notify: false,\n indicate: false,\n authenticatedSignedWrites: false,\n reliableWrite: false,\n writableAuxiliaries: false,\n };\n\n // read\n switch (characteristicName) {\n case \"rx\":\n case \"tx\":\n case \"smp\":\n properties.read = false;\n break;\n }\n\n // notify\n switch (characteristicName) {\n case \"batteryLevel\":\n case \"rx\":\n case \"smp\":\n properties.notify = true;\n break;\n }\n\n // write without response\n switch (characteristicName) {\n case \"smp\":\n properties.writeWithoutResponse = true;\n break;\n }\n\n // write\n switch (characteristicName) {\n case \"tx\":\n properties.write = true;\n break;\n }\n\n return properties;\n}\n\nexport const serviceDataUUID = \"0000\";\n","import { createConsole } from \"../../utils/Console.ts\";\nimport BaseConnectionManager from \"../BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"BluetoothConnectionManager\", { log: false });\n\nimport { BluetoothCharacteristicName } from \"./bluetoothUUIDs.ts\";\n\nabstract class BluetoothConnectionManager extends BaseConnectionManager {\n get isAvailable() {\n // no way to tell if the user has turned bluetooth on or off\n return true;\n }\n\n isInRange = true;\n\n protected onCharacteristicValueChanged(\n characteristicName: BluetoothCharacteristicName,\n dataView: DataView\n ) {\n if (characteristicName == \"rx\") {\n this.parseRxMessage(dataView);\n } else {\n this.onMessageReceived?.(characteristicName, dataView);\n }\n }\n\n protected async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n _console.log(\"writeCharacteristic\", ...arguments);\n }\n\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n await this.writeCharacteristic(\"smp\", data);\n }\n\n async sendTxData(data: ArrayBuffer) {\n super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n await this.writeCharacteristic(\"tx\", data);\n }\n}\n\nexport default BluetoothConnectionManager;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport {\n isInNode,\n isInBrowser,\n isInBluefy,\n isInWebBLE,\n} from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n serviceUUIDs,\n optionalServiceUUIDs,\n getServiceNameFromUUID,\n getCharacteristicNameFromUUID,\n getCharacteristicProperties,\n} from \"./bluetoothUUIDs.ts\";\nimport BluetoothConnectionManager from \"./BluetoothConnectionManager.ts\";\nimport {\n BluetoothCharacteristicName,\n BluetoothServiceName,\n} from \"./bluetoothUUIDs.ts\";\nimport { ConnectionType } from \"../BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"WebBluetoothConnectionManager\", { log: false });\n\ntype WebBluetoothInterface = webbluetooth.Bluetooth | Bluetooth;\n\ninterface BluetoothService extends BluetoothRemoteGATTService {\n name?: BluetoothServiceName;\n}\ninterface BluetoothCharacteristic extends BluetoothRemoteGATTCharacteristic {\n name?: BluetoothCharacteristicName;\n}\n\nvar bluetooth: WebBluetoothInterface | undefined;\n/** NODE_START */\nimport * as webbluetooth from \"webbluetooth\";\nif (isInNode) {\n bluetooth = webbluetooth.bluetooth;\n}\n/** NODE_END */\n\n/** BROWSER_START */\nif (isInBrowser) {\n bluetooth = window.navigator.bluetooth;\n}\n/** BROWSER_END */\n\nclass WebBluetoothConnectionManager extends BluetoothConnectionManager {\n get bluetoothId() {\n return this.device!.id;\n }\n\n get canUpdateFirmware() {\n return this.#characteristics.has(\"smp\");\n }\n\n #boundBluetoothCharacteristicEventListeners: {\n [eventType: string]: EventListener;\n } = {\n characteristicvaluechanged: this.#onCharacteristicvaluechanged.bind(this),\n };\n #boundBluetoothDeviceEventListeners: { [eventType: string]: EventListener } =\n {\n gattserverdisconnected: this.#onGattserverdisconnected.bind(this),\n };\n\n static get isSupported() {\n return Boolean(bluetooth);\n }\n static get type(): ConnectionType {\n return \"webBluetooth\";\n }\n\n #device?: BluetoothDevice;\n get device() {\n return this.#device;\n }\n set device(newDevice) {\n if (this.#device == newDevice) {\n _console.log(\"tried to assign the same BluetoothDevice\");\n return;\n }\n if (this.#device) {\n removeEventListeners(\n this.#device,\n this.#boundBluetoothDeviceEventListeners\n );\n }\n if (newDevice) {\n addEventListeners(newDevice, this.#boundBluetoothDeviceEventListeners);\n }\n this.#device = newDevice;\n }\n\n get server(): BluetoothRemoteGATTServer | undefined {\n return this.#device?.gatt;\n }\n get isConnected() {\n return this.server?.connected || false;\n }\n\n #services: Map<BluetoothServiceName, BluetoothService> = new Map();\n #characteristics: Map<BluetoothCharacteristicName, BluetoothCharacteristic> =\n new Map();\n\n async connect() {\n const canContinue = super.connect();\n if (!canContinue) {\n return false;\n }\n\n try {\n const device = await bluetooth!.requestDevice({\n filters: [{ services: serviceUUIDs }],\n optionalServices: isInBrowser ? optionalServiceUUIDs : [],\n });\n\n _console.log(\"got BluetoothDevice\");\n this.device = device;\n\n _console.log(\"connecting to device...\");\n const server = await this.server!.connect();\n _console.log(`connected to device? ${server.connected}`);\n\n await this.#getServicesAndCharacteristics();\n\n _console.log(\"fully connected\");\n\n this.status = \"connected\";\n return true;\n } catch (error) {\n _console.error(error);\n this.status = \"notConnected\";\n this.server?.disconnect();\n await this.#removeEventListeners();\n return false;\n }\n }\n async #getServicesAndCharacteristics() {\n this.#removeEventListeners();\n\n _console.log(\"getting services...\");\n const services = await this.server!.getPrimaryServices();\n _console.log(\"got services\", services.length);\n //const service = await this.server!.getPrimaryService(\"8d53dc1d-1db7-4cd3-868b-8a527460aa84\");\n\n _console.log(\"getting characteristics...\");\n for (const serviceIndex in services) {\n const service = services[serviceIndex] as BluetoothService;\n _console.log({ service });\n const serviceName = getServiceNameFromUUID(service.uuid)!;\n _console.assertWithError(\n serviceName,\n `no name found for service uuid \"${service.uuid}\"`\n );\n _console.log(`got \"${serviceName}\" service`);\n service.name = serviceName;\n this.#services.set(serviceName, service);\n _console.log(`getting characteristics for \"${serviceName}\" service`);\n const characteristics = await service.getCharacteristics();\n _console.log(`got characteristics for \"${serviceName}\" service`);\n for (const characteristicIndex in characteristics) {\n const characteristic = characteristics[\n characteristicIndex\n ] as BluetoothCharacteristic;\n _console.log({ characteristic });\n const characteristicName = getCharacteristicNameFromUUID(\n characteristic.uuid\n )!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic uuid \"${characteristic.uuid}\" in \"${serviceName}\" service`\n );\n _console.log(\n `got \"${characteristicName}\" characteristic in \"${serviceName}\" service`\n );\n characteristic.name = characteristicName;\n this.#characteristics.set(characteristicName, characteristic);\n addEventListeners(\n characteristic,\n this.#boundBluetoothCharacteristicEventListeners\n );\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.notify) {\n _console.log(\n `starting notifications for \"${characteristicName}\" characteristic`\n );\n await characteristic.startNotifications();\n }\n if (characteristicProperties.read) {\n _console.log(`reading \"${characteristicName}\" characteristic...`);\n await characteristic.readValue();\n if (isInBluefy || isInWebBLE) {\n this.#onCharacteristicValueChanged(characteristic);\n }\n }\n }\n }\n }\n async #removeEventListeners() {\n if (this.device) {\n removeEventListeners(\n this.device,\n this.#boundBluetoothDeviceEventListeners\n );\n }\n\n const promises = Array.from(this.#characteristics.keys()).map(\n (characteristicName) => {\n const characteristic = this.#characteristics.get(characteristicName)!;\n removeEventListeners(\n characteristic,\n this.#boundBluetoothCharacteristicEventListeners\n );\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.notify) {\n _console.log(\n `stopping notifications for \"${characteristicName}\" characteristic`\n );\n return characteristic.stopNotifications();\n }\n }\n );\n\n return Promise.allSettled(promises);\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n await this.#removeEventListeners();\n this.server?.disconnect();\n this.status = \"notConnected\";\n return true;\n }\n\n #onCharacteristicvaluechanged(event: Event) {\n _console.log(\"oncharacteristicvaluechanged\");\n\n const characteristic = event.target as BluetoothCharacteristic;\n this.#onCharacteristicValueChanged(characteristic);\n }\n\n #onCharacteristicValueChanged(characteristic: BluetoothCharacteristic) {\n _console.log(\"onCharacteristicValue\");\n\n const characteristicName = characteristic.name!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic with uuid \"${characteristic.uuid}\"`\n );\n\n _console.log(\n `oncharacteristicvaluechanged for \"${characteristicName}\" characteristic`\n );\n const dataView = characteristic.value!;\n _console.assertWithError(\n dataView,\n `no data found for \"${characteristicName}\" characteristic`\n );\n _console.log(\n `data for \"${characteristicName}\" characteristic`,\n Array.from(new Uint8Array(dataView.buffer))\n );\n\n try {\n this.onCharacteristicValueChanged(characteristicName, dataView);\n } catch (error) {\n _console.error(error);\n }\n }\n\n async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n super.writeCharacteristic(characteristicName, data);\n\n const characteristic = this.#characteristics.get(characteristicName)!;\n _console.assertWithError(\n characteristic,\n `${characteristicName} characteristic not found`\n );\n _console.log(\"writing characteristic\", characteristic, data);\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.writeWithoutResponse) {\n _console.log(\"writing without response\");\n await characteristic.writeValueWithoutResponse(data);\n } else {\n _console.log(\"writing with response\");\n await characteristic.writeValueWithResponse(data);\n }\n _console.log(\"wrote characteristic\");\n\n if (characteristicProperties.read && !characteristicProperties.notify) {\n _console.log(\"reading value after write...\");\n await characteristic.readValue();\n if (isInBluefy || isInWebBLE) {\n this.#onCharacteristicValueChanged(characteristic);\n }\n }\n }\n\n #onGattserverdisconnected() {\n _console.log(\"gattserverdisconnected\");\n this.status = \"notConnected\";\n }\n\n get canReconnect() {\n return Boolean(this.server && !this.server.connected && this.isInRange);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n try {\n await this.server!.connect();\n } catch (error) {\n _console.error(error);\n this.isInRange = false;\n return false;\n }\n\n if (this.isConnected) {\n _console.log(\"successfully reconnected!\");\n await this.#getServicesAndCharacteristics();\n this.status = \"connected\";\n return true;\n } else {\n _console.log(\"unable to reconnect\");\n this.status = \"notConnected\";\n return false;\n }\n }\n\n remove() {\n super.remove();\n this.device = undefined;\n }\n}\n\nexport default WebBluetoothConnectionManager;\n","/*\n * The MIT License (MIT)\n *\n * Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nconst POW_2_24 = 5.960464477539063e-8;\nconst POW_2_32 = 4294967296;\nconst POW_2_53 = 9007199254740992;\n\nexport function encode(value) {\n let data = new ArrayBuffer(256);\n let dataView = new DataView(data);\n let lastLength;\n let offset = 0;\n\n function prepareWrite(length) {\n let newByteLength = data.byteLength;\n const requiredLength = offset + length;\n while (newByteLength < requiredLength) {\n newByteLength <<= 1;\n }\n if (newByteLength !== data.byteLength) {\n const oldDataView = dataView;\n data = new ArrayBuffer(newByteLength);\n dataView = new DataView(data);\n const uint32count = (offset + 3) >> 2;\n for (let i = 0; i < uint32count; ++i) {\n dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));\n }\n }\n\n lastLength = length;\n return dataView;\n }\n function commitWrite() {\n offset += lastLength;\n }\n function writeFloat64(value) {\n commitWrite(prepareWrite(8).setFloat64(offset, value));\n }\n function writeUint8(value) {\n commitWrite(prepareWrite(1).setUint8(offset, value));\n }\n function writeUint8Array(value) {\n const dataView = prepareWrite(value.length);\n for (let i = 0; i < value.length; ++i) {\n dataView.setUint8(offset + i, value[i]);\n }\n commitWrite();\n }\n function writeUint16(value) {\n commitWrite(prepareWrite(2).setUint16(offset, value));\n }\n function writeUint32(value) {\n commitWrite(prepareWrite(4).setUint32(offset, value));\n }\n function writeUint64(value) {\n const low = value % POW_2_32;\n const high = (value - low) / POW_2_32;\n const dataView = prepareWrite(8);\n dataView.setUint32(offset, high);\n dataView.setUint32(offset + 4, low);\n commitWrite();\n }\n function writeTypeAndLength(type, length) {\n if (length < 24) {\n writeUint8((type << 5) | length);\n } else if (length < 0x100) {\n writeUint8((type << 5) | 24);\n writeUint8(length);\n } else if (length < 0x10000) {\n writeUint8((type << 5) | 25);\n writeUint16(length);\n } else if (length < 0x100000000) {\n writeUint8((type << 5) | 26);\n writeUint32(length);\n } else {\n writeUint8((type << 5) | 27);\n writeUint64(length);\n }\n }\n\n function encodeItem(value) {\n let i;\n const utf8data = [];\n let length;\n\n if (value === false) {\n return writeUint8(0xf4);\n }\n if (value === true) {\n return writeUint8(0xf5);\n }\n if (value === null) {\n return writeUint8(0xf6);\n }\n if (value === undefined) {\n return writeUint8(0xf7);\n }\n\n switch (typeof value) {\n case \"number\":\n if (Math.floor(value) === value) {\n if (value >= 0 && value <= POW_2_53) {\n return writeTypeAndLength(0, value);\n }\n if (-POW_2_53 <= value && value < 0) {\n return writeTypeAndLength(1, -(value + 1));\n }\n }\n writeUint8(0xfb);\n return writeFloat64(value);\n\n case \"string\":\n for (i = 0; i < value.length; ++i) {\n let charCode = value.charCodeAt(i);\n if (charCode < 0x80) {\n utf8data.push(charCode);\n } else if (charCode < 0x800) {\n utf8data.push(0xc0 | (charCode >> 6));\n utf8data.push(0x80 | (charCode & 0x3f));\n } else if (charCode < 0xd800) {\n utf8data.push(0xe0 | (charCode >> 12));\n utf8data.push(0x80 | ((charCode >> 6) & 0x3f));\n utf8data.push(0x80 | (charCode & 0x3f));\n } else {\n charCode = (charCode & 0x3ff) << 10;\n charCode |= value.charCodeAt(++i) & 0x3ff;\n charCode += 0x10000;\n\n utf8data.push(0xf0 | (charCode >> 18));\n utf8data.push(0x80 | ((charCode >> 12) & 0x3f));\n utf8data.push(0x80 | ((charCode >> 6) & 0x3f));\n utf8data.push(0x80 | (charCode & 0x3f));\n }\n }\n\n writeTypeAndLength(3, utf8data.length);\n return writeUint8Array(utf8data);\n\n default:\n if (Array.isArray(value)) {\n length = value.length;\n writeTypeAndLength(4, length);\n for (i = 0; i < length; ++i) {\n encodeItem(value[i]);\n }\n } else if (value instanceof Uint8Array) {\n writeTypeAndLength(2, value.length);\n writeUint8Array(value);\n } else {\n const keys = Object.keys(value);\n length = keys.length;\n writeTypeAndLength(5, length);\n for (i = 0; i < length; ++i) {\n const key = keys[i];\n encodeItem(key);\n encodeItem(value[key]);\n }\n }\n }\n }\n\n encodeItem(value);\n\n if (\"slice\" in data) {\n return data.slice(0, offset);\n }\n\n const ret = new ArrayBuffer(offset);\n const retView = new DataView(ret);\n for (let i = 0; i < offset; ++i) {\n retView.setUint8(i, dataView.getUint8(i));\n }\n return ret;\n}\n\nexport function decode(data, tagger, simpleValue) {\n const dataView = new DataView(data);\n let offset = 0;\n\n if (typeof tagger !== \"function\") {\n tagger = function (value) {\n return value;\n };\n }\n if (typeof simpleValue !== \"function\") {\n simpleValue = function () {\n return undefined;\n };\n }\n\n function commitRead(length, value) {\n offset += length;\n return value;\n }\n function readArrayBuffer(length) {\n return commitRead(length, new Uint8Array(data, offset, length));\n }\n function readFloat16() {\n const tempArrayBuffer = new ArrayBuffer(4);\n const tempDataView = new DataView(tempArrayBuffer);\n const value = readUint16();\n\n const sign = value & 0x8000;\n let exponent = value & 0x7c00;\n const fraction = value & 0x03ff;\n\n if (exponent === 0x7c00) {\n exponent = 0xff << 10;\n } else if (exponent !== 0) {\n exponent += (127 - 15) << 10;\n } else if (fraction !== 0) {\n return (sign ? -1 : 1) * fraction * POW_2_24;\n }\n\n tempDataView.setUint32(0, (sign << 16) | (exponent << 13) | (fraction << 13));\n return tempDataView.getFloat32(0);\n }\n function readFloat32() {\n return commitRead(4, dataView.getFloat32(offset));\n }\n function readFloat64() {\n return commitRead(8, dataView.getFloat64(offset));\n }\n function readUint8() {\n return commitRead(1, dataView.getUint8(offset));\n }\n function readUint16() {\n return commitRead(2, dataView.getUint16(offset));\n }\n function readUint32() {\n return commitRead(4, dataView.getUint32(offset));\n }\n function readUint64() {\n return readUint32() * POW_2_32 + readUint32();\n }\n function readBreak() {\n if (dataView.getUint8(offset) !== 0xff) {\n return false;\n }\n offset += 1;\n return true;\n }\n function readLength(additionalInformation) {\n if (additionalInformation < 24) {\n return additionalInformation;\n }\n if (additionalInformation === 24) {\n return readUint8();\n }\n if (additionalInformation === 25) {\n return readUint16();\n }\n if (additionalInformation === 26) {\n return readUint32();\n }\n if (additionalInformation === 27) {\n return readUint64();\n }\n if (additionalInformation === 31) {\n return -1;\n }\n throw new Error(\"Invalid length encoding\");\n }\n function readIndefiniteStringLength(majorType) {\n const initialByte = readUint8();\n if (initialByte === 0xff) {\n return -1;\n }\n const length = readLength(initialByte & 0x1f);\n if (length < 0 || initialByte >> 5 !== majorType) {\n throw new Error(\"Invalid indefinite length element\");\n }\n return length;\n }\n\n function appendUtf16Data(utf16data, length) {\n for (let i = 0; i < length; ++i) {\n let value = readUint8();\n if (value & 0x80) {\n if (value < 0xe0) {\n value = ((value & 0x1f) << 6) | (readUint8() & 0x3f);\n length -= 1;\n } else if (value < 0xf0) {\n value = ((value & 0x0f) << 12) | ((readUint8() & 0x3f) << 6) | (readUint8() & 0x3f);\n length -= 2;\n } else {\n value =\n ((value & 0x0f) << 18) | ((readUint8() & 0x3f) << 12) | ((readUint8() & 0x3f) << 6) | (readUint8() & 0x3f);\n length -= 3;\n }\n }\n\n if (value < 0x10000) {\n utf16data.push(value);\n } else {\n value -= 0x10000;\n utf16data.push(0xd800 | (value >> 10));\n utf16data.push(0xdc00 | (value & 0x3ff));\n }\n }\n }\n\n function decodeItem() {\n const initialByte = readUint8();\n const majorType = initialByte >> 5;\n const additionalInformation = initialByte & 0x1f;\n let i;\n let length;\n\n if (majorType === 7) {\n switch (additionalInformation) {\n case 25:\n return readFloat16();\n case 26:\n return readFloat32();\n case 27:\n return readFloat64();\n }\n }\n\n length = readLength(additionalInformation);\n if (length < 0 && (majorType < 2 || majorType > 6)) {\n throw new Error(\"Invalid length\");\n }\n\n const utf16data = [];\n let retArray;\n const retObject = {};\n\n switch (majorType) {\n case 0:\n return length;\n case 1:\n return -1 - length;\n case 2:\n if (length < 0) {\n const elements = [];\n let fullArrayLength = 0;\n while ((length = readIndefiniteStringLength(majorType)) >= 0) {\n fullArrayLength += length;\n elements.push(readArrayBuffer(length));\n }\n const fullArray = new Uint8Array(fullArrayLength);\n let fullArrayOffset = 0;\n for (i = 0; i < elements.length; ++i) {\n fullArray.set(elements[i], fullArrayOffset);\n fullArrayOffset += elements[i].length;\n }\n return fullArray;\n }\n return readArrayBuffer(length);\n case 3:\n if (length < 0) {\n while ((length = readIndefiniteStringLength(majorType)) >= 0) {\n appendUtf16Data(utf16data, length);\n }\n } else {\n appendUtf16Data(utf16data, length);\n }\n return String.fromCharCode.apply(null, utf16data);\n case 4:\n if (length < 0) {\n retArray = [];\n while (!readBreak()) {\n retArray.push(decodeItem());\n }\n } else {\n retArray = new Array(length);\n for (i = 0; i < length; ++i) {\n retArray[i] = decodeItem();\n }\n }\n return retArray;\n case 5:\n for (i = 0; i < length || (length < 0 && !readBreak()); ++i) {\n const key = decodeItem();\n retObject[key] = decodeItem();\n }\n return retObject;\n case 6:\n return tagger(decodeItem(), length);\n case 7:\n switch (length) {\n case 20:\n return false;\n case 21:\n return true;\n case 22:\n return null;\n case 23:\n return undefined;\n default:\n return simpleValue(length);\n }\n }\n }\n\n const ret = decodeItem();\n if (offset !== data.byteLength) {\n throw new Error(\"Remaining bytes\");\n }\n return ret;\n}\n\nexport const CBOR = {\n encode,\n decode,\n};\n","/*\n * The MIT License (MIT)\n *\n * Copyright (c) 2023 Laird Connectivity\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * @file mcumgr\n * @brief Provides MCU manager operation functions for the Xbit USB Shell.\n * This file is inspired by the MIT licensed mcumgr file originally\n * authored by Andras Barthazi (https://github.com/boogie/mcumgr-web),\n * updated to also support file upload/download over SMP.\n */\n\nimport { CBOR } from \"./cbor.js\";\nimport { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"mcumgr\", { log: false });\n\nexport const constants = {\n // Opcodes\n MGMT_OP_READ: 0,\n MGMT_OP_READ_RSP: 1,\n MGMT_OP_WRITE: 2,\n MGMT_OP_WRITE_RSP: 3,\n\n // Groups\n MGMT_GROUP_ID_OS: 0,\n MGMT_GROUP_ID_IMAGE: 1,\n MGMT_GROUP_ID_STAT: 2,\n MGMT_GROUP_ID_CONFIG: 3,\n MGMT_GROUP_ID_LOG: 4,\n MGMT_GROUP_ID_CRASH: 5,\n MGMT_GROUP_ID_SPLIT: 6,\n MGMT_GROUP_ID_RUN: 7,\n MGMT_GROUP_ID_FS: 8,\n MGMT_GROUP_ID_SHELL: 9,\n\n // OS group\n OS_MGMT_ID_ECHO: 0,\n OS_MGMT_ID_CONS_ECHO_CTRL: 1,\n OS_MGMT_ID_TASKSTAT: 2,\n OS_MGMT_ID_MPSTAT: 3,\n OS_MGMT_ID_DATETIME_STR: 4,\n OS_MGMT_ID_RESET: 5,\n\n // Image group\n IMG_MGMT_ID_STATE: 0,\n IMG_MGMT_ID_UPLOAD: 1,\n IMG_MGMT_ID_FILE: 2,\n IMG_MGMT_ID_CORELIST: 3,\n IMG_MGMT_ID_CORELOAD: 4,\n IMG_MGMT_ID_ERASE: 5,\n\n // Filesystem group\n FS_MGMT_ID_FILE: 0,\n};\n\nexport class MCUManager {\n constructor() {\n this._mtu = 256;\n this._messageCallback = null;\n this._imageUploadProgressCallback = null;\n this._imageUploadNextCallback = null;\n this._fileUploadProgressCallback = null;\n this._fileUploadNextCallback = null;\n this._uploadIsInProgress = false;\n this._downloadIsInProgress = false;\n this._buffer = new Uint8Array();\n this._seq = 0;\n }\n\n onMessage(callback) {\n this._messageCallback = callback;\n return this;\n }\n\n onImageUploadNext(callback) {\n this._imageUploadNextCallback = callback;\n return this;\n }\n\n onImageUploadProgress(callback) {\n this._imageUploadProgressCallback = callback;\n return this;\n }\n\n onImageUploadFinished(callback) {\n this._imageUploadFinishedCallback = callback;\n return this;\n }\n\n onFileUploadNext(callback) {\n this._fileUploadNextCallback = callback;\n return this;\n }\n\n onFileUploadProgress(callback) {\n this._fileUploadProgressCallback = callback;\n return this;\n }\n\n onFileUploadFinished(callback) {\n this._fileUploadFinishedCallback = callback;\n return this;\n }\n\n onFileDownloadNext(callback) {\n this._fileDownloadNextCallback = callback;\n return this;\n }\n\n onFileDownloadProgress(callback) {\n this._fileDownloadProgressCallback = callback;\n return this;\n }\n\n onFileDownloadFinished(callback) {\n this._fileDownloadFinishedCallback = callback;\n return this;\n }\n\n _getMessage(op, group, id, data) {\n const _flags = 0;\n let encodedData = [];\n if (typeof data !== \"undefined\") {\n encodedData = [...new Uint8Array(CBOR.encode(data))];\n }\n const lengthLo = encodedData.length & 255;\n const lengthHi = encodedData.length >> 8;\n const groupLo = group & 255;\n const groupHi = group >> 8;\n const message = [op, _flags, lengthHi, lengthLo, groupHi, groupLo, this._seq, id, ...encodedData];\n this._seq = (this._seq + 1) % 256;\n\n return message;\n }\n\n _notification(buffer) {\n _console.log(\"mcumgr - message received\");\n const message = new Uint8Array(buffer);\n this._buffer = new Uint8Array([...this._buffer, ...message]);\n const messageLength = this._buffer[2] * 256 + this._buffer[3];\n if (this._buffer.length < messageLength + 8) return;\n this._processMessage(this._buffer.slice(0, messageLength + 8));\n this._buffer = this._buffer.slice(messageLength + 8);\n }\n\n _processMessage(message) {\n const [op, , lengthHi, lengthLo, groupHi, groupLo, , id] = message;\n const data = CBOR.decode(message.slice(8).buffer);\n const length = lengthHi * 256 + lengthLo;\n const group = groupHi * 256 + groupLo;\n\n _console.log(\"mcumgr - Process Message - Group: \" + group + \", Id: \" + id + \", Off: \" + data.off);\n if (group === constants.MGMT_GROUP_ID_IMAGE && id === constants.IMG_MGMT_ID_UPLOAD && data.off) {\n this._uploadOffset = data.off;\n this._uploadNext();\n return;\n }\n if (\n op === constants.MGMT_OP_WRITE_RSP &&\n group === constants.MGMT_GROUP_ID_FS &&\n id === constants.FS_MGMT_ID_FILE &&\n data.off\n ) {\n this._uploadFileOffset = data.off;\n this._uploadFileNext();\n return;\n }\n if (op === constants.MGMT_OP_READ_RSP && group === constants.MGMT_GROUP_ID_FS && id === constants.FS_MGMT_ID_FILE) {\n this._downloadFileOffset += data.data.length;\n if (data.len != undefined) {\n this._downloadFileLength = data.len;\n }\n _console.log(\"downloaded \" + this._downloadFileOffset + \" bytes of \" + this._downloadFileLength);\n if (this._downloadFileLength > 0) {\n this._fileDownloadProgressCallback({\n percentage: Math.floor((this._downloadFileOffset / this._downloadFileLength) * 100),\n });\n }\n if (this._messageCallback) this._messageCallback({ op, group, id, data, length });\n this._downloadFileNext();\n return;\n }\n\n if (this._messageCallback) this._messageCallback({ op, group, id, data, length });\n }\n\n cmdReset() {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_OS, constants.OS_MGMT_ID_RESET);\n }\n\n smpEcho(message) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_OS, constants.OS_MGMT_ID_ECHO, {\n d: message,\n });\n }\n\n cmdImageState() {\n return this._getMessage(constants.MGMT_OP_READ, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE);\n }\n\n cmdImageErase() {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_ERASE, {});\n }\n\n cmdImageTest(hash) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE, {\n hash,\n confirm: false,\n });\n }\n\n cmdImageConfirm(hash) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE, {\n hash,\n confirm: true,\n });\n }\n\n _hash(image) {\n return crypto.subtle.digest(\"SHA-256\", image);\n }\n\n async _uploadNext() {\n if (!this._uploadImage) {\n return;\n }\n\n if (this._uploadOffset >= this._uploadImage.byteLength) {\n this._uploadIsInProgress = false;\n this._imageUploadFinishedCallback();\n return;\n }\n\n const nmpOverhead = 8;\n const message = { data: new Uint8Array(), off: this._uploadOffset };\n if (this._uploadOffset === 0) {\n message.len = this._uploadImage.byteLength;\n message.sha = new Uint8Array(await this._hash(this._uploadImage));\n }\n this._imageUploadProgressCallback({\n percentage: Math.floor((this._uploadOffset / this._uploadImage.byteLength) * 100),\n });\n\n const length = this._mtu - CBOR.encode(message).byteLength - nmpOverhead - 3 - 5;\n\n message.data = new Uint8Array(this._uploadImage.slice(this._uploadOffset, this._uploadOffset + length));\n\n this._uploadOffset += length;\n\n const packet = this._getMessage(\n constants.MGMT_OP_WRITE,\n constants.MGMT_GROUP_ID_IMAGE,\n constants.IMG_MGMT_ID_UPLOAD,\n message\n );\n\n _console.log(\"mcumgr - _uploadNext: Message Length: \" + packet.length);\n\n this._imageUploadNextCallback({ packet });\n }\n async reset() {\n this._messageCallback = null;\n this._imageUploadProgressCallback = null;\n this._imageUploadNextCallback = null;\n this._fileUploadProgressCallback = null;\n this._fileUploadNextCallback = null;\n this._uploadIsInProgress = false;\n this._downloadIsInProgress = false;\n this._buffer = new Uint8Array();\n this._seq = 0;\n }\n\n async cmdUpload(image, slot = 0) {\n if (this._uploadIsInProgress) {\n _console.error(\"Upload is already in progress.\");\n return;\n }\n this._uploadIsInProgress = true;\n\n this._uploadOffset = 0;\n this._uploadImage = image;\n this._uploadSlot = slot;\n\n this._uploadNext();\n }\n\n async cmdUploadFile(filebuf, destFilename) {\n if (this._uploadIsInProgress) {\n _console.error(\"Upload is already in progress.\");\n return;\n }\n this._uploadIsInProgress = true;\n this._uploadFileOffset = 0;\n this._uploadFile = filebuf;\n this._uploadFilename = destFilename;\n\n this._uploadFileNext();\n }\n\n async _uploadFileNext() {\n _console.log(\"uploadFileNext - offset: \" + this._uploadFileOffset + \", length: \" + this._uploadFile.byteLength);\n\n if (this._uploadFileOffset >= this._uploadFile.byteLength) {\n this._uploadIsInProgress = false;\n this._fileUploadFinishedCallback();\n return;\n }\n\n const nmpOverhead = 8;\n const message = { data: new Uint8Array(), off: this._uploadFileOffset };\n if (this._uploadFileOffset === 0) {\n message.len = this._uploadFile.byteLength;\n }\n message.name = this._uploadFilename;\n this._fileUploadProgressCallback({\n percentage: Math.floor((this._uploadFileOffset / this._uploadFile.byteLength) * 100),\n });\n\n const length = this._mtu - CBOR.encode(message).byteLength - nmpOverhead;\n\n message.data = new Uint8Array(this._uploadFile.slice(this._uploadFileOffset, this._uploadFileOffset + length));\n\n this._uploadFileOffset += length;\n\n const packet = this._getMessage(\n constants.MGMT_OP_WRITE,\n constants.MGMT_GROUP_ID_FS,\n constants.FS_MGMT_ID_FILE,\n message\n );\n\n _console.log(\"mcumgr - _uploadNext: Message Length: \" + packet.length);\n\n this._fileUploadNextCallback({ packet });\n }\n\n async cmdDownloadFile(filename, destFilename) {\n if (this._downloadIsInProgress) {\n _console.error(\"Download is already in progress.\");\n return;\n }\n this._downloadIsInProgress = true;\n this._downloadFileOffset = 0;\n this._downloadFileLength = 0;\n this._downloadRemoteFilename = filename;\n this._downloadLocalFilename = destFilename;\n\n this._downloadFileNext();\n }\n\n async _downloadFileNext() {\n if (this._downloadFileLength > 0) {\n if (this._downloadFileOffset >= this._downloadFileLength) {\n this._downloadIsInProgress = false;\n this._fileDownloadFinishedCallback();\n return;\n }\n }\n\n const message = { off: this._downloadFileOffset };\n if (this._downloadFileOffset === 0) {\n message.name = this._downloadRemoteFilename;\n }\n\n const packet = this._getMessage(\n constants.MGMT_OP_READ,\n constants.MGMT_GROUP_ID_FS,\n constants.FS_MGMT_ID_FILE,\n message\n );\n _console.log(\"mcumgr - _downloadNext: Message Length: \" + packet.length);\n this._fileDownloadNextCallback({ packet });\n }\n\n async imageInfo(image) {\n const info = {};\n const view = new Uint8Array(image);\n\n // check header length\n if (view.length < 32) {\n throw new Error(\"Invalid image (too short file)\");\n }\n\n // check MAGIC bytes 0x96f3b83d\n if (view[0] !== 0x3d || view[1] !== 0xb8 || view[2] !== 0xf3 || view[3] !== 0x96) {\n throw new Error(\"Invalid image (wrong magic bytes)\");\n }\n\n // check load address is 0x00000000\n if (view[4] !== 0x00 || view[5] !== 0x00 || view[6] !== 0x00 || view[7] !== 0x00) {\n throw new Error(\"Invalid image (wrong load address)\");\n }\n\n const headerSize = view[8] + view[9] * 2 ** 8;\n\n // check protected TLV area size is 0\n if (view[10] !== 0x00 || view[11] !== 0x00) {\n throw new Error(\"Invalid image (wrong protected TLV area size)\");\n }\n\n const imageSize = view[12] + view[13] * 2 ** 8 + view[14] * 2 ** 16 + view[15] * 2 ** 24;\n info.imageSize = imageSize;\n\n // check image size is correct\n if (view.length < imageSize + headerSize) {\n throw new Error(\"Invalid image (wrong image size)\");\n }\n\n // check flags is 0x00000000\n if (view[16] !== 0x00 || view[17] !== 0x00 || view[18] !== 0x00 || view[19] !== 0x00) {\n throw new Error(\"Invalid image (wrong flags)\");\n }\n\n const version = `${view[20]}.${view[21]}.${view[22] + view[23] * 2 ** 8}`;\n info.version = version;\n\n info.hash = [...new Uint8Array(await this._hash(image.slice(0, imageSize + 32)))]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return info;\n }\n}\n","import Device, { SendSmpMessageCallback } from \"./Device.ts\";\nimport { getFileBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { MCUManager, constants } from \"./utils/mcumgr.js\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"FirmwareManager\", { log: false });\n\nexport const FirmwareMessageTypes = [\"smp\"] as const;\nexport type FirmwareMessageType = (typeof FirmwareMessageTypes)[number];\n\nexport const FirmwareEventTypes = [\n ...FirmwareMessageTypes,\n \"firmwareImages\",\n \"firmwareUploadProgress\",\n \"firmwareStatus\",\n \"firmwareUploadComplete\",\n] as const;\nexport type FirmwareEventType = (typeof FirmwareEventTypes)[number];\n\nexport const FirmwareStatuses = [\"idle\", \"uploading\", \"uploaded\", \"pending\", \"testing\", \"erasing\"] as const;\nexport type FirmwareStatus = (typeof FirmwareStatuses)[number];\n\nexport interface FirmwareImage {\n slot: number;\n active: boolean;\n confirmed: boolean;\n pending: boolean;\n permanent: boolean;\n bootable: boolean;\n version: string;\n hash?: Uint8Array;\n empty?: boolean;\n}\n\nexport interface FirmwareEventMessages {\n smp: { dataView: DataView };\n firmwareImages: { firmwareImages: FirmwareImage[] };\n firmwareUploadProgress: { progress: number };\n firmwareStatus: { firmwareStatus: FirmwareStatus };\n //firmwareUploadComplete: {};\n}\n\nexport type FirmwareEventDispatcher = EventDispatcher<Device, FirmwareEventType, FirmwareEventMessages>;\n\nclass FirmwareManager {\n sendMessage!: SendSmpMessageCallback;\n\n constructor() {\n this.#assignMcuManagerCallbacks();\n autoBind(this);\n }\n\n eventDispatcher!: FirmwareEventDispatcher;\n get addEventListenter() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n parseMessage(messageType: FirmwareMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"smp\":\n this.#mcuManager._notification(Array.from(new Uint8Array(dataView.buffer)));\n this.#dispatchEvent(\"smp\", { dataView });\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n async uploadFirmware(file: FileLike) {\n _console.log(\"uploadFirmware\", file);\n\n const promise = this.waitForEvent(\"firmwareUploadComplete\");\n\n await this.getImages();\n\n const arrayBuffer = await getFileBuffer(file);\n const imageInfo = await this.#mcuManager.imageInfo(arrayBuffer);\n _console.log({ imageInfo });\n\n this.#mcuManager.cmdUpload(arrayBuffer, 1);\n\n this.#updateStatus(\"uploading\");\n\n await promise;\n }\n\n #status: FirmwareStatus = \"idle\";\n get status() {\n return this.#status;\n }\n #updateStatus(newStatus: FirmwareStatus) {\n _console.assertEnumWithError(newStatus, FirmwareStatuses);\n if (this.#status == newStatus) {\n _console.log(`redundant firmwareStatus assignment \"${newStatus}\"`);\n return;\n }\n\n this.#status = newStatus;\n _console.log({ firmwareStatus: this.#status });\n this.#dispatchEvent(\"firmwareStatus\", { firmwareStatus: this.#status });\n }\n\n // COMMANDS\n\n #images!: FirmwareImage[];\n get images() {\n return this.#images;\n }\n #assertImages() {\n _console.assertWithError(this.#images, \"didn't get imageState\");\n }\n #assertValidImageIndex(imageIndex: number) {\n _console.assertTypeWithError(imageIndex, \"number\");\n _console.assertWithError(imageIndex == 0 || imageIndex == 1, \"imageIndex must be 0 or 1\");\n }\n async getImages() {\n const promise = this.waitForEvent(\"firmwareImages\");\n\n _console.log(\"getting firmware image state...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageState()).buffer);\n\n await promise;\n }\n\n async testImage(imageIndex: number = 1) {\n this.#assertValidImageIndex(imageIndex);\n this.#assertImages();\n if (!this.#images[imageIndex]) {\n _console.log(`image ${imageIndex} not found`);\n return;\n }\n if (this.#images[imageIndex].pending == true) {\n _console.log(`image ${imageIndex} is already pending`);\n return;\n }\n if (this.#images[imageIndex].empty) {\n _console.log(`image ${imageIndex} is empty`);\n return;\n }\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"testing firmware image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageTest(this.#images[imageIndex].hash)).buffer);\n\n await promise;\n }\n\n async eraseImage() {\n this.#assertImages();\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"erasing image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageErase()).buffer);\n\n this.#updateStatus(\"erasing\");\n\n await promise;\n await this.getImages();\n }\n\n async confirmImage(imageIndex: number = 0) {\n this.#assertValidImageIndex(imageIndex);\n this.#assertImages();\n if (this.#images[imageIndex].confirmed === true) {\n _console.log(`image ${imageIndex} is already confirmed`);\n return;\n }\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"confirming image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageConfirm(this.#images[imageIndex].hash)).buffer);\n\n await promise;\n }\n\n async echo(string: string) {\n _console.assertTypeWithError(string, \"string\");\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"sending echo...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.smpEcho(string)).buffer);\n\n await promise;\n }\n\n async reset() {\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"resetting...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdReset()).buffer);\n\n await promise;\n }\n\n // MTU\n #mtu!: number;\n get mtu() {\n return this.#mtu;\n }\n set mtu(newMtu: number) {\n this.#mtu = newMtu;\n this.#mcuManager._mtu = newMtu;\n }\n\n // MCUManager\n #mcuManager = new MCUManager();\n\n #assignMcuManagerCallbacks() {\n this.#mcuManager.onMessage(this.#onMcuMessage.bind(this));\n\n this.#mcuManager.onFileDownloadNext(this.#onMcuFileDownloadNext);\n this.#mcuManager.onFileDownloadProgress(this.#onMcuFileDownloadProgress.bind(this));\n this.#mcuManager.onFileDownloadFinished(this.#onMcuFileDownloadFinished.bind(this));\n\n this.#mcuManager.onFileUploadNext(this.#onMcuFileUploadNext.bind(this));\n this.#mcuManager.onFileUploadProgress(this.#onMcuFileUploadProgress.bind(this));\n this.#mcuManager.onFileUploadFinished(this.#onMcuFileUploadFinished.bind(this));\n\n this.#mcuManager.onImageUploadNext(this.#onMcuImageUploadNext.bind(this));\n this.#mcuManager.onImageUploadProgress(this.#onMcuImageUploadProgress.bind(this));\n this.#mcuManager.onImageUploadFinished(this.#onMcuImageUploadFinished.bind(this));\n }\n\n #onMcuMessage({ op, group, id, data, length }: { op: number; group: number; id: number; data: any; length: number }) {\n _console.log(\"onMcuMessage\", ...arguments);\n\n switch (group) {\n case constants.MGMT_GROUP_ID_OS:\n switch (id) {\n case constants.OS_MGMT_ID_ECHO:\n _console.log(`echo \"${data.r}\"`);\n break;\n case constants.OS_MGMT_ID_TASKSTAT:\n _console.table(data.tasks);\n break;\n case constants.OS_MGMT_ID_MPSTAT:\n _console.log(data);\n break;\n }\n break;\n case constants.MGMT_GROUP_ID_IMAGE:\n switch (id) {\n case constants.IMG_MGMT_ID_STATE:\n this.#onMcuImageState(data);\n }\n break;\n default:\n throw Error(`uncaught mcuMessage group ${group}`);\n }\n }\n\n #onMcuFileDownloadNext() {\n _console.log(\"onMcuFileDownloadNext\", ...arguments);\n }\n #onMcuFileDownloadProgress() {\n _console.log(\"onMcuFileDownloadProgress\", ...arguments);\n }\n #onMcuFileDownloadFinished() {\n _console.log(\"onMcuFileDownloadFinished\", ...arguments);\n }\n\n #onMcuFileUploadNext() {\n _console.log(\"onMcuFileUploadNext\");\n }\n #onMcuFileUploadProgress() {\n _console.log(\"onMcuFileUploadProgress\");\n }\n #onMcuFileUploadFinished() {\n _console.log(\"onMcuFileUploadFinished\");\n }\n\n #onMcuImageUploadNext({ packet }: { packet: number[] }) {\n _console.log(\"onMcuImageUploadNext\");\n this.sendMessage(Uint8Array.from(packet).buffer);\n }\n #onMcuImageUploadProgress({ percentage }: { percentage: number }) {\n const progress = percentage / 100;\n _console.log(\"onMcuImageUploadProgress\", ...arguments);\n this.#dispatchEvent(\"firmwareUploadProgress\", { progress });\n }\n async #onMcuImageUploadFinished() {\n _console.log(\"onMcuImageUploadFinished\", ...arguments);\n\n await this.getImages();\n\n this.#dispatchEvent(\"firmwareUploadProgress\", { progress: 100 });\n this.#dispatchEvent(\"firmwareUploadComplete\", {});\n }\n\n #onMcuImageState({ images }: { images?: FirmwareImage[] }) {\n if (images) {\n this.#images = images;\n _console.log(\"images\", this.#images);\n } else {\n _console.log(\"no images found\");\n return;\n }\n\n let newStatus: FirmwareStatus = \"idle\";\n\n if (this.#images.length == 2) {\n if (!this.#images[1].bootable) {\n _console.warn('Slot 1 has a invalid image. Click \"Erase Image\" to erase it or upload a different image');\n } else if (!this.#images[0].confirmed) {\n _console.log(\n 'Slot 0 has a valid image. Click \"Confirm Image\" to confirm it or wait and the device will swap images back.'\n );\n newStatus = \"testing\";\n } else {\n if (this.#images[1].pending) {\n _console.log(\"reset to upload to the new firmware image\");\n newStatus = \"pending\";\n } else {\n _console.log(\"Slot 1 has a valid image. run testImage() to test it or upload a different image.\");\n newStatus = \"uploaded\";\n }\n }\n }\n\n if (this.#images.length == 1) {\n this.#images.push({\n slot: 1,\n empty: true,\n version: \"Empty\",\n pending: false,\n confirmed: false,\n bootable: false,\n active: false,\n permanent: false,\n });\n\n _console.log(\"Select a firmware upload image to upload to slot 1.\");\n }\n\n this.#updateStatus(newStatus);\n this.#dispatchEvent(\"firmwareImages\", { firmwareImages: this.#images });\n }\n}\n\nexport default FirmwareManager;\n","import { ConnectionStatus } from \"./connection/BaseConnectionManager.ts\";\nimport WebBluetoothConnectionManager from \"./connection/bluetooth/WebBluetoothConnectionManager.ts\";\nimport Device, { BoundDeviceEventListeners, DeviceEventMap } from \"./Device.ts\";\nimport { DeviceType } from \"./InformationManager.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInBluefy, isInBrowser } from \"./utils/environment.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"./utils/EventDispatcher.ts\";\nimport { addEventListeners } from \"./utils/EventUtils.ts\";\n\nconst _console = createConsole(\"DeviceManager\", { log: false });\n\nexport interface LocalStorageDeviceInformation {\n type: DeviceType;\n bluetoothId: string;\n ipAddress?: string;\n isWifiSecure?: boolean;\n}\n\nexport interface LocalStorageConfiguration {\n devices: LocalStorageDeviceInformation[];\n}\n\nexport const DeviceManagerEventTypes = [\n \"deviceConnected\",\n \"deviceDisconnected\",\n \"deviceIsConnected\",\n \"availableDevices\",\n \"connectedDevices\",\n] as const;\nexport type DeviceManagerEventType = (typeof DeviceManagerEventTypes)[number];\n\ninterface DeviceManagerEventMessage {\n device: Device;\n}\nexport interface DeviceManagerEventMessages {\n deviceConnected: DeviceManagerEventMessage;\n deviceDisconnected: DeviceManagerEventMessage;\n deviceIsConnected: DeviceManagerEventMessage;\n availableDevices: { availableDevices: Device[] };\n connectedDevices: { connectedDevices: Device[] };\n}\n\nexport type DeviceManagerEventDispatcher = EventDispatcher<\n DeviceManager,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEventMap = EventMap<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEventListenerMap = EventListenerMap<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEvent = Event<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type BoundDeviceManagerEventListeners = BoundEventListeners<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\n\nclass DeviceManager {\n static readonly shared = new DeviceManager();\n\n constructor() {\n if (DeviceManager.shared && this != DeviceManager.shared) {\n throw Error(\"DeviceManager is a singleton - use DeviceManager.shared\");\n }\n\n if (this.CanUseLocalStorage) {\n this.UseLocalStorage = true;\n }\n }\n\n // DEVICE LISTENERS\n #boundDeviceEventListeners: BoundDeviceEventListeners = {\n getType: this.#onDeviceType.bind(this),\n isConnected: this.#OnDeviceIsConnected.bind(this),\n };\n /** @private */\n onDevice(device: Device) {\n addEventListeners(device, this.#boundDeviceEventListeners);\n }\n\n #onDeviceType(event: DeviceEventMap[\"getType\"]) {\n if (this.#UseLocalStorage) {\n this.#UpdateLocalStorageConfigurationForDevice(event.target);\n }\n }\n\n // CONNECTION STATUS\n /** @private */\n OnDeviceConnectionStatusUpdated(\n device: Device,\n connectionStatus: ConnectionStatus\n ) {\n if (\n connectionStatus == \"notConnected\" &&\n !device.canReconnect &&\n this.#AvailableDevices.includes(device)\n ) {\n const deviceIndex = this.#AvailableDevices.indexOf(device);\n this.AvailableDevices.splice(deviceIndex, 1);\n this.#DispatchAvailableDevices();\n }\n }\n\n // CONNECTED DEVICES\n\n #ConnectedDevices: Device[] = [];\n get ConnectedDevices() {\n return this.#ConnectedDevices;\n }\n\n #UseLocalStorage = false;\n get UseLocalStorage() {\n return this.#UseLocalStorage;\n }\n set UseLocalStorage(newUseLocalStorage) {\n this.#AssertLocalStorage();\n _console.assertTypeWithError(newUseLocalStorage, \"boolean\");\n this.#UseLocalStorage = newUseLocalStorage;\n if (this.#UseLocalStorage && !this.#LocalStorageConfiguration) {\n this.#LoadFromLocalStorage();\n }\n }\n\n #DefaultLocalStorageConfiguration: LocalStorageConfiguration = {\n devices: [],\n };\n #LocalStorageConfiguration?: LocalStorageConfiguration;\n\n get CanUseLocalStorage() {\n return isInBrowser && window.localStorage;\n }\n\n #AssertLocalStorage() {\n _console.assertWithError(\n isInBrowser,\n \"localStorage is only available in the browser\"\n );\n _console.assertWithError(window.localStorage, \"localStorage not found\");\n }\n #LocalStorageKey = \"BS.Device\";\n #SaveToLocalStorage() {\n this.#AssertLocalStorage();\n localStorage.setItem(\n this.#LocalStorageKey,\n JSON.stringify(this.#LocalStorageConfiguration)\n );\n }\n async #LoadFromLocalStorage() {\n this.#AssertLocalStorage();\n let localStorageString = localStorage.getItem(this.#LocalStorageKey);\n if (typeof localStorageString != \"string\") {\n _console.log(\"no info found in localStorage\");\n this.#LocalStorageConfiguration = Object.assign(\n {},\n this.#DefaultLocalStorageConfiguration\n );\n this.#SaveToLocalStorage();\n return;\n }\n try {\n const configuration = JSON.parse(localStorageString);\n _console.log({ configuration });\n this.#LocalStorageConfiguration = configuration;\n if (this.CanGetDevices) {\n await this.GetDevices(); // redundant?\n }\n } catch (error) {\n _console.error(error);\n }\n }\n\n #UpdateLocalStorageConfigurationForDevice(device: Device) {\n if (device.connectionType != \"webBluetooth\") {\n _console.log(\"localStorage is only for webBluetooth devices\");\n return;\n }\n this.#AssertLocalStorage();\n const deviceInformationIndex =\n this.#LocalStorageConfiguration!.devices.findIndex(\n (deviceInformation) => {\n return deviceInformation.bluetoothId == device.bluetoothId;\n }\n );\n if (deviceInformationIndex == -1) {\n return;\n }\n this.#LocalStorageConfiguration!.devices[deviceInformationIndex].type =\n device.type;\n this.#SaveToLocalStorage();\n }\n\n // AVAILABLE DEVICES\n #AvailableDevices: Device[] = [];\n get AvailableDevices() {\n return this.#AvailableDevices;\n }\n\n get CanGetDevices() {\n return isInBrowser && navigator.bluetooth?.getDevices;\n }\n /**\n * retrieves devices already connected via web bluetooth in other tabs/windows\n *\n * _only available on web-bluetooth enabled browsers_\n */\n async GetDevices(): Promise<Device[] | undefined> {\n if (!isInBrowser) {\n _console.warn(\"GetDevices is only available in the browser\");\n return;\n }\n\n if (!navigator.bluetooth) {\n _console.warn(\"bluetooth is not available in this browser\");\n return;\n }\n\n if (isInBluefy) {\n _console.warn(\"bluefy lists too many devices...\");\n return;\n }\n\n if (!navigator.bluetooth.getDevices) {\n _console.warn(\"bluetooth.getDevices() is not available in this browser\");\n return;\n }\n\n if (!this.CanGetDevices) {\n _console.log(\"CanGetDevices is false\");\n return;\n }\n\n if (!this.#LocalStorageConfiguration) {\n this.#LoadFromLocalStorage();\n }\n\n const configuration = this.#LocalStorageConfiguration!;\n if (!configuration.devices || configuration.devices.length == 0) {\n _console.log(\"no devices found in configuration\");\n return;\n }\n\n const bluetoothDevices = await navigator.bluetooth.getDevices();\n\n _console.log({ bluetoothDevices });\n\n bluetoothDevices.forEach((bluetoothDevice) => {\n if (!bluetoothDevice.gatt) {\n return;\n }\n let deviceInformation = configuration.devices.find(\n (deviceInformation) =>\n bluetoothDevice.id == deviceInformation.bluetoothId\n );\n if (!deviceInformation) {\n return;\n }\n\n let existingConnectedDevice = this.ConnectedDevices.filter(\n (device) => device.connectionType == \"webBluetooth\"\n ).find((device) => device.bluetoothId == bluetoothDevice.id);\n\n const existingAvailableDevice = this.AvailableDevices.filter(\n (device) => device.connectionType == \"webBluetooth\"\n ).find((device) => device.bluetoothId == bluetoothDevice.id);\n if (existingAvailableDevice) {\n if (\n existingConnectedDevice &&\n existingConnectedDevice?.bluetoothId ==\n existingAvailableDevice.bluetoothId &&\n existingConnectedDevice != existingAvailableDevice\n ) {\n this.AvailableDevices[\n this.#AvailableDevices.indexOf(existingAvailableDevice)\n ] = existingConnectedDevice;\n }\n return;\n }\n\n if (existingConnectedDevice) {\n this.AvailableDevices.push(existingConnectedDevice);\n return;\n }\n\n const device = new Device();\n const connectionManager = new WebBluetoothConnectionManager();\n connectionManager.device = bluetoothDevice;\n if (bluetoothDevice.name) {\n device._informationManager.updateName(bluetoothDevice.name);\n }\n device._informationManager.updateType(deviceInformation.type);\n device.connectionManager = connectionManager;\n this.AvailableDevices.push(device);\n });\n this.#DispatchAvailableDevices();\n return this.AvailableDevices;\n }\n\n // STATIC EVENTLISTENERS\n\n #EventDispatcher: DeviceManagerEventDispatcher = new EventDispatcher(\n this as DeviceManager,\n DeviceManagerEventTypes\n );\n\n get AddEventListener() {\n return this.#EventDispatcher.addEventListener;\n }\n get #DispatchEvent() {\n return this.#EventDispatcher.dispatchEvent;\n }\n get RemoveEventListener() {\n return this.#EventDispatcher.removeEventListener;\n }\n get RemoveEventListeners() {\n return this.#EventDispatcher.removeEventListeners;\n }\n get RemoveAllEventListeners() {\n return this.#EventDispatcher.removeAllEventListeners;\n }\n\n #OnDeviceIsConnected(event: DeviceEventMap[\"isConnected\"]) {\n const { target: device } = event;\n if (device.isConnected) {\n if (!this.#ConnectedDevices.includes(device)) {\n _console.log(\"adding device\", device);\n this.#ConnectedDevices.push(device);\n if (this.UseLocalStorage && device.connectionType == \"webBluetooth\") {\n const deviceInformation: LocalStorageDeviceInformation = {\n type: device.type,\n bluetoothId: device.bluetoothId!,\n ipAddress: device.ipAddress,\n isWifiSecure: device.isWifiSecure,\n };\n const deviceInformationIndex =\n this.#LocalStorageConfiguration!.devices.findIndex(\n (_deviceInformation) =>\n _deviceInformation.bluetoothId == deviceInformation.bluetoothId\n );\n if (deviceInformationIndex == -1) {\n this.#LocalStorageConfiguration!.devices.push(deviceInformation);\n } else {\n this.#LocalStorageConfiguration!.devices[deviceInformationIndex] =\n deviceInformation;\n }\n this.#SaveToLocalStorage();\n }\n this.#DispatchEvent(\"deviceConnected\", { device });\n this.#DispatchEvent(\"deviceIsConnected\", { device });\n this.#DispatchConnectedDevices();\n } else {\n _console.log(\"device already included\");\n }\n } else {\n if (this.#ConnectedDevices.includes(device)) {\n _console.log(\"removing device\", device);\n this.#ConnectedDevices.splice(\n this.#ConnectedDevices.indexOf(device),\n 1\n );\n this.#DispatchEvent(\"deviceDisconnected\", { device });\n this.#DispatchEvent(\"deviceIsConnected\", { device });\n this.#DispatchConnectedDevices();\n } else {\n _console.log(\"device already not included\");\n }\n }\n if (this.CanGetDevices) {\n this.GetDevices();\n }\n if (device.isConnected && !this.AvailableDevices.includes(device)) {\n const existingAvailableDevice = this.AvailableDevices.find(\n (_device) => _device.bluetoothId == device.bluetoothId\n );\n _console.log({ existingAvailableDevice });\n if (existingAvailableDevice) {\n this.AvailableDevices[\n this.AvailableDevices.indexOf(existingAvailableDevice)\n ] = device;\n } else {\n this.AvailableDevices.push(device);\n }\n this.#DispatchAvailableDevices();\n }\n this._CheckDeviceAvailability(device);\n }\n\n _CheckDeviceAvailability(device: Device) {\n if (\n !device.isConnected &&\n !device.isAvailable &&\n this.#AvailableDevices.includes(device)\n ) {\n _console.log(\"removing device from availableDevices...\");\n this.#AvailableDevices.splice(this.#AvailableDevices.indexOf(device), 1);\n this.#DispatchAvailableDevices();\n }\n }\n\n #DispatchAvailableDevices() {\n _console.log({ AvailableDevices: this.AvailableDevices });\n this.#DispatchEvent(\"availableDevices\", {\n availableDevices: this.AvailableDevices,\n });\n }\n #DispatchConnectedDevices() {\n _console.log({ ConnectedDevices: this.ConnectedDevices });\n this.#DispatchEvent(\"connectedDevices\", {\n connectedDevices: this.ConnectedDevices,\n });\n }\n}\n\nexport default DeviceManager.shared;\n","import { DeviceEventTypes } from \"../Device.ts\";\nimport {\n ConnectionMessageType,\n ConnectionMessageTypes,\n} from \"../connection/BaseConnectionManager.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { DeviceEventType } from \"../Device.ts\";\n\nconst _console = createConsole(\"ServerUtils\", { log: false });\n\nexport const ServerMessageTypes = [\n \"isScanningAvailable\",\n \"isScanning\",\n \"startScan\",\n \"stopScan\",\n \"discoveredDevice\",\n \"discoveredDevices\",\n \"expiredDiscoveredDevice\",\n \"connectToDevice\",\n \"disconnectFromDevice\",\n \"connectedDevices\",\n \"deviceMessage\",\n \"requiredDeviceInformation\",\n] as const;\nexport type ServerMessageType = (typeof ServerMessageTypes)[number];\n\nexport const DeviceMessageTypes = [\n \"connectionStatus\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"rx\",\n \"smp\",\n] as const;\nexport type DeviceMessageType = (typeof DeviceMessageTypes)[number];\n\n// MESSAGING\n\nexport type MessageLike =\n | number\n | number[]\n | ArrayBufferLike\n | DataView\n | boolean\n | string\n | any;\n\nexport interface Message<MessageType extends string> {\n type: MessageType;\n data?: MessageLike | MessageLike[];\n}\n\nexport function createMessage<MessageType extends string>(\n enumeration: readonly MessageType[],\n ...messages: (Message<MessageType> | MessageType)[]\n) {\n _console.log(\"createMessage\", ...messages);\n\n const messageBuffers = messages.map((message) => {\n if (typeof message == \"string\") {\n message = { type: message };\n }\n\n if (message.data != undefined) {\n if (!Array.isArray(message.data)) {\n message.data = [message.data];\n }\n } else {\n message.data = [];\n }\n\n const messageDataArrayBuffer = concatenateArrayBuffers(...message.data);\n const messageDataArrayBufferByteLength = messageDataArrayBuffer.byteLength;\n\n _console.assertEnumWithError(message.type, enumeration);\n const messageTypeEnum = enumeration.indexOf(message.type);\n\n const messageDataLengthDataView = new DataView(new ArrayBuffer(2));\n messageDataLengthDataView.setUint16(\n 0,\n messageDataArrayBufferByteLength,\n true\n );\n\n return concatenateArrayBuffers(\n messageTypeEnum,\n messageDataLengthDataView,\n messageDataArrayBuffer\n );\n });\n _console.log(\"messageBuffers\", ...messageBuffers);\n return concatenateArrayBuffers(...messageBuffers);\n}\n\nexport type ServerMessage = ServerMessageType | Message<ServerMessageType>;\nexport function createServerMessage(...messages: ServerMessage[]) {\n _console.log(\"createServerMessage\", ...messages);\n return createMessage(ServerMessageTypes, ...messages);\n}\n\nexport type DeviceMessage = DeviceEventType | Message<DeviceEventType>;\nexport function createDeviceMessage(...messages: DeviceMessage[]) {\n _console.log(\"createDeviceMessage\", ...messages);\n return createMessage(DeviceEventTypes, ...messages);\n}\n\nexport type ClientDeviceMessage =\n | ConnectionMessageType\n | Message<ConnectionMessageType>;\nexport function createClientDeviceMessage(...messages: ClientDeviceMessage[]) {\n _console.log(\"createClientDeviceMessage\", ...messages);\n return createMessage(ConnectionMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const isScanningAvailableRequestMessage = createServerMessage(\n \"isScanningAvailable\"\n);\nexport const isScanningRequestMessage = createServerMessage(\"isScanning\");\nexport const startScanRequestMessage = createServerMessage(\"startScan\");\nexport const stopScanRequestMessage = createServerMessage(\"stopScan\");\nexport const discoveredDevicesMessage =\n createServerMessage(\"discoveredDevices\");\n","import { createConsole } from \"../../utils/Console.ts\";\nimport { createMessage, Message } from \"../ServerUtils.ts\";\n\nconst _console = createConsole(\"WebSocketUtils\", { log: false });\n\nexport const webSocketPingTimeout = 30_000;\nexport const webSocketReconnectTimeout = 3_000;\n\nexport const WebSocketMessageTypes = [\"ping\", \"pong\", \"serverMessage\"] as const;\nexport type WebSocketMessageType = (typeof WebSocketMessageTypes)[number];\n\nexport type WebSocketMessage =\n | WebSocketMessageType\n | Message<WebSocketMessageType>;\nexport function createWebSocketMessage(...messages: WebSocketMessage[]) {\n _console.log(\"createWebSocketMessage\", ...messages);\n return createMessage(WebSocketMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const webSocketPingMessage = createWebSocketMessage(\"ping\");\nexport const webSocketPongMessage = createWebSocketMessage(\"pong\");\n","import { DeviceInformationTypes } from \"../../DeviceInformationManager.ts\";\nimport {\n createMessage,\n Message,\n MessageLike,\n} from \"../../server/ServerUtils.ts\";\nimport { webSocketPingTimeout } from \"../../server/websocket/WebSocketUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseConnectionManager, {\n ConnectionType,\n} from \"../BaseConnectionManager.ts\";\nimport type * as ws from \"ws\";\n\nconst _console = createConsole(\"WebSocketConnectionManager\", { log: false });\n\nconst WebSocketMessageTypes = [\n \"ping\",\n \"pong\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"message\",\n] as const;\ntype WebSocketMessageType = (typeof WebSocketMessageTypes)[number];\n\ntype WebSocketMessage = WebSocketMessageType | Message<WebSocketMessageType>;\nfunction createWebSocketMessage(...messages: WebSocketMessage[]) {\n _console.log(\"createWebSocketMessage\", ...messages);\n return createMessage(WebSocketMessageTypes, ...messages);\n}\n\nconst WebSocketDeviceInformationMessageTypes: WebSocketMessageType[] = [\n \"deviceInformation\",\n \"batteryLevel\",\n];\n\nclass WebSocketConnectionManager extends BaseConnectionManager {\n #bluetoothId?: string;\n get bluetoothId() {\n return this.#bluetoothId ?? \"\";\n }\n\n defaultMtu = 2 ** 10;\n\n constructor(\n ipAddress: string,\n isSecure: boolean = false,\n bluetoothId?: string\n ) {\n super();\n this.ipAddress = ipAddress;\n this.isSecure = isSecure;\n this.mtu = this.defaultMtu;\n this.#bluetoothId = bluetoothId;\n }\n\n get isAvailable() {\n return true;\n }\n\n static get isSupported() {\n return true;\n }\n static get type(): ConnectionType {\n return \"webSocket\";\n }\n\n // WEBSOCKET\n #webSocket?: WebSocket;\n get webSocket() {\n return this.#webSocket;\n }\n set webSocket(newWebSocket) {\n if (this.#webSocket == newWebSocket) {\n _console.log(\"redundant webSocket assignment\");\n return;\n }\n\n _console.log(\"assigning webSocket\", newWebSocket);\n\n if (this.#webSocket) {\n removeEventListeners(this.#webSocket, this.#boundWebSocketEventListeners);\n if (this.#webSocket.readyState == this.#webSocket.OPEN) {\n this.#webSocket.close();\n }\n }\n\n if (newWebSocket) {\n addEventListeners(newWebSocket, this.#boundWebSocketEventListeners);\n }\n this.#webSocket = newWebSocket;\n\n _console.log(\"assigned webSocket\");\n }\n\n // IP ADDRESS\n #ipAddress!: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n set ipAddress(newIpAddress) {\n this.assertIsNotConnected();\n if (this.#ipAddress == newIpAddress) {\n _console.log(`redundnant ipAddress assignment \"${newIpAddress}\"`);\n return;\n }\n this.#ipAddress = newIpAddress;\n _console.log(`updated ipAddress to \"${this.ipAddress}\"`);\n }\n\n // IS SECURE\n #isSecure = false;\n get isSecure() {\n return this.#isSecure;\n }\n set isSecure(newIsSecure) {\n this.assertIsNotConnected();\n if (this.#isSecure == newIsSecure) {\n _console.log(`redundant isSecure assignment ${newIsSecure}`);\n return;\n }\n this.#isSecure = newIsSecure;\n _console.log(`updated isSecure to \"${this.isSecure}\"`);\n }\n\n // URL\n get url() {\n return `${this.isSecure ? \"wss\" : \"ws\"}://${this.ipAddress}/ws`;\n }\n\n // CONNECTION\n async connect() {\n const canContinue = await super.connect();\n if (!canContinue) {\n return false;\n }\n try {\n this.webSocket = new WebSocket(this.url);\n return true;\n } catch (error) {\n _console.error(\"error connecting to webSocket\", error);\n this.status = \"notConnected\";\n return false;\n }\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n _console.log(\"closing websocket\");\n this.#pingTimer.stop();\n this.#webSocket?.close();\n return true;\n }\n\n get canReconnect() {\n return Boolean(this.webSocket);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n this.webSocket = new WebSocket(this.url);\n return true;\n }\n\n // BASE CONNECTION MANAGER\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n _console.error(\"smp not supported on webSockets\");\n }\n\n async sendTxData(data: ArrayBuffer) {\n await super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n this.#sendWebSocketMessage({ type: \"message\", data });\n }\n\n // WEBSOCKET MESSAGING\n #sendMessage(message: MessageLike) {\n this.assertIsConnected();\n _console.log(\"sending webSocket message\", message);\n this.#webSocket!.send(message);\n this.#pingTimer.restart();\n }\n\n #sendWebSocketMessage(...messages: WebSocketMessage[]) {\n this.#sendMessage(createWebSocketMessage(...messages));\n }\n\n // WEBSOCKET EVENTS\n #boundWebSocketEventListeners: { [eventType: string]: Function } = {\n open: this.#onWebSocketOpen.bind(this),\n message: this.#onWebSocketMessage.bind(this),\n close: this.#onWebSocketClose.bind(this),\n error: this.#onWebSocketError.bind(this),\n };\n\n #onWebSocketOpen(event: ws.Event) {\n _console.log(\"webSocket.open\", event);\n this.#pingTimer.start();\n this.status = \"connected\";\n this.#requestDeviceInformation();\n }\n async #onWebSocketMessage(event: ws.MessageEvent) {\n // this.#pingTimer.restart();\n //@ts-expect-error\n const arrayBuffer = await event.data.arrayBuffer();\n const dataView = new DataView(arrayBuffer);\n _console.log(`webSocket.message (${dataView.byteLength} bytes)`);\n this.#parseWebSocketMessage(dataView);\n }\n #onWebSocketClose(event: ws.CloseEvent) {\n _console.log(\"webSocket.close\", event);\n this.status = \"notConnected\";\n this.#pingTimer.stop();\n }\n #onWebSocketError(event: ws.ErrorEvent) {\n _console.error(\"webSocket.error\", event);\n }\n\n // PARSING\n #parseWebSocketMessage(dataView: DataView) {\n parseMessage(\n dataView,\n WebSocketMessageTypes,\n this.#onMessage.bind(this),\n null,\n true\n );\n }\n\n #onMessage(messageType: WebSocketMessageType, dataView: DataView) {\n _console.log(\n `received \"${messageType}\" message (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"ping\":\n this.#pong();\n break;\n case \"pong\":\n break;\n case \"batteryLevel\":\n this.onMessageReceived?.(\"batteryLevel\", dataView);\n break;\n case \"deviceInformation\":\n parseMessage(\n dataView,\n DeviceInformationTypes,\n (deviceInformationType, dataView) => {\n this.onMessageReceived!(deviceInformationType, dataView);\n }\n );\n break;\n case \"message\":\n this.parseRxMessage(dataView);\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // PING\n #pingTimer = new Timer(this.#ping.bind(this), webSocketPingTimeout - 1_000);\n #ping() {\n _console.log(\"pinging\");\n this.#sendWebSocketMessage(\"ping\");\n }\n #pong() {\n _console.log(\"ponging\");\n this.#sendWebSocketMessage(\"pong\");\n }\n\n // DEVICE INFORMATION\n #requestDeviceInformation() {\n this.#sendWebSocketMessage(...WebSocketDeviceInformationMessageTypes);\n }\n\n remove() {\n super.remove();\n this.webSocket = undefined;\n }\n}\n\nexport default WebSocketConnectionManager;\n","import { DeviceInformationTypes } from \"../../DeviceInformationManager.ts\";\nimport {\n createMessage,\n Message,\n MessageLike,\n} from \"../../server/ServerUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport { isInNode } from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseConnectionManager, {\n ConnectionType,\n} from \"../BaseConnectionManager.ts\";\n\nimport * as dgram from \"dgram\";\n\nconst _console = createConsole(\"UDPConnectionManager\", { log: false });\n\nexport const UDPSendPort = 3000;\n\nexport const UDPPingInterval = 2_000;\n\nconst SocketMessageTypes = [\n \"ping\",\n \"pong\",\n \"setRemoteReceivePort\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"message\",\n] as const;\ntype SocketMessageType = (typeof SocketMessageTypes)[number];\n\ntype SocketMessage = SocketMessageType | Message<SocketMessageType>;\nfunction createSocketMessage(...messages: SocketMessage[]) {\n _console.log(\"createSocketMessage\", ...messages);\n return createMessage(SocketMessageTypes, ...messages);\n}\n\nconst SocketDeviceInformationMessageTypes: SocketMessageType[] = [\n \"deviceInformation\",\n \"batteryLevel\",\n];\n\nclass UDPConnectionManager extends BaseConnectionManager {\n #bluetoothId?: string;\n get bluetoothId() {\n return this.#bluetoothId ?? \"\";\n }\n\n defaultMtu = 2 ** 10;\n\n constructor(ipAddress: string, bluetoothId?: string, receivePort?: number) {\n super();\n this.ipAddress = ipAddress;\n this.mtu = this.defaultMtu;\n this.#bluetoothId = bluetoothId;\n if (receivePort) {\n this.receivePort = receivePort;\n }\n }\n\n get isAvailable() {\n return true;\n }\n static get isSupported() {\n return isInNode;\n }\n static get type(): ConnectionType {\n return \"udp\";\n }\n\n // IP ADDRESS\n #ipAddress!: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n set ipAddress(newIpAddress) {\n this.assertIsNotConnected();\n if (this.#ipAddress == newIpAddress) {\n _console.log(`redundnant ipAddress assignment \"${newIpAddress}\"`);\n return;\n }\n this.#ipAddress = newIpAddress;\n _console.log(`updated ipAddress to \"${this.ipAddress}\"`);\n }\n\n // RECEIVE PORT\n #receivePort?: number;\n get receivePort() {\n return this.#receivePort;\n }\n set receivePort(newReceivePort) {\n this.assertIsNotConnected();\n if (this.#receivePort == newReceivePort) {\n _console.log(`redundnant receivePort assignment ${newReceivePort}`);\n return;\n }\n this.#receivePort = newReceivePort;\n _console.log(`updated receivePort to ${this.#receivePort}`);\n if (this.#receivePort) {\n this.#setRemoteReceivePortDataView.setUint16(0, this.#receivePort, true);\n }\n }\n\n // SET REMOTE RECEIVE PORT\n #didSetRemoteReceivePort = false;\n #setRemoteReceivePortDataView = new DataView(new ArrayBuffer(2));\n #parseReceivePort(dataView: DataView) {\n const parsedReceivePort = dataView.getUint16(0, true);\n if (parsedReceivePort != this.receivePort) {\n _console.error(\n `incorrect receivePort (expected ${this.receivePort}, got ${parsedReceivePort})`\n );\n return;\n }\n this.#didSetRemoteReceivePort = true;\n }\n\n // SOCKET\n #socket?: dgram.Socket;\n get socket() {\n return this.#socket;\n }\n set socket(newSocket) {\n if (this.#socket == newSocket) {\n _console.log(\"redundant socket assignment\");\n return;\n }\n\n _console.log(\"assigning socket\", newSocket);\n\n if (this.#socket) {\n _console.log(\"removing existing socket...\");\n removeEventListeners(this.#socket, this.#boundSocketEventListeners);\n try {\n this.#socket.close();\n } catch (error) {\n _console.error(error);\n }\n }\n\n if (newSocket) {\n addEventListeners(newSocket, this.#boundSocketEventListeners);\n }\n this.#socket = newSocket;\n\n _console.log(\"assigned socket\");\n }\n\n // SOCKET MESSAGING\n #sendMessage(message: MessageLike) {\n // this.assertIsConnected();\n _console.log(\"sending socket message\", message);\n const dataView = Buffer.from(message);\n this.#socket!.send(dataView);\n this.#pingTimer.restart();\n }\n\n #sendSocketMessage(...messages: SocketMessage[]) {\n this.#sendMessage(createSocketMessage(...messages));\n }\n\n // BASE CONNECTION MANAGER\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n _console.error(\"smp not supported on udp\");\n }\n\n async sendTxData(data: ArrayBuffer) {\n super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n this.#sendSocketMessage({ type: \"message\", data });\n }\n\n // SOCKET EVENTS\n #boundSocketEventListeners: { [eventType: string]: Function } = {\n close: this.#onSocketClose.bind(this),\n connect: this.#onSocketConnect.bind(this),\n error: this.#onSocketError.bind(this),\n listening: this.#onSocketListening.bind(this),\n message: this.#onSocketMessage.bind(this),\n };\n\n #onSocketClose() {\n _console.log(\"socket.close\");\n this.status = \"notConnected\";\n this.clear();\n }\n #onSocketConnect() {\n _console.log(\"socket.connect\");\n this.#pingTimer.start(true);\n }\n #onSocketError(error: Error) {\n _console.error(\"socket.error\", error);\n }\n #onSocketListening() {\n const address = this.socket!.address();\n _console.log(`socket.listening on ${address.address}:${address.port}`);\n this.receivePort = address.port;\n this.socket!.connect(UDPSendPort, this.ipAddress);\n }\n #onSocketMessage(message: Buffer, remoteInfo: dgram.RemoteInfo) {\n this.#pongTimeoutTimer.stop();\n _console.log(\"socket.message\", message.byteLength, remoteInfo);\n const arrayBuffer = message.buffer.slice(\n message.byteOffset,\n message.byteOffset + message.byteLength\n );\n const dataView = new DataView(arrayBuffer);\n this.#parseSocketMessage(dataView);\n\n if (this.status == \"connecting\" && this.#didSetRemoteReceivePort) {\n this.status = \"connected\";\n this.#requestDeviceInformation();\n }\n }\n\n #setupSocket() {\n this.#didSetRemoteReceivePort = false;\n this.socket = dgram.createSocket({\n type: \"udp4\",\n });\n try {\n if (this.receivePort) {\n this.socket.bind(this.receivePort);\n } else {\n this.socket.bind();\n }\n } catch (error) {\n _console.error(error);\n this.disconnect();\n }\n }\n\n // CONNECTION\n async connect() {\n const canContinue = await super.connect();\n if (!canContinue) {\n return false;\n }\n this.#setupSocket();\n return true;\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n _console.log(\"closing socket\");\n this.#pingTimer.stop();\n try {\n this.#socket?.close();\n return true;\n } catch (error) {\n _console.error(error);\n return false;\n }\n }\n\n get canReconnect() {\n return Boolean(this.socket);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n this.#setupSocket();\n return true;\n }\n\n // PARSING\n #parseSocketMessage(dataView: DataView) {\n parseMessage(\n dataView,\n SocketMessageTypes,\n this.#onMessage.bind(this),\n null,\n true\n );\n }\n\n #onMessage(messageType: SocketMessageType, dataView: DataView) {\n _console.log(\n `received \"${messageType}\" message (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"ping\":\n this.#pong();\n break;\n case \"pong\":\n break;\n case \"setRemoteReceivePort\":\n this.#parseReceivePort(dataView);\n break;\n case \"batteryLevel\":\n this.onMessageReceived?.(\"batteryLevel\", dataView);\n break;\n case \"deviceInformation\":\n parseMessage(\n dataView,\n DeviceInformationTypes,\n (deviceInformationType, dataView) => {\n this.onMessageReceived!(deviceInformationType, dataView);\n }\n );\n break;\n case \"message\":\n this.parseRxMessage(dataView);\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // PING\n #pingTimer = new Timer(this.#ping.bind(this), UDPPingInterval);\n #ping() {\n _console.log(\"pinging\");\n if (this.#didSetRemoteReceivePort || !this.#receivePort) {\n this.#sendSocketMessage(\"ping\");\n } else {\n this.#sendSocketMessage({\n type: \"setRemoteReceivePort\",\n data: this.#setRemoteReceivePortDataView,\n });\n }\n if (this.isConnected) {\n this.#pongTimeoutTimer.start();\n }\n }\n #pong() {\n _console.log(\"ponging\");\n this.#sendSocketMessage(\"pong\");\n }\n\n #pongTimeout() {\n this.#pongTimeoutTimer.stop();\n _console.log(\"pong timeout\");\n this.disconnect();\n }\n #pongTimeoutTimer = new Timer(() => this.#pongTimeout(), 1_000);\n\n // DEVICE INFORMATION\n #requestDeviceInformation() {\n this.#sendSocketMessage(...SocketDeviceInformationMessageTypes);\n }\n\n clear() {\n super.clear();\n this.#didSetRemoteReceivePort = false;\n this.#pingTimer.stop();\n this.#pongTimeoutTimer.stop();\n }\n\n remove() {\n super.remove();\n this.socket = undefined;\n }\n}\n\nexport default UDPConnectionManager;\n","import { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"./utils/EventDispatcher.ts\";\nimport BaseConnectionManager, {\n TxMessage,\n TxRxMessageType,\n ConnectionStatus,\n ConnectionMessageType,\n MetaConnectionMessageTypes,\n BatteryLevelMessageTypes,\n ConnectionEventTypes,\n ConnectionStatusEventMessages,\n ConnectOptions,\n} from \"./connection/BaseConnectionManager.ts\";\nimport { isInBrowser, isInNode } from \"./utils/environment.ts\";\nimport WebBluetoothConnectionManager from \"./connection/bluetooth/WebBluetoothConnectionManager.ts\";\nimport SensorConfigurationManager, {\n SendSensorConfigurationMessageCallback,\n SensorConfiguration,\n SensorConfigurationEventDispatcher,\n SensorConfigurationEventMessages,\n SensorConfigurationEventTypes,\n SensorConfigurationMessageType,\n SensorConfigurationMessageTypes,\n} from \"./sensor/SensorConfigurationManager.ts\";\nimport SensorDataManager, {\n SensorDataEventMessages,\n SensorDataEventTypes,\n SensorDataMessageType,\n SensorDataMessageTypes,\n SensorType,\n ContinuousSensorTypes,\n SensorDataEventDispatcher,\n RequiredPressureMessageTypes,\n} from \"./sensor/SensorDataManager.ts\";\nimport VibrationManager, {\n SendVibrationMessageCallback,\n VibrationConfiguration,\n VibrationEventDispatcher,\n VibrationEventTypes,\n VibrationMessageType,\n VibrationMessageTypes,\n} from \"./vibration/VibrationManager.ts\";\nimport FileTransferManager, {\n FileTransferEventTypes,\n FileTransferEventMessages,\n FileTransferEventDispatcher,\n SendFileTransferMessageCallback,\n FileTransferMessageTypes,\n FileTransferMessageType,\n FileType,\n FileTypes,\n RequiredFileTransferMessageTypes,\n SendFileCallback,\n} from \"./FileTransferManager.ts\";\nimport TfliteManager, {\n TfliteEventTypes,\n TfliteEventMessages,\n TfliteEventDispatcher,\n SendTfliteMessageCallback,\n TfliteMessageTypes,\n TfliteMessageType,\n TfliteSensorTypes,\n TfliteFileConfiguration,\n TfliteSensorType,\n RequiredTfliteMessageTypes,\n} from \"./TfliteManager.ts\";\nimport FirmwareManager, {\n FirmwareEventDispatcher,\n FirmwareEventMessages,\n FirmwareEventTypes,\n FirmwareMessageType,\n FirmwareMessageTypes,\n} from \"./FirmwareManager.ts\";\nimport DeviceInformationManager, {\n DeviceInformationEventDispatcher,\n DeviceInformationEventTypes,\n DeviceInformationType,\n DeviceInformationTypes,\n DeviceInformationEventMessages,\n} from \"./DeviceInformationManager.ts\";\nimport InformationManager, {\n DeviceType,\n InformationEventDispatcher,\n InformationEventTypes,\n InformationMessageType,\n InformationMessageTypes,\n InformationEventMessages,\n SendInformationMessageCallback,\n} from \"./InformationManager.ts\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport DeviceManager from \"./DeviceManager.ts\";\nimport CameraManager, {\n CameraEventDispatcher,\n CameraEventMessages,\n CameraEventTypes,\n CameraMessageType,\n CameraMessageTypes,\n RequiredCameraMessageTypes,\n SendCameraMessageCallback,\n} from \"./CameraManager.ts\";\nimport MicrophoneManager, {\n MicrophoneEventDispatcher,\n MicrophoneEventMessages,\n MicrophoneEventTypes,\n MicrophoneMessageType,\n MicrophoneMessageTypes,\n RequiredMicrophoneMessageTypes,\n SendMicrophoneMessageCallback,\n} from \"./MicrophoneManager.ts\";\nimport DisplayManager, {\n DisplayEventDispatcher,\n DisplayEventMessages,\n DisplayEventTypes,\n DisplayMessageType,\n DisplayMessageTypes,\n RequiredDisplayMessageTypes,\n SendDisplayMessageCallback,\n} from \"./DisplayManager.ts\";\nimport WifiManager, {\n RequiredWifiMessageTypes,\n SendWifiMessageCallback,\n WifiEventDispatcher,\n WifiEventMessages,\n WifiEventTypes,\n WifiMessageType,\n WifiMessageTypes,\n} from \"./WifiManager.ts\";\nimport WebSocketConnectionManager from \"./connection/websocket/WebSocketConnectionManager.ts\";\nimport ClientConnectionManager from \"./connection/ClientConnectionManager.ts\";\n\n/** NODE_START */\nimport UDPConnectionManager from \"./connection/udp/UDPConnectionManager.ts\";\nimport { DisplayManagerInterface } from \"./utils/DisplayManagerInterface.ts\";\n/** NODE_END */\n\nconst _console = createConsole(\"Device\", { log: false });\n\nexport const DeviceEventTypes = [\n \"connectionMessage\",\n ...ConnectionEventTypes,\n ...MetaConnectionMessageTypes,\n ...BatteryLevelMessageTypes,\n ...InformationEventTypes,\n ...DeviceInformationEventTypes,\n ...SensorConfigurationEventTypes,\n ...SensorDataEventTypes,\n ...VibrationEventTypes,\n ...FileTransferEventTypes,\n ...TfliteEventTypes,\n ...WifiEventTypes,\n ...CameraEventTypes,\n ...MicrophoneEventTypes,\n ...DisplayEventTypes,\n ...FirmwareEventTypes,\n] as const;\nexport type DeviceEventType = (typeof DeviceEventTypes)[number];\n\nexport interface DeviceEventMessages\n extends ConnectionStatusEventMessages,\n DeviceInformationEventMessages,\n InformationEventMessages,\n SensorDataEventMessages,\n SensorConfigurationEventMessages,\n TfliteEventMessages,\n FileTransferEventMessages,\n WifiEventMessages,\n CameraEventMessages,\n MicrophoneEventMessages,\n DisplayEventMessages,\n FirmwareEventMessages {\n batteryLevel: { batteryLevel: number };\n connectionMessage: { messageType: ConnectionMessageType; dataView: DataView };\n}\n\nexport type SendMessageCallback<MessageType extends string> = (\n messages?: { type: MessageType; data?: ArrayBuffer }[],\n sendImmediately?: boolean\n) => Promise<void>;\n\nexport type SendSmpMessageCallback = (data: ArrayBuffer) => Promise<void>;\n\nexport type DeviceEventDispatcher = EventDispatcher<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type DeviceEvent = Event<Device, DeviceEventType, DeviceEventMessages>;\nexport type DeviceEventMap = EventMap<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type DeviceEventListenerMap = EventListenerMap<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type BoundDeviceEventListeners = BoundEventListeners<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\n\nexport const RequiredInformationConnectionMessages: TxRxMessageType[] = [\n \"isCharging\",\n \"getBatteryCurrent\",\n \"getId\",\n \"getMtu\",\n\n \"getName\",\n \"getType\",\n \"getCurrentTime\",\n \"getSensorConfiguration\",\n \"getSensorScalars\",\n\n \"getVibrationLocations\",\n\n \"getFileTypes\",\n\n \"isWifiAvailable\",\n];\n\nclass Device {\n get bluetoothId() {\n return this.#connectionManager?.bluetoothId;\n }\n\n get isAvailable() {\n return this.#connectionManager?.isAvailable;\n }\n\n constructor() {\n this.#deviceInformationManager.eventDispatcher = this\n .#eventDispatcher as DeviceInformationEventDispatcher;\n\n this._informationManager.sendMessage = this\n .sendTxMessages as SendInformationMessageCallback;\n this._informationManager.eventDispatcher = this\n .#eventDispatcher as InformationEventDispatcher;\n\n this.#sensorConfigurationManager.sendMessage = this\n .sendTxMessages as SendSensorConfigurationMessageCallback;\n this.#sensorConfigurationManager.eventDispatcher = this\n .#eventDispatcher as SensorConfigurationEventDispatcher;\n\n this.#sensorDataManager.eventDispatcher = this\n .#eventDispatcher as SensorDataEventDispatcher;\n\n this.#vibrationManager.sendMessage = this\n .sendTxMessages as SendVibrationMessageCallback;\n this.#vibrationManager.eventDispatcher = this\n .#eventDispatcher as VibrationEventDispatcher;\n\n this.#tfliteManager.sendMessage = this\n .sendTxMessages as SendTfliteMessageCallback;\n this.#tfliteManager.eventDispatcher = this\n .#eventDispatcher as TfliteEventDispatcher;\n\n this.#fileTransferManager.sendMessage = this\n .sendTxMessages as SendFileTransferMessageCallback;\n this.#fileTransferManager.eventDispatcher = this\n .#eventDispatcher as FileTransferEventDispatcher;\n\n this.#wifiManager.sendMessage = this\n .sendTxMessages as SendWifiMessageCallback;\n this.#wifiManager.eventDispatcher = this\n .#eventDispatcher as WifiEventDispatcher;\n\n this.#cameraManager.sendMessage = this\n .sendTxMessages as SendCameraMessageCallback;\n this.#cameraManager.eventDispatcher = this\n .#eventDispatcher as CameraEventDispatcher;\n\n this.#microphoneManager.sendMessage = this\n .sendTxMessages as SendMicrophoneMessageCallback;\n this.#microphoneManager.eventDispatcher = this\n .#eventDispatcher as MicrophoneEventDispatcher;\n\n this.#displayManager.sendMessage = this\n .sendTxMessages as SendDisplayMessageCallback;\n this.#displayManager.eventDispatcher = this\n .#eventDispatcher as DisplayEventDispatcher;\n this.#displayManager.sendFile = this.#fileTransferManager\n .send as SendFileCallback;\n\n this.#firmwareManager.sendMessage = this\n .sendSmpMessage as SendSmpMessageCallback;\n this.#firmwareManager.eventDispatcher = this\n .#eventDispatcher as FirmwareEventDispatcher;\n\n this.addEventListener(\"getMtu\", () => {\n _console.log(\"updating mtu...\");\n this.#firmwareManager.mtu = this.mtu;\n this.#fileTransferManager.mtu = this.mtu;\n this.connectionManager!.mtu = this.mtu;\n this.#displayManager.mtu = this.mtu;\n });\n this.addEventListener(\"getSensorConfiguration\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.sensorTypes.includes(\"pressure\")) {\n _console.log(\"requesting required pressure information\");\n const messages = RequiredPressureMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request pressure infomration\");\n }\n\n if (this.sensorTypes.includes(\"camera\")) {\n _console.log(\"requesting required camera information\");\n const messages = RequiredCameraMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request camera infomration\");\n }\n\n if (this.sensorTypes.includes(\"microphone\")) {\n _console.log(\"requesting required microphone information\");\n const messages = RequiredMicrophoneMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request microphone infomration\");\n }\n });\n this.addEventListener(\"getFileTypes\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.fileTypes.length > 0) {\n this.#fileTransferManager.requestRequiredInformation();\n }\n if (this.fileTypes.includes(\"tflite\")) {\n this.#tfliteManager.requestRequiredInformation();\n }\n });\n this.addEventListener(\"isWifiAvailable\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.connectionType == \"client\" && !isInNode) {\n return;\n }\n if (this.isWifiAvailable) {\n if (this.connectionType != \"client\") {\n this.#wifiManager.requestRequiredInformation();\n }\n }\n });\n this.addEventListener(\"getType\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.type == \"glasses\") {\n this.#displayManager.requestRequiredInformation();\n }\n });\n this.addEventListener(\"fileTransferProgress\", (event) => {\n const { fileType, progress } = event.message;\n switch (fileType) {\n case \"spriteSheet\":\n this.#dispatchEvent(\"displaySpriteSheetUploadProgress\", {\n spriteSheet: this.#displayManager.pendingSpriteSheet!,\n spriteSheetName: this.#displayManager.pendingSpriteSheetName!,\n progress,\n });\n break;\n default:\n break;\n }\n });\n this.addEventListener(\"fileTransferStatus\", (event) => {\n const { fileType, fileTransferStatus } = event.message;\n switch (fileType) {\n case \"spriteSheet\":\n if (fileTransferStatus == \"sending\") {\n this.#dispatchEvent(\"displaySpriteSheetUploadStart\", {\n spriteSheet: this.#displayManager.pendingSpriteSheet!,\n spriteSheetName: this.#displayManager.pendingSpriteSheetName!,\n });\n }\n break;\n default:\n break;\n }\n });\n DeviceManager.onDevice(this);\n if (isInBrowser) {\n window.addEventListener(\"beforeunload\", () => {\n if (this.isConnected && this.clearSensorConfigurationOnLeave) {\n this.clearSensorConfiguration();\n }\n });\n }\n if (isInNode) {\n /** can add more node leave handlers https://gist.github.com/hyrious/30a878f6e6a057f09db87638567cb11a */\n process.on(\"exit\", () => {\n if (this.isConnected && this.clearSensorConfigurationOnLeave) {\n this.clearSensorConfiguration();\n }\n });\n }\n }\n\n static #DefaultConnectionManager(): BaseConnectionManager {\n return new WebBluetoothConnectionManager();\n }\n\n #eventDispatcher: DeviceEventDispatcher = new EventDispatcher(\n this as Device,\n DeviceEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n get removeEventListeners() {\n return this.#eventDispatcher.removeEventListeners;\n }\n get removeAllEventListeners() {\n return this.#eventDispatcher.removeAllEventListeners;\n }\n\n // CONNECTION MANAGER\n\n #connectionManager?: BaseConnectionManager;\n get connectionManager() {\n return this.#connectionManager;\n }\n set connectionManager(newConnectionManager) {\n if (this.connectionManager == newConnectionManager) {\n _console.log(\"same connectionManager is already assigned\");\n return;\n }\n\n if (this.connectionManager) {\n this.connectionManager.remove();\n }\n if (newConnectionManager) {\n newConnectionManager.onStatusUpdated =\n this.#onConnectionStatusUpdated.bind(this);\n newConnectionManager.onMessageReceived =\n this.#onConnectionMessageReceived.bind(this);\n newConnectionManager.onMessagesReceived =\n this.#onConnectionMessagesReceived.bind(this);\n }\n\n this.#connectionManager = newConnectionManager;\n _console.log(\"assigned new connectionManager\", this.#connectionManager);\n\n this._informationManager.connectionType = this.connectionType;\n }\n async #sendTxMessages(messages?: TxMessage[], sendImmediately?: boolean) {\n await this.#connectionManager?.sendTxMessages(messages, sendImmediately);\n }\n private sendTxMessages = this.#sendTxMessages.bind(this);\n\n async connect(options?: ConnectOptions) {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return;\n }\n if (this.connectionStatus == \"connecting\") {\n _console.log(\"already connecting\");\n return;\n }\n\n _console.log(\"connect options\", options);\n if (options) {\n switch (options.type) {\n case \"webBluetooth\":\n if (this.connectionType != \"webBluetooth\") {\n this.connectionManager = new WebBluetoothConnectionManager();\n }\n break;\n case \"webSocket\":\n {\n let createConnectionManager = false;\n if (this.connectionType == \"webSocket\") {\n const connectionManager = this\n .connectionManager as WebSocketConnectionManager;\n if (\n connectionManager.ipAddress != options.ipAddress ||\n connectionManager.isSecure != options.isWifiSecure\n ) {\n createConnectionManager = true;\n }\n } else {\n createConnectionManager = true;\n }\n if (createConnectionManager) {\n this.connectionManager = new WebSocketConnectionManager(\n options.ipAddress,\n options.isWifiSecure,\n this.bluetoothId\n );\n }\n }\n\n break;\n case \"udp\":\n {\n let createConnectionManager = false;\n if (this.connectionType == \"udp\") {\n const connectionManager = this\n .connectionManager as UDPConnectionManager;\n if (connectionManager.ipAddress != options.ipAddress) {\n createConnectionManager = true;\n }\n this.reconnectOnDisconnection = true;\n } else {\n createConnectionManager = true;\n }\n if (createConnectionManager) {\n this.connectionManager = new UDPConnectionManager(\n options.ipAddress,\n this.bluetoothId\n );\n }\n }\n break;\n }\n }\n if (!this.connectionManager) {\n this.connectionManager = Device.#DefaultConnectionManager();\n }\n this.#clear();\n\n if (options?.type == \"client\") {\n _console.assertWithError(\n this.connectionType == \"client\",\n \"expected clientConnectionManager\"\n );\n const clientConnectionManager = this\n .connectionManager as ClientConnectionManager;\n clientConnectionManager.subType = options.subType;\n return clientConnectionManager.connect();\n }\n _console.log(\"connectionManager type\", this.connectionManager.type);\n return this.connectionManager.connect();\n }\n #isConnected = false;\n get isConnected() {\n return this.#isConnected;\n }\n /** @throws {Error} if not connected */\n #assertIsConnected() {\n _console.assertWithError(this.isConnected, \"notConnected\");\n }\n\n #didReceiveMessageTypes(messageTypes: ConnectionMessageType[]) {\n return messageTypes.every((messageType) => {\n const hasConnectionMessage =\n this.latestConnectionMessages.has(messageType);\n if (!hasConnectionMessage) {\n _console.log(`didn't receive \"${messageType}\" message`);\n }\n return hasConnectionMessage;\n });\n }\n get #hasRequiredInformation() {\n let hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredInformationConnectionMessages\n );\n if (hasRequiredInformation && this.sensorTypes.includes(\"pressure\")) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredPressureMessageTypes\n );\n }\n if (hasRequiredInformation && this.isWifiAvailable) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredWifiMessageTypes\n );\n }\n if (hasRequiredInformation && this.fileTypes.length > 0) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredFileTransferMessageTypes\n );\n }\n if (hasRequiredInformation && this.fileTypes.includes(\"tflite\")) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredTfliteMessageTypes\n );\n }\n if (hasRequiredInformation && this.hasCamera) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredCameraMessageTypes\n );\n }\n if (hasRequiredInformation && this.hasMicrophone) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredMicrophoneMessageTypes\n );\n }\n if (hasRequiredInformation && this.isDisplayAvailable) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredDisplayMessageTypes\n );\n }\n return hasRequiredInformation;\n }\n #requestRequiredInformation() {\n _console.log(\"requesting required information\");\n const messages: TxMessage[] = RequiredInformationConnectionMessages.map(\n (messageType) => ({\n type: messageType,\n })\n );\n this.#sendTxMessages(messages);\n }\n\n get canReconnect() {\n return this.connectionManager?.canReconnect;\n }\n #assertCanReconnect() {\n _console.assertWithError(this.canReconnect, \"cannot reconnect to device\");\n }\n async reconnect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return;\n }\n if (this.connectionStatus == \"connecting\") {\n _console.log(\"already connecting\");\n return;\n }\n if (!this.canReconnect) {\n _console.warn(\"cannot reconnect\");\n return false;\n }\n // this.#assertCanReconnect();\n _console.log(\"attempting to reconnect...\");\n this.#clear();\n _console.log(\"reconnecting...\");\n return this.connectionManager?.reconnect();\n }\n\n static async Connect() {\n const device = new Device();\n await device.connect();\n return device;\n }\n\n static #ReconnectOnDisconnection = false;\n static get ReconnectOnDisconnection() {\n return this.#ReconnectOnDisconnection;\n }\n static set ReconnectOnDisconnection(newReconnectOnDisconnection) {\n _console.assertTypeWithError(newReconnectOnDisconnection, \"boolean\");\n this.#ReconnectOnDisconnection = newReconnectOnDisconnection;\n }\n\n #reconnectOnDisconnection = Device.ReconnectOnDisconnection;\n get reconnectOnDisconnection() {\n return this.#reconnectOnDisconnection;\n }\n set reconnectOnDisconnection(newReconnectOnDisconnection) {\n _console.assertTypeWithError(newReconnectOnDisconnection, \"boolean\");\n this.#reconnectOnDisconnection = newReconnectOnDisconnection;\n }\n #reconnectIntervalId?: NodeJS.Timeout | number;\n\n get connectionType() {\n return this.connectionManager?.type;\n }\n async disconnect() {\n if (!this.isConnected) {\n _console.log(\"already not connected\");\n return;\n }\n if (this.connectionStatus == \"disconnecting\") {\n _console.log(\"already disconnecting\");\n return;\n }\n //this.#assertIsConnected();\n if (this.reconnectOnDisconnection) {\n this.reconnectOnDisconnection = false;\n this.addEventListener(\n \"isConnected\",\n () => {\n this.reconnectOnDisconnection = true;\n },\n { once: true }\n );\n }\n\n return this.connectionManager!.disconnect();\n }\n\n toggleConnection() {\n if (this.isConnected) {\n this.disconnect();\n } else if (this.canReconnect) {\n try {\n this.reconnect();\n } catch (error) {\n _console.error(\"error trying to reconnect\", error);\n this.connect();\n }\n } else {\n this.connect();\n }\n }\n\n get connectionStatus(): ConnectionStatus {\n switch (this.#connectionManager?.status) {\n case \"connected\":\n return this.isConnected ? \"connected\" : \"connecting\";\n case \"notConnected\":\n case \"connecting\":\n case \"disconnecting\":\n return this.#connectionManager.status;\n default:\n return \"notConnected\";\n }\n }\n get isConnectionBusy() {\n return (\n this.connectionStatus == \"connecting\" ||\n this.connectionStatus == \"disconnecting\"\n );\n }\n\n #onConnectionStatusUpdated(connectionStatus: ConnectionStatus) {\n _console.log({ connectionStatus });\n\n if (connectionStatus == \"notConnected\") {\n this.#clearConnection();\n\n if (this.canReconnect && this.reconnectOnDisconnection) {\n _console.log(\"starting reconnect interval...\");\n this.#reconnectIntervalId = setInterval(() => {\n _console.log(\"attempting reconnect...\");\n this.reconnect();\n }, 1000);\n }\n } else {\n if (this.#reconnectIntervalId != undefined) {\n _console.log(\"clearing reconnect interval\");\n clearInterval(this.#reconnectIntervalId);\n this.#reconnectIntervalId = undefined;\n }\n }\n\n this.#checkConnection();\n\n if (connectionStatus == \"connected\" && !this.#isConnected) {\n if (this.connectionType != \"client\") {\n this.#requestRequiredInformation();\n }\n }\n\n DeviceManager.OnDeviceConnectionStatusUpdated(this, connectionStatus);\n }\n\n #dispatchConnectionEvents(includeIsConnected: boolean = false) {\n this.#dispatchEvent(\"connectionStatus\", {\n connectionStatus: this.connectionStatus,\n });\n this.#dispatchEvent(this.connectionStatus, {});\n if (includeIsConnected) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n }\n #checkConnection() {\n this.#isConnected =\n Boolean(this.connectionManager?.isConnected) &&\n this.#hasRequiredInformation &&\n this._informationManager.isCurrentTimeSet;\n\n switch (this.connectionStatus) {\n case \"connected\":\n if (this.#isConnected) {\n this.#dispatchConnectionEvents(true);\n }\n break;\n case \"notConnected\":\n this.#dispatchConnectionEvents(true);\n break;\n default:\n this.#dispatchConnectionEvents(false);\n break;\n }\n }\n\n #clear() {\n this.#clearConnection();\n this._informationManager.clear();\n this.#deviceInformationManager.clear();\n this.#tfliteManager.clear();\n this.#fileTransferManager.clear();\n this.#wifiManager.clear();\n this.#cameraManager.clear();\n this.#microphoneManager.clear();\n this.#sensorConfigurationManager.clear();\n this.#displayManager.reset();\n this.#isServerSide = false;\n }\n #clearConnection() {\n this.connectionManager?.clear();\n this.latestConnectionMessages.clear();\n }\n\n #onConnectionMessageReceived(\n messageType: ConnectionMessageType,\n dataView: DataView\n ) {\n _console.log({ messageType, dataView });\n switch (messageType) {\n case \"batteryLevel\":\n const batteryLevel = dataView.getUint8(0);\n _console.log(\"received battery level\", { batteryLevel });\n this.#updateBatteryLevel(batteryLevel);\n break;\n\n default:\n if (\n FileTransferMessageTypes.includes(\n messageType as FileTransferMessageType\n )\n ) {\n this.#fileTransferManager.parseMessage(\n messageType as FileTransferMessageType,\n dataView\n );\n } else if (\n TfliteMessageTypes.includes(messageType as TfliteMessageType)\n ) {\n this.#tfliteManager.parseMessage(\n messageType as TfliteMessageType,\n dataView\n );\n } else if (\n SensorDataMessageTypes.includes(messageType as SensorDataMessageType)\n ) {\n this.#sensorDataManager.parseMessage(\n messageType as SensorDataMessageType,\n dataView\n );\n } else if (\n FirmwareMessageTypes.includes(messageType as FirmwareMessageType)\n ) {\n this.#firmwareManager.parseMessage(\n messageType as FirmwareMessageType,\n dataView\n );\n } else if (\n DeviceInformationTypes.includes(messageType as DeviceInformationType)\n ) {\n this.#deviceInformationManager.parseMessage(\n messageType as DeviceInformationType,\n dataView\n );\n } else if (\n InformationMessageTypes.includes(\n messageType as InformationMessageType\n )\n ) {\n this._informationManager.parseMessage(\n messageType as InformationMessageType,\n dataView\n );\n } else if (\n SensorConfigurationMessageTypes.includes(\n messageType as SensorConfigurationMessageType\n )\n ) {\n this.#sensorConfigurationManager.parseMessage(\n messageType as SensorConfigurationMessageType,\n dataView\n );\n } else if (\n VibrationMessageTypes.includes(messageType as VibrationMessageType)\n ) {\n this.#vibrationManager.parseMessage(\n messageType as VibrationMessageType,\n dataView\n );\n } else if (WifiMessageTypes.includes(messageType as WifiMessageType)) {\n this.#wifiManager.parseMessage(\n messageType as WifiMessageType,\n dataView\n );\n } else if (\n CameraMessageTypes.includes(messageType as CameraMessageType)\n ) {\n this.#cameraManager.parseMessage(\n messageType as CameraMessageType,\n dataView\n );\n } else if (\n MicrophoneMessageTypes.includes(messageType as MicrophoneMessageType)\n ) {\n this.#microphoneManager.parseMessage(\n messageType as MicrophoneMessageType,\n dataView\n );\n } else if (\n DisplayMessageTypes.includes(messageType as DisplayMessageType)\n ) {\n this.#displayManager.parseMessage(\n messageType as DisplayMessageType,\n dataView\n );\n } else {\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n this.latestConnectionMessages.set(messageType, dataView);\n if (messageType.startsWith(\"set\")) {\n this.latestConnectionMessages.set(\n // @ts-expect-error\n messageType.replace(\"set\", \"get\"),\n dataView\n );\n }\n this.#dispatchEvent(\"connectionMessage\", { messageType, dataView });\n }\n #onConnectionMessagesReceived() {\n if (!this.isConnected && this.#hasRequiredInformation) {\n this.#checkConnection();\n }\n if (\n this.connectionStatus == \"notConnected\" ||\n this.connectionStatus == \"disconnecting\"\n ) {\n return;\n }\n this.#sendTxMessages();\n }\n\n latestConnectionMessages: Map<ConnectionMessageType, DataView> = new Map();\n\n // DEVICE INFORMATION\n #deviceInformationManager = new DeviceInformationManager();\n get deviceInformation() {\n return this.#deviceInformationManager.information;\n }\n\n // BATTERY LEVEL\n #batteryLevel = 0;\n get batteryLevel() {\n return this.#batteryLevel;\n }\n #updateBatteryLevel(updatedBatteryLevel: number) {\n _console.assertTypeWithError(updatedBatteryLevel, \"number\");\n if (this.#batteryLevel == updatedBatteryLevel) {\n _console.log(`duplicate batteryLevel assignment ${updatedBatteryLevel}`);\n return;\n }\n this.#batteryLevel = updatedBatteryLevel;\n _console.log({ updatedBatteryLevel: this.#batteryLevel });\n this.#dispatchEvent(\"batteryLevel\", { batteryLevel: this.#batteryLevel });\n }\n\n // INFORMATION\n /** @private */\n _informationManager = new InformationManager();\n\n get id() {\n return this._informationManager.id;\n }\n\n get isCharging() {\n return this._informationManager.isCharging;\n }\n get batteryCurrent() {\n return this._informationManager.batteryCurrent;\n }\n get getBatteryCurrent() {\n return this._informationManager.getBatteryCurrent;\n }\n\n get name() {\n return this._informationManager.name;\n }\n get setName() {\n return this._informationManager.setName;\n }\n\n get type() {\n return this._informationManager.type;\n }\n get setType() {\n return this._informationManager.setType;\n }\n\n get isInsole() {\n return this._informationManager.isInsole;\n }\n get isGlove() {\n return this._informationManager.isGlove;\n }\n get side() {\n return this._informationManager.side;\n }\n\n get mtu() {\n return this._informationManager.mtu;\n }\n\n // SENSOR TYPES\n get sensorTypes() {\n return Object.keys(this.sensorConfiguration) as SensorType[];\n }\n get continuousSensorTypes() {\n return ContinuousSensorTypes.filter((sensorType) =>\n this.sensorTypes.includes(sensorType)\n );\n }\n\n // SENSOR CONFIGURATION\n\n #sensorConfigurationManager = new SensorConfigurationManager();\n\n get sensorConfiguration() {\n return this.#sensorConfigurationManager.configuration;\n }\n\n get setSensorConfiguration() {\n return this.#sensorConfigurationManager.setConfiguration;\n }\n\n async clearSensorConfiguration() {\n return this.#sensorConfigurationManager.clearSensorConfiguration();\n }\n\n static #ClearSensorConfigurationOnLeave = true;\n static get ClearSensorConfigurationOnLeave() {\n return this.#ClearSensorConfigurationOnLeave;\n }\n static set ClearSensorConfigurationOnLeave(\n newClearSensorConfigurationOnLeave\n ) {\n _console.assertTypeWithError(newClearSensorConfigurationOnLeave, \"boolean\");\n this.#ClearSensorConfigurationOnLeave = newClearSensorConfigurationOnLeave;\n }\n\n #clearSensorConfigurationOnLeave = Device.ClearSensorConfigurationOnLeave;\n get clearSensorConfigurationOnLeave() {\n return this.#clearSensorConfigurationOnLeave;\n }\n set clearSensorConfigurationOnLeave(newClearSensorConfigurationOnLeave) {\n _console.assertTypeWithError(newClearSensorConfigurationOnLeave, \"boolean\");\n this.#clearSensorConfigurationOnLeave = newClearSensorConfigurationOnLeave;\n }\n\n // PRESSURE\n get numberOfPressureSensors() {\n return this.#sensorDataManager.pressureSensorDataManager.numberOfSensors;\n }\n\n // SENSOR DATA\n #sensorDataManager = new SensorDataManager();\n resetPressureRange() {\n this.#sensorDataManager.pressureSensorDataManager.resetRange();\n }\n\n // VIBRATION\n get vibrationLocations() {\n return this.#vibrationManager.vibrationLocations;\n }\n\n #vibrationManager = new VibrationManager();\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately?: boolean\n ) {\n this.#vibrationManager.triggerVibration(\n vibrationConfigurations,\n sendImmediately\n );\n }\n\n // FILE TRANSFER\n #fileTransferManager = new FileTransferManager();\n\n get fileTypes() {\n return this.#fileTransferManager.fileTypes;\n }\n get maxFileLength() {\n return this.#fileTransferManager.maxLength;\n }\n get validFileTypes() {\n return FileTypes.filter((fileType) => {\n if (fileType.includes(\"wifi\") && !this.isWifiAvailable) {\n return false;\n }\n return true;\n });\n }\n\n async sendFile(fileType: FileType, file: FileLike) {\n _console.assertWithError(\n this.validFileTypes.includes(fileType),\n `invalid fileType ${fileType}`\n );\n const promise = this.waitForEvent(\"fileTransferComplete\");\n this.#fileTransferManager.send(fileType, file);\n await promise;\n }\n async receiveFile(fileType: FileType) {\n const promise = this.waitForEvent(\"fileTransferComplete\");\n this.#fileTransferManager.receive(fileType);\n await promise;\n }\n\n get fileTransferStatus() {\n return this.#fileTransferManager.status;\n }\n\n cancelFileTransfer() {\n this.#fileTransferManager.cancel();\n }\n\n // TFLITE\n #tfliteManager = new TfliteManager();\n\n get isTfliteAvailable() {\n return this.fileTypes.includes(\"tflite\");\n }\n get tfliteName() {\n return this.#tfliteManager.name;\n }\n get setTfliteName() {\n return this.#tfliteManager.setName;\n }\n\n async sendTfliteConfiguration(configuration: TfliteFileConfiguration) {\n configuration.type = \"tflite\";\n this.#tfliteManager.sendConfiguration(configuration, false);\n const didSendFile = await this.#fileTransferManager.send(\n configuration.type,\n configuration.file\n );\n if (!didSendFile) {\n this.#sendTxMessages();\n }\n }\n\n // TFLITE MODEL CONFIG\n get tfliteTask() {\n return this.#tfliteManager.task;\n }\n get setTfliteTask() {\n return this.#tfliteManager.setTask;\n }\n get tfliteSampleRate() {\n return this.#tfliteManager.sampleRate;\n }\n get setTfliteSampleRate() {\n return this.#tfliteManager.setSampleRate;\n }\n get tfliteSensorTypes() {\n return this.#tfliteManager.sensorTypes;\n }\n get allowedTfliteSensorTypes() {\n return this.sensorTypes.filter((sensorType) =>\n TfliteSensorTypes.includes(sensorType as TfliteSensorType)\n );\n }\n get setTfliteSensorTypes() {\n return this.#tfliteManager.setSensorTypes;\n }\n get tfliteIsReady() {\n return this.#tfliteManager.isReady;\n }\n\n // TFLITE INFERENCING\n\n get tfliteInferencingEnabled() {\n return this.#tfliteManager.inferencingEnabled;\n }\n get setTfliteInferencingEnabled() {\n return this.#tfliteManager.setInferencingEnabled;\n }\n async enableTfliteInferencing() {\n return this.setTfliteInferencingEnabled(true);\n }\n async disableTfliteInferencing() {\n return this.setTfliteInferencingEnabled(false);\n }\n get toggleTfliteInferencing() {\n return this.#tfliteManager.toggleInferencingEnabled;\n }\n\n // TFLITE INFERENCE CONFIG\n\n get tfliteCaptureDelay() {\n return this.#tfliteManager.captureDelay;\n }\n get setTfliteCaptureDelay() {\n return this.#tfliteManager.setCaptureDelay;\n }\n get tfliteThreshold() {\n return this.#tfliteManager.threshold;\n }\n get setTfliteThreshold() {\n return this.#tfliteManager.setThreshold;\n }\n\n // FIRMWARE MANAGER\n\n #firmwareManager = new FirmwareManager();\n\n get canUpdateFirmware() {\n return this.#connectionManager?.canUpdateFirmware;\n }\n #assertCanUpdateFirmware() {\n _console.assertWithError(this.canUpdateFirmware, \"can't update firmware\");\n }\n\n #sendSmpMessage(data: ArrayBuffer) {\n this.#assertCanUpdateFirmware();\n return this.#connectionManager!.sendSmpMessage(data);\n }\n private sendSmpMessage = this.#sendSmpMessage.bind(this);\n\n get uploadFirmware() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.uploadFirmware;\n }\n get canReset() {\n return this.canUpdateFirmware;\n }\n async reset() {\n _console.assertWithError(\n this.canReset,\n \"reset is not enabled for this device\"\n );\n await this.#firmwareManager.reset();\n return this.#connectionManager!.disconnect();\n }\n get firmwareStatus() {\n return this.#firmwareManager.status;\n }\n get getFirmwareImages() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.getImages;\n }\n get firmwareImages() {\n return this.#firmwareManager.images;\n }\n get eraseFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.eraseImage;\n }\n get confirmFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.confirmImage;\n }\n get testFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.testImage;\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n _console.log(\"redundant isServerSide assignment\");\n return;\n }\n _console.log({ newIsServerSide });\n this.#isServerSide = newIsServerSide;\n\n this.#fileTransferManager.isServerSide = this.isServerSide;\n this.#displayManager.isServerSide = this.isServerSide;\n }\n\n // UKATON\n get isUkaton() {\n return this.deviceInformation.modelNumber.includes(\"Ukaton\");\n }\n\n // WIFI MANAGER\n #wifiManager = new WifiManager();\n get isWifiAvailable() {\n return this.#wifiManager.isWifiAvailable;\n }\n get wifiSSID() {\n return this.#wifiManager.wifiSSID;\n }\n async setWifiSSID(newWifiSSID: string) {\n return this.#wifiManager.setWifiSSID(newWifiSSID);\n }\n get wifiPassword() {\n return this.#wifiManager.wifiPassword;\n }\n async setWifiPassword(newWifiPassword: string) {\n return this.#wifiManager.setWifiPassword(newWifiPassword);\n }\n get isWifiConnected() {\n return this.#wifiManager.isWifiConnected;\n }\n get ipAddress() {\n return this.#wifiManager.ipAddress;\n }\n get wifiConnectionEnabled() {\n return this.#wifiManager.wifiConnectionEnabled;\n }\n get enableWifiConnection() {\n return this.#wifiManager.enableWifiConnection;\n }\n get setWifiConnectionEnabled() {\n return this.#wifiManager.setWifiConnectionEnabled;\n }\n get disableWifiConnection() {\n return this.#wifiManager.disableWifiConnection;\n }\n get toggleWifiConnection() {\n return this.#wifiManager.toggleWifiConnection;\n }\n get isWifiSecure() {\n return this.#wifiManager.isWifiSecure;\n }\n\n async reconnectViaWebSockets() {\n _console.assertWithError(this.isWifiConnected, \"wifi is not connected\");\n _console.assertWithError(\n this.connectionType != \"webSocket\",\n \"already connected via webSockets\"\n );\n _console.assertTypeWithError(this.ipAddress, \"string\");\n _console.log(\"reconnecting via websockets...\");\n await this.disconnect();\n await this.connect({\n type: \"webSocket\",\n ipAddress: this.ipAddress!,\n isWifiSecure: this.isWifiSecure,\n });\n }\n\n async reconnectViaUDP() {\n _console.assertWithError(isInNode, \"udp is only available in node\");\n _console.assertWithError(this.isWifiConnected, \"wifi is not connected\");\n _console.assertWithError(\n this.connectionType != \"udp\",\n \"already connected via udp\"\n );\n _console.assertTypeWithError(this.ipAddress, \"string\");\n _console.log(\"reconnecting via udp...\");\n await this.disconnect();\n await this.connect({\n type: \"udp\",\n ipAddress: this.ipAddress!,\n });\n }\n\n // CAMERA MANAGER\n #cameraManager = new CameraManager();\n get hasCamera() {\n return this.sensorTypes.includes(\"camera\");\n }\n get cameraStatus() {\n return this.#cameraManager.cameraStatus;\n }\n #assertHasCamera() {\n _console.assertWithError(this.hasCamera, \"camera not available\");\n }\n async takePicture(sensorRate: number = 10) {\n this.#assertHasCamera();\n if (this.sensorConfiguration.camera == 0) {\n this.setSensorConfiguration({ camera: sensorRate }, false, false);\n }\n await this.#cameraManager.takePicture();\n }\n async focusCamera(sensorRate: number = 10) {\n this.#assertHasCamera();\n if (this.sensorConfiguration.camera == 0) {\n this.setSensorConfiguration({ camera: sensorRate }, false, false);\n }\n await this.#cameraManager.focus();\n }\n async stopCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.stop();\n }\n async wakeCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.wake();\n }\n async sleepCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.sleep();\n }\n\n get cameraConfiguration() {\n return this.#cameraManager.cameraConfiguration;\n }\n get availableCameraConfigurationTypes() {\n return this.#cameraManager.availableCameraConfigurationTypes;\n }\n get cameraConfigurationRanges() {\n return this.#cameraManager.cameraConfigurationRanges;\n }\n\n get setCameraConfiguration() {\n return this.#cameraManager.setCameraConfiguration;\n }\n\n // MICROPHONE\n #microphoneManager = new MicrophoneManager();\n get hasMicrophone() {\n return this.sensorTypes.includes(\"microphone\");\n }\n get microphoneStatus() {\n return this.#microphoneManager.microphoneStatus;\n }\n #assertHasMicrophone() {\n _console.assertWithError(this.hasMicrophone, \"microphone not available\");\n }\n\n async startMicrophone(sensorRate: number = 10) {\n this.#assertHasMicrophone();\n if (this.sensorConfiguration.microphone == 0) {\n this.setSensorConfiguration({ microphone: sensorRate }, false, false);\n }\n await this.#microphoneManager.start();\n }\n async stopMicrophone() {\n this.#assertHasMicrophone();\n await this.#microphoneManager.stop();\n }\n async enableMicrophoneVad() {\n this.#assertHasMicrophone();\n await this.#microphoneManager.vad();\n }\n async toggleMicrophone(sensorRate: number = 10) {\n this.#assertHasMicrophone();\n if (this.sensorConfiguration.microphone == 0) {\n this.setSensorConfiguration({ microphone: sensorRate }, false, false);\n }\n await this.#microphoneManager.toggle();\n }\n\n get microphoneConfiguration() {\n return this.#microphoneManager.microphoneConfiguration;\n }\n get availableMicrophoneConfigurationTypes() {\n return this.#microphoneManager.availableMicrophoneConfigurationTypes;\n }\n get setMicrophoneConfiguration() {\n return this.#microphoneManager.setMicrophoneConfiguration;\n }\n\n #assertWebAudioSupport() {\n _console.assertWithError(AudioContext, \"WebAudio is not supported\");\n }\n\n get audioContext() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.audioContext;\n }\n set audioContext(newAudioContext) {\n this.#assertWebAudioSupport();\n this.#microphoneManager.audioContext = newAudioContext;\n }\n get microphoneMediaStreamDestination() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.mediaStreamDestination;\n }\n get microphoneGainNode() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.gainNode;\n }\n\n get isRecordingMicrophone() {\n return this.#microphoneManager.isRecording;\n }\n startRecordingMicrophone() {\n this.#microphoneManager.startRecording();\n }\n stopRecordingMicrophone() {\n this.#microphoneManager.stopRecording();\n }\n toggleMicrophoneRecording() {\n this.#microphoneManager.toggleRecording();\n }\n\n // DISPLAY\n #displayManager = new DisplayManager();\n\n get isDisplayAvailable() {\n return this.#displayManager.isAvailable;\n }\n get isDisplayReady() {\n return this.#displayManager.isReady;\n }\n get displayContextState() {\n return this.#displayManager.contextState;\n }\n get displayColors() {\n return this.#displayManager.colors;\n }\n get displayBitmapColors() {\n return this.#displayManager.bitmapColors;\n }\n get displayBitmapColorIndices() {\n return this.#displayManager.bitmapColorIndices;\n }\n get displayColorOpacities() {\n return this.#displayManager.opacities;\n }\n #assertDisplayIsAvailable() {\n _console.assertWithError(this.isDisplayAvailable, \"display not available\");\n }\n get displayStatus() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.displayStatus;\n }\n get displayBrightness() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.brightness;\n }\n get setDisplayBrightness() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBrightness;\n }\n\n get displayInformation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.displayInformation;\n }\n get numberOfDisplayColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.numberOfColors;\n }\n\n get wakeDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.wake;\n }\n get sleepDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.sleep;\n }\n get toggleDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.toggle;\n }\n get isDisplayAwake() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.isDisplayAwake;\n }\n\n get showDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.show;\n }\n get clearDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clear;\n }\n\n get setDisplayColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setColor;\n }\n get setDisplayColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setColorOpacity;\n }\n get setDisplayOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setOpacity;\n }\n\n get saveDisplayContext() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.saveContext;\n }\n get restoreDisplayContext() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.restoreContext;\n }\n\n get clearDisplayRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRect;\n }\n\n get selectDisplayBackgroundColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBackgroundColor;\n }\n get selectDisplayFillColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectFillColor;\n }\n get selectDisplayLineColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectLineColor;\n }\n get setDisplayIgnoreFill() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setIgnoreFill;\n }\n get setDisplayIgnoreLine() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setIgnoreLine;\n }\n get setDisplayFillBackground() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setFillBackground;\n }\n get setDisplayLineWidth() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setLineWidth;\n }\n get setDisplayRotation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotation;\n }\n get clearDisplayRotation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRotation;\n }\n\n get setDisplaySegmentStartCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentStartCap;\n }\n get setDisplaySegmentEndCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentEndCap;\n }\n get setDisplaySegmentCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentCap;\n }\n\n get setDisplaySegmentStartRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentStartRadius;\n }\n get setDisplaySegmentEndRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentEndRadius;\n }\n get setDisplaySegmentRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentRadius;\n }\n\n get setDisplayCropTop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropTop;\n }\n get setDisplayCropRight() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropRight;\n }\n get setDisplayCropBottom() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropBottom;\n }\n get setDisplayCropLeft() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropLeft;\n }\n get setDisplayCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCrop;\n }\n get clearDisplayCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearCrop;\n }\n\n get setDisplayRotationCropTop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropTop;\n }\n get setDisplayRotationCropRight() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropRight;\n }\n get setDisplayRotationCropBottom() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropBottom;\n }\n get setDisplayRotationCropLeft() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropLeft;\n }\n get setDisplayRotationCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCrop;\n }\n get clearDisplayRotationCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRotationCrop;\n }\n get flushDisplayContextCommands() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.flushContextCommands;\n }\n\n get drawDisplayRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRect;\n }\n get drawDisplayCircle() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawCircle;\n }\n get drawDisplayEllipse() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawEllipse;\n }\n get drawDisplayRoundRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRoundRect;\n }\n get drawDisplayRegularPolygon() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRegularPolygon;\n }\n get drawDisplayPolygon() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawPolygon;\n }\n get drawDisplayWireframe() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawWireframe;\n }\n get drawDisplaySegment() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSegment;\n }\n get drawDisplaySegments() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSegments;\n }\n get drawDisplayArc() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawArc;\n }\n get drawDisplayArcEllipse() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawArcEllipse;\n }\n get drawDisplayBitmap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawBitmap;\n }\n get imageToDisplayBitmap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.imageToBitmap;\n }\n get quantizeDisplayImage() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.quantizeImage;\n }\n get resizeAndQuantizeDisplayImage() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resizeAndQuantizeImage;\n }\n\n get setDisplayContextState() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setContextState;\n }\n\n get selectDisplayBitmapColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBitmapColor;\n }\n get selectDisplayBitmapColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBitmapColors;\n }\n get setDisplayBitmapColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapColor;\n }\n get setDisplayBitmapColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapColorOpacity;\n }\n\n get setDisplayBitmapScaleDirection() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleDirection;\n }\n get setDisplayBitmapScaleX() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleX;\n }\n get setDisplayBitmapScaleY() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleY;\n }\n get setDisplayBitmapScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScale;\n }\n get resetDisplayBitmapScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetBitmapScale;\n }\n\n get selectDisplaySpriteColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteColor;\n }\n get selectDisplaySpriteColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteColors;\n }\n get setDisplaySpriteColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteColor;\n }\n get setDisplaySpriteColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteColorOpacity;\n }\n get resetDisplaySpriteColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetSpriteColors;\n }\n\n get setDisplaySpriteScaleDirection() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleDirection;\n }\n get setDisplaySpriteScaleX() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleX;\n }\n get setDisplaySpriteScaleY() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleY;\n }\n get setDisplaySpriteScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScale;\n }\n get resetDisplaySpriteScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetSpriteScale;\n }\n\n get displayManager() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager as DisplayManagerInterface;\n }\n\n get uploadDisplaySpriteSheet() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.uploadSpriteSheet;\n }\n get uploadDisplaySpriteSheets() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.uploadSpriteSheets;\n }\n get selectDisplaySpriteSheet() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteSheet;\n }\n get drawDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSprite;\n }\n\n get startDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.startSprite;\n }\n get endDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.endSprite;\n }\n\n get displaySpriteSheets() {\n return this.#displayManager.spriteSheets;\n }\n\n get serializeDisplaySpriteSheet() {\n return this.#displayManager.serializeSpriteSheet;\n }\n\n get setDisplayAlignment() {\n return this.#displayManager.setAlignment;\n }\n get setDisplayVerticalAlignment() {\n return this.#displayManager.setVerticalAlignment;\n }\n get setDisplayHorizontalAlignment() {\n return this.#displayManager.setHorizontalAlignment;\n }\n get resetDisplayAlignment() {\n return this.#displayManager.resetAlignment;\n }\n\n get setDisplaySpritesDirection() {\n return this.#displayManager.setSpritesDirection;\n }\n get setDisplaySpritesLineDirection() {\n return this.#displayManager.setSpritesLineDirection;\n }\n get setDisplaySpritesSpacing() {\n return this.#displayManager.setSpritesSpacing;\n }\n get setDisplaySpritesLineSpacing() {\n return this.#displayManager.setSpritesLineSpacing;\n }\n get setDisplaySpritesAlignment() {\n return this.#displayManager.setSpritesAlignment;\n }\n\n get drawDisplayQuadraticBezierCurve() {\n return this.#displayManager.drawQuadraticBezierCurve;\n }\n get drawDisplayQuadraticBezierCurves() {\n return this.#displayManager.drawQuadraticBezierCurves;\n }\n get drawDisplayCubicBezierCurve() {\n return this.#displayManager.drawCubicBezierCurve;\n }\n get drawDisplayCubicBezierCurves() {\n return this.#displayManager.drawCubicBezierCurves;\n }\n get drawDisplayPath() {\n return this.#displayManager.drawPath;\n }\n get drawDisplayClosedPath() {\n return this.#displayManager.drawClosedPath;\n }\n}\n\nexport default Device;\n","import { createConsole } from \"../utils/Console.ts\";\nimport CenterOfPressureHelper from \"../utils/CenterOfPressureHelper.ts\";\nimport {\n PressureData,\n PressureSensorPosition,\n PressureSensorValue,\n} from \"../sensor/PressureSensorDataManager.ts\";\nimport { CenterOfPressure } from \"../utils/CenterOfPressureHelper.ts\";\nimport { Side, Sides } from \"../InformationManager.ts\";\nimport { DeviceEventMap } from \"../Device.ts\";\nimport { RangeHelper } from \"../BS.ts\";\n\nconst _console = createConsole(\"DevicePairPressureSensorDataManager\", {\n log: false,\n});\n\nexport type DevicePairRawPressureData = { [side in Side]: PressureData };\n\nexport interface DevicePairPressureData {\n sensors: { [key in Side]: PressureSensorValue[] };\n scaledSum: number;\n normalizedSum: number;\n center?: CenterOfPressure;\n normalizedCenter?: CenterOfPressure;\n}\n\nexport interface DevicePairPressureDataEventMessage {\n pressure: DevicePairPressureData;\n}\n\nexport interface DevicePairPressureDataEventMessages {\n pressure: DevicePairPressureDataEventMessage;\n}\n\nclass DevicePairPressureSensorDataManager {\n #rawPressure: Partial<DevicePairRawPressureData> = {};\n\n #centerOfPressureHelper = new CenterOfPressureHelper();\n\n #normalizedSumRangeHelper = new RangeHelper();\n\n constructor() {\n this.resetPressureRange();\n }\n\n resetPressureRange() {\n this.#centerOfPressureHelper.reset();\n this.#normalizedSumRangeHelper.reset();\n }\n\n onDevicePressureData(event: DeviceEventMap[\"pressure\"]) {\n const { pressure } = event.message;\n const { side } = event.target;\n _console.log({ pressure, side });\n this.#rawPressure[side] = pressure;\n if (this.#hasAllPressureData) {\n return this.#updatePressureData();\n } else {\n _console.log(\"doesn't have all pressure data yet...\");\n }\n }\n\n get #hasAllPressureData() {\n return Sides.every((side) => side in this.#rawPressure);\n }\n\n #updatePressureData() {\n const pressure: DevicePairPressureData = {\n scaledSum: 0,\n normalizedSum: 0,\n sensors: { left: [], right: [] },\n };\n\n Sides.forEach((side) => {\n const sidePressure = this.#rawPressure[side]!;\n pressure.scaledSum += sidePressure.scaledSum;\n //pressure.normalizedSum += this.#rawPressure[side]!.normalizedSum;\n });\n pressure.normalizedSum +=\n this.#normalizedSumRangeHelper.updateAndGetNormalization(\n pressure.scaledSum,\n false\n );\n\n if (pressure.scaledSum > 0) {\n pressure.center = { x: 0, y: 0 };\n Sides.forEach((side) => {\n const sidePressure = this.#rawPressure[side]!;\n\n if (false) {\n const sidePressureWeight =\n sidePressure.scaledSum / pressure.scaledSum;\n if (sidePressureWeight > 0) {\n if (sidePressure.normalizedCenter?.y != undefined) {\n pressure.center!.y +=\n sidePressure.normalizedCenter!.y * sidePressureWeight;\n }\n if (side == \"right\") {\n pressure.center!.x = sidePressureWeight;\n }\n }\n } else {\n sidePressure.sensors.forEach((sensor) => {\n const _sensor: PressureSensorValue = structuredClone(sensor);\n _sensor.weightedValue = sensor.scaledValue / pressure.scaledSum;\n let { x, y } = sensor.position;\n x /= 2;\n if (side == \"right\") {\n x += 0.5;\n }\n _sensor.position = { x, y };\n pressure.center!.x += _sensor.position.x * _sensor.weightedValue;\n pressure.center!.y += _sensor.position.y * _sensor.weightedValue;\n pressure.sensors[side].push(_sensor);\n });\n }\n });\n\n pressure.normalizedCenter =\n this.#centerOfPressureHelper.updateAndGetNormalization(\n pressure.center,\n false\n );\n }\n\n _console.log({ devicePairPressure: pressure });\n\n return pressure;\n }\n}\n\nexport default DevicePairPressureSensorDataManager;\n","import DevicePairPressureSensorDataManager, {\n DevicePairPressureDataEventMessages,\n} from \"./DevicePairPressureSensorDataManager.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { Side } from \"../InformationManager.ts\";\nimport { SensorType } from \"../sensor/SensorDataManager.ts\";\nimport { DeviceEventMap } from \"../Device.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport DevicePair from \"./DevicePair.ts\";\nimport { AddKeysAsPropertyToInterface, ExtendInterfaceValues, ValueOf } from \"../utils/TypeScriptUtils.ts\";\n\nconst _console = createConsole(\"DevicePairSensorDataManager\", { log: false });\n\nexport const DevicePairSensorTypes = [\"pressure\", \"sensorData\"] as const;\nexport type DevicePairSensorType = (typeof DevicePairSensorTypes)[number];\n\nexport const DevicePairSensorDataEventTypes = DevicePairSensorTypes;\nexport type DevicePairSensorDataEventType = (typeof DevicePairSensorDataEventTypes)[number];\n\nexport type DevicePairSensorDataTimestamps = { [side in Side]: number };\n\ninterface BaseDevicePairSensorDataEventMessage {\n timestamps: DevicePairSensorDataTimestamps;\n}\n\ntype BaseDevicePairSensorDataEventMessages = DevicePairPressureDataEventMessages;\ntype _DevicePairSensorDataEventMessages = ExtendInterfaceValues<\n AddKeysAsPropertyToInterface<BaseDevicePairSensorDataEventMessages, \"sensorType\">,\n BaseDevicePairSensorDataEventMessage\n>;\n\nexport type DevicePairSensorDataEventMessage = ValueOf<_DevicePairSensorDataEventMessages>;\ninterface AnyDevicePairSensorDataEventMessages {\n sensorData: DevicePairSensorDataEventMessage;\n}\nexport type DevicePairSensorDataEventMessages = _DevicePairSensorDataEventMessages &\n AnyDevicePairSensorDataEventMessages;\n\nexport type DevicePairSensorDataEventDispatcher = EventDispatcher<\n DevicePair,\n DevicePairSensorDataEventType,\n DevicePairSensorDataEventMessages\n>;\n\nclass DevicePairSensorDataManager {\n eventDispatcher!: DevicePairSensorDataEventDispatcher;\n get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n #timestamps: { [sensorType in SensorType]?: Partial<DevicePairSensorDataTimestamps> } = {};\n\n pressureSensorDataManager = new DevicePairPressureSensorDataManager();\n resetPressureRange() {\n this.pressureSensorDataManager.resetPressureRange();\n }\n\n onDeviceSensorData(event: DeviceEventMap[\"sensorData\"]) {\n const { timestamp, sensorType } = event.message;\n\n _console.log({ sensorType, timestamp, event });\n\n if (!this.#timestamps[sensorType]) {\n this.#timestamps[sensorType] = {};\n }\n this.#timestamps[sensorType]![event.target.side] = timestamp;\n\n let value;\n switch (sensorType) {\n case \"pressure\":\n value = this.pressureSensorDataManager.onDevicePressureData(event as unknown as DeviceEventMap[\"pressure\"]);\n break;\n default:\n _console.log(`uncaught sensorType \"${sensorType}\"`);\n break;\n }\n\n if (value) {\n const timestamps = Object.assign({}, this.#timestamps[sensorType]) as DevicePairSensorDataTimestamps;\n // @ts-expect-error\n this.dispatchEvent(sensorType as DevicePairSensorDataEventType, { sensorType, timestamps, [sensorType]: value });\n // @ts-expect-error\n this.dispatchEvent(\"sensorData\", { sensorType, timestamps, [sensorType]: value });\n } else {\n _console.log(\"no value received\");\n }\n }\n}\n\nexport default DevicePairSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../utils/EventUtils.ts\";\nimport Device, {\n DeviceEvent,\n DeviceEventType,\n DeviceEventMessages,\n DeviceEventTypes,\n BoundDeviceEventListeners,\n DeviceEventMap,\n} from \"../Device.ts\";\nimport DevicePairSensorDataManager, {\n DevicePairSensorDataEventDispatcher,\n} from \"./DevicePairSensorDataManager.ts\";\nimport { capitalizeFirstCharacter } from \"../utils/stringUtils.ts\";\nimport { Side, Sides } from \"../InformationManager.ts\";\nimport { VibrationConfiguration } from \"../vibration/VibrationManager.ts\";\nimport { SensorConfiguration } from \"../sensor/SensorConfigurationManager.ts\";\nimport {\n DevicePairSensorDataEventMessages,\n DevicePairSensorDataEventTypes,\n} from \"./DevicePairSensorDataManager.ts\";\nimport {\n AddPrefixToInterfaceKeys,\n ExtendInterfaceValues,\n KeyOf,\n} from \"../utils/TypeScriptUtils.ts\";\nimport DeviceManager from \"../DeviceManager.ts\";\n\nconst _console = createConsole(\"DevicePair\", { log: false });\n\ninterface BaseDevicePairDeviceEventMessage {\n device: Device;\n side: Side;\n}\ntype DevicePairDeviceEventMessages = ExtendInterfaceValues<\n AddPrefixToInterfaceKeys<DeviceEventMessages, \"device\">,\n BaseDevicePairDeviceEventMessage\n>;\ntype DevicePairDeviceEventType = KeyOf<DevicePairDeviceEventMessages>;\nfunction getDevicePairDeviceEventType(deviceEventType: DeviceEventType) {\n return `device${capitalizeFirstCharacter(\n deviceEventType\n )}` as DevicePairDeviceEventType;\n}\nconst DevicePairDeviceEventTypes = DeviceEventTypes.map((eventType) =>\n getDevicePairDeviceEventType(eventType)\n) as DevicePairDeviceEventType[];\n\nexport const DevicePairConnectionEventTypes = [\"isConnected\"] as const;\nexport type DevicePairConnectionEventType =\n (typeof DevicePairConnectionEventTypes)[number];\n\nexport interface DevicePairConnectionEventMessages {\n isConnected: { isConnected: boolean };\n}\n\nexport const DevicePairEventTypes = [\n ...DevicePairConnectionEventTypes,\n ...DevicePairSensorDataEventTypes,\n ...DevicePairDeviceEventTypes,\n] as const;\nexport type DevicePairEventType = (typeof DevicePairEventTypes)[number];\n\nexport type DevicePairEventMessages = DevicePairConnectionEventMessages &\n DevicePairSensorDataEventMessages &\n DevicePairDeviceEventMessages;\n\nexport type DevicePairEventDispatcher = EventDispatcher<\n DevicePair,\n DevicePairEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEventMap = EventMap<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEventListenerMap = EventListenerMap<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEvent = Event<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type BoundDevicePairEventListeners = BoundEventListeners<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\n\nexport const DevicePairTypes = [\"insoles\", \"gloves\"] as const;\nexport type DevicePairType = (typeof DevicePairTypes)[number];\n\nclass DevicePair {\n constructor(type: DevicePairType) {\n this.#type = type;\n this.#sensorDataManager.eventDispatcher = this\n .#eventDispatcher as DevicePairSensorDataEventDispatcher;\n }\n\n get sides() {\n return Sides;\n }\n\n #type: DevicePairType;\n get type() {\n return this.#type;\n }\n\n #eventDispatcher: DevicePairEventDispatcher = new EventDispatcher(\n this as DevicePair,\n DevicePairEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n get removeEventListeners() {\n return this.#eventDispatcher.removeEventListeners;\n }\n get removeAllEventListeners() {\n return this.#eventDispatcher.removeAllEventListeners;\n }\n\n // SIDES\n #left?: Device;\n get left() {\n return this.#left;\n }\n\n #right?: Device;\n get right() {\n return this.#right;\n }\n\n get isConnected() {\n return Sides.every((side) => this[side]?.isConnected);\n }\n get isPartiallyConnected() {\n return Sides.some((side) => this[side]?.isConnected);\n }\n get isHalfConnected() {\n return this.isPartiallyConnected && !this.isConnected;\n }\n #assertIsConnected() {\n _console.assertWithError(this.isConnected, \"devicePair must be connected\");\n }\n\n #isDeviceCorrectType(device: Device) {\n switch (this.type) {\n case \"insoles\":\n return device.isInsole;\n case \"gloves\":\n return device.isGlove;\n }\n }\n\n assignDevice(device: Device) {\n if (!this.#isDeviceCorrectType(device)) {\n _console.log(\n `device is incorrect type ${device.type} for ${this.type} devicePair`\n );\n return;\n }\n const side = device.side;\n\n const currentDevice = this[side];\n\n if (device == currentDevice) {\n _console.log(\"device already assigned\");\n return;\n }\n\n if (currentDevice) {\n this.#removeDeviceEventListeners(currentDevice);\n }\n this.#addDeviceEventListeners(device);\n\n switch (side) {\n case \"left\":\n this.#left = device;\n break;\n case \"right\":\n this.#right = device;\n break;\n }\n\n _console.log(`assigned ${side} ${this.type} device`, device);\n\n this.resetPressureRange();\n\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n this.#dispatchEvent(\"deviceIsConnected\", {\n device,\n isConnected: device.isConnected,\n side,\n });\n\n return currentDevice;\n }\n\n #addDeviceEventListeners(device: Device) {\n addEventListeners(device, this.#boundDeviceEventListeners);\n DeviceEventTypes.forEach((deviceEventType) => {\n device.addEventListener(\n // @ts-expect-error\n deviceEventType,\n this.#redispatchDeviceEvent.bind(this)\n );\n });\n }\n #removeDeviceEventListeners(device: Device) {\n removeEventListeners(device, this.#boundDeviceEventListeners);\n DeviceEventTypes.forEach((deviceEventType) => {\n device.removeEventListener(\n // @ts-expect-error\n deviceEventType,\n this.#redispatchDeviceEvent.bind(this)\n );\n });\n }\n\n #removeDevice(device: Device) {\n const foundDevice = Sides.some((side) => {\n if (this[side] != device) {\n return false;\n }\n\n _console.log(`removing ${side} ${this.type} device`, device);\n removeEventListeners(device, this.#boundDeviceEventListeners);\n\n switch (side) {\n case \"left\":\n this.#left = undefined;\n break;\n case \"right\":\n this.#right = undefined;\n break;\n }\n\n return true;\n });\n if (foundDevice) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n return foundDevice;\n }\n\n #boundDeviceEventListeners: BoundDeviceEventListeners = {\n isConnected: this.#onDeviceIsConnected.bind(this),\n sensorData: this.#onDeviceSensorData.bind(this),\n getType: this.#onDeviceType.bind(this),\n };\n\n #redispatchDeviceEvent(deviceEvent: DeviceEvent) {\n const { type, target: device, message } = deviceEvent;\n this.#dispatchEvent(getDevicePairDeviceEventType(type), {\n ...message,\n device,\n side: device.side,\n });\n }\n\n #onDeviceIsConnected(deviceEvent: DeviceEventMap[\"isConnected\"]) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n\n #onDeviceType(deviceEvent: DeviceEventMap[\"getType\"]) {\n const { target: device } = deviceEvent;\n if (this[device.side] == device) {\n return;\n }\n const foundDevice = this.#removeDevice(device);\n if (!foundDevice) {\n return;\n }\n this.assignDevice(device);\n }\n\n // SENSOR CONFIGURATION\n async setSensorConfiguration(sensorConfiguration: SensorConfiguration) {\n for (let i = 0; i < Sides.length; i++) {\n const side = Sides[i];\n if (this[side]?.isConnected) {\n await this[side].setSensorConfiguration(sensorConfiguration);\n }\n }\n }\n\n // SENSOR DATA\n #sensorDataManager = new DevicePairSensorDataManager();\n #onDeviceSensorData(deviceEvent: DeviceEventMap[\"sensorData\"]) {\n if (this.isConnected) {\n this.#sensorDataManager.onDeviceSensorData(deviceEvent);\n }\n }\n resetPressureRange() {\n Sides.forEach((side) => this[side]?.resetPressureRange());\n this.#sensorDataManager.resetPressureRange();\n }\n\n // VIBRATION\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately?: boolean\n ) {\n const promises = Sides.map((side) => {\n return this[side]?.triggerVibration(\n vibrationConfigurations,\n sendImmediately\n );\n }).filter(Boolean);\n return Promise.allSettled(promises);\n }\n\n // SHARED INSTANCES\n static #insoles = new DevicePair(\"insoles\");\n static get insoles() {\n return this.#insoles;\n }\n static #gloves = new DevicePair(\"gloves\");\n static get gloves() {\n return this.#gloves;\n }\n static {\n DeviceManager.AddEventListener(\"deviceConnected\", (event) => {\n const { device } = event.message;\n if (device.isInsole) {\n this.#insoles.assignDevice(device);\n }\n if (device.isGlove) {\n this.#gloves.assignDevice(device);\n }\n });\n }\n}\n\nexport default DevicePair;\n","export function throttle<T extends (...args: any[]) => void>(\n fn: T,\n interval: number,\n trailing = false\n): (...args: Parameters<T>) => void {\n let lastTime = 0;\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let lastArgs: Parameters<T> | null = null;\n\n return function (...args: Parameters<T>) {\n const now = Date.now();\n const remaining = interval - (now - lastTime);\n\n if (remaining <= 0) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n lastTime = now;\n fn(...args);\n } else if (trailing) {\n lastArgs = args;\n if (!timeout) {\n timeout = setTimeout(() => {\n lastTime = Date.now();\n timeout = null;\n if (lastArgs) {\n fn(...lastArgs);\n lastArgs = null;\n }\n }, remaining);\n }\n }\n };\n}\n\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n interval: number,\n callImmediately = false\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return function (...args: Parameters<T>) {\n const callNow = callImmediately && !timeout;\n\n if (timeout) {\n clearTimeout(timeout);\n }\n\n timeout = setTimeout(() => {\n timeout = null;\n if (!callImmediately) {\n fn(...args);\n }\n }, interval);\n\n if (callNow) {\n fn(...args);\n }\n };\n}\n","import EventDispatcher, {\n BoundEventListeners,\n Event,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport { addEventListeners } from \"../utils/EventUtils.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { Timer } from \"../utils/Timer.ts\";\nimport { DeviceType } from \"../InformationManager.ts\";\nimport { ConnectionType } from \"../connection/BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"BaseScanner\", { log: false });\n\nexport const ScannerEventTypes = [\n \"isScanningAvailable\",\n \"isScanning\",\n \"discoveredDevice\",\n \"expiredDiscoveredDevice\",\n \"scanningAvailable\",\n \"scanningNotAvailable\",\n \"scanning\",\n \"notScanning\",\n] as const;\nexport type ScannerEventType = (typeof ScannerEventTypes)[number];\n\nexport interface DiscoveredDevice {\n bluetoothId: string;\n name: string;\n deviceType: DeviceType;\n rssi: number;\n ipAddress?: string;\n isWifiSecure?: boolean;\n}\n\ninterface ScannerDiscoveredDeviceEventMessage {\n discoveredDevice: DiscoveredDevice;\n}\n\nexport interface ScannerEventMessages {\n discoveredDevice: ScannerDiscoveredDeviceEventMessage;\n expiredDiscoveredDevice: ScannerDiscoveredDeviceEventMessage;\n isScanningAvailable: { isScanningAvailable: boolean };\n isScanning: { isScanning: boolean };\n scanning: {};\n notScanning: {};\n scanningAvailable: {};\n scanningNotAvailable: {};\n}\n\nexport type ScannerEventDispatcher = EventDispatcher<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type ScannerEventMap = EventMap<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type ScannerEvent = Event<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type BoundScannerEventListeners = BoundEventListeners<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\n\nexport type DiscoveredDevicesMap = { [deviceId: string]: DiscoveredDevice };\n\nabstract class BaseScanner {\n // IS SUPPORTED\n protected get baseConstructor() {\n return this.constructor as typeof BaseScanner;\n }\n static get isSupported() {\n return false;\n }\n get isSupported() {\n return this.baseConstructor.isSupported;\n }\n\n #assertIsSupported() {\n _console.assertWithError(\n this.isSupported,\n `${this.constructor.name} is not supported`\n );\n }\n\n // CONSTRUCTOR\n #assertIsSubclass() {\n _console.assertWithError(\n this.constructor != BaseScanner,\n `${this.constructor.name} must be subclassed`\n );\n }\n constructor() {\n this.#assertIsSubclass();\n this.#assertIsSupported();\n addEventListeners(this, this.#boundEventListeners);\n }\n\n #boundEventListeners: BoundScannerEventListeners = {\n discoveredDevice: this.#onDiscoveredDevice.bind(this),\n isScanning: this.#onIsScanning.bind(this),\n isScanningAvailable: this.#onIsScanningAvailable.bind(this),\n };\n\n // EVENT DISPATCHER\n #eventDispatcher: ScannerEventDispatcher = new EventDispatcher(\n this as BaseScanner,\n ScannerEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n protected get dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n\n // AVAILABILITY\n get isScanningAvailable() {\n return false;\n }\n #assertIsAvailable() {\n _console.assertWithError(this.isScanningAvailable, \"not available\");\n }\n\n // SCANNING\n get isScanning() {\n return false;\n }\n #assertIsScanning() {\n _console.assertWithError(this.isScanning, \"not scanning\");\n }\n #assertIsNotScanning() {\n _console.assertWithError(!this.isScanning, \"already scanning\");\n }\n\n startScan() {\n if (!this.isScanningAvailable) {\n _console.warn(\"scanning is not available\");\n return false;\n }\n if (this.isScanning) {\n _console.log(\"already scanning\");\n return false;\n }\n return true;\n // this.#assertIsAvailable();\n // this.#assertIsNotScanning();\n }\n stopScan() {\n if (!this.isScanning) {\n _console.log(\"already not scanning\");\n return false;\n }\n return true;\n //this.#assertIsScanning();\n }\n #onIsScanning(event: ScannerEventMap[\"isScanning\"]) {\n if (this.isScanning) {\n this.#discoveredDevices = {};\n this.#discoveredDeviceTimestamps = {};\n } else {\n this.#checkDiscoveredDevicesExpirationTimer.stop();\n }\n\n if (this.isScanning) {\n this.#eventDispatcher.dispatchEvent(\"scanning\", {});\n } else {\n this.#eventDispatcher.dispatchEvent(\"notScanning\", {});\n }\n }\n #onIsScanningAvailable(event: ScannerEventMap[\"isScanningAvailable\"]) {\n if (this.isScanningAvailable) {\n this.#eventDispatcher.dispatchEvent(\"scanningAvailable\", {});\n } else {\n this.#eventDispatcher.dispatchEvent(\"scanningNotAvailable\", {});\n }\n }\n\n // DISCOVERED DEVICES\n #discoveredDevices: DiscoveredDevicesMap = {};\n get discoveredDevices(): Readonly<DiscoveredDevicesMap> {\n return this.#discoveredDevices;\n }\n get discoveredDevicesArray() {\n return Object.values(this.#discoveredDevices).sort((a, b) => {\n return (\n this.#discoveredDeviceTimestamps[a.bluetoothId] -\n this.#discoveredDeviceTimestamps[b.bluetoothId]\n );\n });\n }\n #assertValidDiscoveredDeviceId(discoveredDeviceId: string) {\n _console.assertWithError(\n this.#discoveredDevices[discoveredDeviceId],\n `no discovered device with id \"${discoveredDeviceId}\"`\n );\n }\n\n #onDiscoveredDevice(event: ScannerEventMap[\"discoveredDevice\"]) {\n const { discoveredDevice } = event.message;\n this.#discoveredDevices[discoveredDevice.bluetoothId] = discoveredDevice;\n this.#discoveredDeviceTimestamps[discoveredDevice.bluetoothId] = Date.now();\n this.#checkDiscoveredDevicesExpirationTimer.start();\n }\n\n #discoveredDeviceTimestamps: { [id: string]: number } = {};\n\n static #DiscoveredDeviceExpirationTimeout = 5000;\n static get DiscoveredDeviceExpirationTimeout() {\n return this.#DiscoveredDeviceExpirationTimeout;\n }\n get #discoveredDeviceExpirationTimeout() {\n return BaseScanner.DiscoveredDeviceExpirationTimeout;\n }\n #checkDiscoveredDevicesExpirationTimer = new Timer(\n this.#checkDiscoveredDevicesExpiration.bind(this),\n 1000\n );\n #checkDiscoveredDevicesExpiration() {\n const entries = Object.entries(this.#discoveredDevices);\n if (entries.length == 0) {\n this.#checkDiscoveredDevicesExpirationTimer.stop();\n return;\n }\n const now = Date.now();\n entries.forEach(([id, discoveredDevice]) => {\n const timestamp = this.#discoveredDeviceTimestamps[id];\n if (now - timestamp > this.#discoveredDeviceExpirationTimeout) {\n _console.log(\"discovered device timeout\");\n delete this.#discoveredDevices[id];\n delete this.#discoveredDeviceTimestamps[id];\n this.dispatchEvent(\"expiredDiscoveredDevice\", { discoveredDevice });\n }\n });\n }\n\n // DEVICE CONNECTION\n async connectToDevice(deviceId: string, connectionType?: ConnectionType) {\n this.#assertIsAvailable();\n }\n\n // RESET\n get canReset() {\n return false;\n }\n reset() {\n _console.log(\"resetting...\");\n }\n}\n\nexport default BaseScanner;\n","import { dataToArrayBuffer } from \"../../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport { isInNode } from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n BoundGenericEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n allServiceUUIDs,\n getServiceNameFromUUID,\n getCharacteristicNameFromUUID,\n allCharacteristicNames,\n getCharacteristicProperties,\n} from \"./bluetoothUUIDs.ts\";\nimport BluetoothConnectionManager from \"./BluetoothConnectionManager.ts\";\n\nconst _console = createConsole(\"NobleConnectionManager\", { log: false });\n\nlet filterUUIDs = true;\n\n/** NODE_START */\nimport type * as noble from \"@abandonware/noble\";\nimport os from \"os\";\nconst isLinux = os.platform() == \"linux\";\nfilterUUIDs = !isLinux;\n/** NODE_END */\n\nimport {\n BluetoothCharacteristicName,\n BluetoothServiceName,\n} from \"./bluetoothUUIDs.ts\";\nimport { ConnectionType } from \"../BaseConnectionManager.ts\";\nimport NobleScanner from \"../../scanner/NobleScanner.ts\";\n\ninterface HasConnectionManager {\n connectionManager: NobleConnectionManager | undefined;\n}\nexport interface NoblePeripheral\n extends noble.Peripheral,\n HasConnectionManager {\n scanner: NobleScanner;\n}\ninterface NobleService extends noble.Service, HasConnectionManager {\n name: BluetoothServiceName;\n}\ninterface NobleCharacteristic\n extends noble.Characteristic,\n HasConnectionManager {\n name: BluetoothCharacteristicName;\n}\n\nclass NobleConnectionManager extends BluetoothConnectionManager {\n get bluetoothId() {\n return this.#noblePeripheral!.id;\n }\n\n get canUpdateFirmware() {\n return this.#characteristics.has(\"smp\");\n }\n\n static get isSupported() {\n return isInNode;\n }\n static get type(): ConnectionType {\n return \"noble\";\n }\n\n get isConnected() {\n return this.#noblePeripheral?.state == \"connected\";\n }\n\n async connect() {\n const canConnect = await super.connect();\n _console.log({ canConnect });\n if (!canConnect) {\n return false;\n }\n await this.#noblePeripheral!.connectAsync();\n return true;\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n await this.#noblePeripheral!.disconnectAsync();\n return true;\n }\n\n async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n const characteristic = this.#characteristics.get(characteristicName)!;\n _console.assertWithError(\n characteristic,\n `no characteristic found with name \"${characteristicName}\"`\n );\n // if (data instanceof DataView) {\n // data = data.buffer;\n // }\n const properties = getCharacteristicProperties(characteristicName);\n const buffer = Buffer.from(data);\n const writeWithoutResponse = properties.writeWithoutResponse;\n _console.log(\n `writing to ${characteristicName} ${\n writeWithoutResponse ? \"without\" : \"with\"\n } response`,\n buffer\n );\n await characteristic.writeAsync(buffer, writeWithoutResponse);\n if (characteristic.properties.includes(\"read\")) {\n await characteristic.readAsync();\n }\n }\n\n get canReconnect() {\n return this.#noblePeripheral!.connectable;\n }\n async reconnect() {\n let canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n await this.#noblePeripheral!.connectAsync();\n return true;\n }\n\n // NOBLE\n #noblePeripheral?: NoblePeripheral;\n get noblePeripheral() {\n return this.#noblePeripheral;\n }\n set noblePeripheral(newNoblePeripheral) {\n if (newNoblePeripheral) {\n _console.assertTypeWithError(newNoblePeripheral, \"object\");\n }\n if (this.noblePeripheral == newNoblePeripheral) {\n _console.log(\"attempted to assign duplicate noblePeripheral\");\n return;\n }\n\n _console.log(\"newNoblePeripheral\", newNoblePeripheral?.id);\n\n if (this.#noblePeripheral) {\n removeEventListeners(\n this.#noblePeripheral,\n this.#unboundNoblePeripheralListeners\n );\n delete this.#noblePeripheral!.connectionManager;\n }\n\n if (newNoblePeripheral) {\n newNoblePeripheral.connectionManager = this;\n addEventListeners(\n newNoblePeripheral,\n this.#unboundNoblePeripheralListeners\n );\n }\n\n this.#noblePeripheral = newNoblePeripheral;\n }\n\n // NOBLE EVENTLISTENERS\n #unboundNoblePeripheralListeners: BoundGenericEventListeners = {\n connect: this.#onNoblePeripheralConnect,\n disconnect: this.#onNoblePeripheralDisconnect,\n rssiUpdate: this.#onNoblePeripheralRssiUpdate,\n servicesDiscover: this.#onNoblePeripheralServicesDiscover,\n };\n\n async #onNoblePeripheralConnect(this: NoblePeripheral) {\n //_console.log(\"#onNoblePeripheralConnect\",this.id);\n await this.connectionManager!.onNoblePeripheralConnect(this);\n }\n async onNoblePeripheralConnect(noblePeripheral: NoblePeripheral) {\n _console.log(\n \"onNoblePeripheralConnect\",\n noblePeripheral.id,\n noblePeripheral.state\n );\n if (noblePeripheral.state == \"connected\") {\n _console.log(\"discoverServicesAsync\", noblePeripheral.id, {\n allServiceUUIDs,\n });\n // services don't show up if on ubuntu if serviceUUIDs are supplied\n if (filterUUIDs) {\n await this.#noblePeripheral!.discoverServicesAsync(\n allServiceUUIDs as string[]\n );\n } else {\n await this.#noblePeripheral!.discoverServicesAsync();\n }\n }\n // this gets called when it connects and disconnects, so we use the noblePeripheral's \"state\" property instead\n await this.#onNoblePeripheralState();\n }\n\n async #onNoblePeripheralDisconnect(this: NoblePeripheral) {\n //_console.log(\"#onNoblePeripheralDisconnect\", this.id);\n await this.connectionManager!.onNoblePeripheralConnect(this);\n }\n async onNoblePeripheralDisconnect(noblePeripheral: NoblePeripheral) {\n _console.log(\"onNoblePeripheralDisconnect\", noblePeripheral.id);\n await this.#onNoblePeripheralState();\n }\n\n async #onNoblePeripheralState() {\n _console.log(\n `noblePeripheral ${this.bluetoothId} state ${\n this.#noblePeripheral!.state\n }`\n );\n\n switch (this.#noblePeripheral!.state) {\n case \"connected\":\n //this.status = \"connected\";\n break;\n case \"connecting\":\n //this.status = \"connecting\";\n break;\n case \"disconnected\":\n this.#removeEventListeners();\n this.status = \"notConnected\";\n break;\n case \"disconnecting\":\n this.status = \"disconnecting\";\n break;\n case \"error\":\n _console.error(\"noblePeripheral error\");\n break;\n default:\n _console.log(\n `uncaught noblePeripheral state ${this.#noblePeripheral!.state}`\n );\n break;\n }\n }\n\n #removeEventListeners() {\n _console.log(\"removing noblePeripheral eventListeners\");\n this.#services.forEach((service) => {\n removeEventListeners(service, this.#unboundNobleServiceListeners);\n });\n this.#services.clear();\n\n this.#characteristics.forEach((characteristic) => {\n _console.log(\n `removing listeners from characteristic \"${characteristic.name}\" has ${characteristic.listeners.length} listeners`\n );\n removeEventListeners(\n characteristic,\n this.#unboundNobleCharacteristicListeners\n );\n });\n this.#characteristics.clear();\n }\n\n async #onNoblePeripheralRssiUpdate(this: NoblePeripheral, rssi: number) {\n await this.connectionManager!.onNoblePeripheralRssiUpdate(this, rssi);\n }\n async onNoblePeripheralRssiUpdate(\n noblePeripheral: NoblePeripheral,\n rssi: number\n ) {\n _console.log(\"onNoblePeripheralRssiUpdate\", noblePeripheral.id, rssi);\n // TODO: - can this be useful?\n }\n\n async #onNoblePeripheralServicesDiscover(\n this: NoblePeripheral,\n services: NobleService[]\n ) {\n await this.connectionManager!.onNoblePeripheralServicesDiscover(\n this,\n services\n );\n }\n async onNoblePeripheralServicesDiscover(\n noblePeripheral: NoblePeripheral,\n services: NobleService[]\n ) {\n _console.log(\n \"onNoblePeripheralServicesDiscover\",\n noblePeripheral.id,\n services.map((service) => service.uuid)\n );\n for (const index in services) {\n const service = services[index];\n _console.log(\"service\", service.uuid);\n if (service.uuid == \"1800\") {\n _console.log(\"skipping 1800\");\n continue;\n }\n if (service.uuid == \"1801\") {\n _console.log(\"skipping 1801\");\n continue;\n }\n const serviceName = getServiceNameFromUUID(service.uuid)!;\n _console.assertWithError(\n serviceName,\n `no name found for service uuid \"${service.uuid}\"`\n );\n _console.log({ serviceName });\n this.#services.set(serviceName, service);\n service.name = serviceName;\n service.connectionManager = this;\n addEventListeners(service, this.#unboundNobleServiceListeners);\n await service.discoverCharacteristicsAsync();\n }\n }\n\n // NOBLE SERVICE\n #services: Map<BluetoothServiceName, NobleService> = new Map();\n\n #unboundNobleServiceListeners = {\n characteristicsDiscover: this.#onNobleServiceCharacteristicsDiscover,\n };\n\n async #onNobleServiceCharacteristicsDiscover(\n this: NobleService,\n characteristics: NobleCharacteristic[]\n ) {\n await this.connectionManager!.onNobleServiceCharacteristicsDiscover(\n this,\n characteristics\n );\n }\n async onNobleServiceCharacteristicsDiscover(\n service: NobleService,\n characteristics: NobleCharacteristic[]\n ) {\n _console.log(\n \"onNobleServiceCharacteristicsDiscover\",\n service.uuid,\n characteristics.map((characteristic) => characteristic.uuid)\n );\n\n for (const index in characteristics) {\n const characteristic = characteristics[index];\n _console.log(\"characteristic\", characteristic.uuid);\n const characteristicName = getCharacteristicNameFromUUID(\n characteristic.uuid\n )!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic uuid \"${characteristic.uuid}\"`\n );\n _console.log({ characteristicName });\n this.#characteristics.set(characteristicName, characteristic);\n characteristic.name = characteristicName;\n characteristic.connectionManager = this;\n _console.log(\n `adding listeners to characteristic \"${characteristic.name}\" (currently has ${characteristic.listeners.length} listeners)`\n );\n addEventListeners(\n characteristic,\n this.#unboundNobleCharacteristicListeners\n );\n if (characteristic.properties.includes(\"read\")) {\n await characteristic.readAsync();\n }\n if (characteristic.properties.includes(\"notify\")) {\n await characteristic.subscribeAsync();\n }\n }\n\n if (this.#hasAllCharacteristics) {\n this.status = \"connected\";\n }\n }\n\n // NOBLE CHARACTERISRTIC\n #unboundNobleCharacteristicListeners = {\n data: this.#onNobleCharacteristicData,\n write: this.#onNobleCharacteristicWrite,\n notify: this.#onNobleCharacteristicNotify,\n };\n\n #characteristics: Map<BluetoothCharacteristicName, NobleCharacteristic> =\n new Map();\n\n get #hasAllCharacteristics() {\n return allCharacteristicNames.every((characteristicName) => {\n if (characteristicName == \"smp\") {\n return true;\n }\n return this.#characteristics.has(characteristicName);\n });\n }\n\n #onNobleCharacteristicData(\n this: NobleCharacteristic,\n data: Buffer,\n isNotification: boolean\n ) {\n this.connectionManager!.onNobleCharacteristicData(\n this,\n data,\n isNotification\n );\n }\n onNobleCharacteristicData(\n characteristic: NobleCharacteristic,\n data: Buffer,\n isNotification: boolean\n ) {\n _console.log(\n \"onNobleCharacteristicData\",\n characteristic.uuid,\n data,\n isNotification\n );\n const dataView = new DataView(dataToArrayBuffer(data));\n\n const characteristicName: BluetoothCharacteristicName = characteristic.name;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic with uuid \"${characteristic.uuid}\"`\n );\n\n this.onCharacteristicValueChanged(characteristicName, dataView);\n }\n\n #onNobleCharacteristicWrite(this: NobleCharacteristic) {\n this.connectionManager!.onNobleCharacteristicWrite(this);\n }\n onNobleCharacteristicWrite(characteristic: NobleCharacteristic) {\n _console.log(\"onNobleCharacteristicWrite\", characteristic.uuid);\n // TODO: - can this be useful?\n }\n\n #onNobleCharacteristicNotify(\n this: NobleCharacteristic,\n isSubscribed: boolean\n ) {\n this.connectionManager!.onNobleCharacteristicNotify(this, isSubscribed);\n }\n onNobleCharacteristicNotify(\n characteristic: NobleCharacteristic,\n isSubscribed: boolean\n ) {\n _console.log(\n \"onNobleCharacteristicNotify\",\n characteristic.uuid,\n isSubscribed\n );\n }\n\n remove() {\n super.remove();\n this.noblePeripheral = undefined;\n }\n}\n\nexport default NobleConnectionManager;\n","import BaseScanner, {\n DiscoveredDevice,\n ScannerEventMap,\n} from \"./BaseScanner.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { addEventListeners } from \"../utils/EventUtils.ts\";\nimport {\n serviceDataUUID,\n serviceUUIDs,\n} from \"../connection/bluetooth/bluetoothUUIDs.ts\";\nimport Device from \"../Device.ts\";\nimport NobleConnectionManager, {\n NoblePeripheral,\n} from \"../connection/bluetooth/NobleConnectionManager.ts\";\n\nconst _console = createConsole(\"NobleScanner\", { log: false });\n\nlet isSupported = false;\nlet filterManually = true;\nconst filterServiceUuid = (serviceUUIDs[0] as string).replaceAll(\"-\", \"\");\n\n/** NODE_START */\nimport noble from \"@abandonware/noble\";\nimport { DeviceTypes } from \"../InformationManager.ts\";\nimport DeviceManager from \"../DeviceManager.ts\";\nimport { ClientConnectionType } from \"../connection/BaseConnectionManager.ts\";\nisSupported = true;\nimport os from \"os\";\nconst platform = os.platform();\nfilterManually = platform == \"linux\";\n_console.log({ platform, filterManually, filterServiceUuid });\n/** NODE_END */\n\nexport const NobleStates = [\n \"unknown\",\n \"resetting\",\n \"unsupported\",\n \"unauthorized\",\n \"poweredOff\",\n \"poweredOn\",\n] as const;\nexport type NobleState = (typeof NobleStates)[number];\n\nclass NobleScanner extends BaseScanner {\n static get isSupported() {\n return isSupported;\n }\n\n // SCANNING\n #_isScanning = false;\n get #isScanning() {\n return this.#_isScanning;\n }\n set #isScanning(newIsScanning) {\n _console.assertTypeWithError(newIsScanning, \"boolean\");\n if (this.isScanning == newIsScanning) {\n _console.log(\"duplicate isScanning assignment\");\n return;\n }\n this.#_isScanning = newIsScanning;\n this.dispatchEvent(\"isScanning\", { isScanning: this.isScanning });\n }\n get isScanning() {\n return this.#isScanning;\n }\n\n // NOBLE STATE\n #_nobleState: NobleState = \"unknown\";\n get #nobleState() {\n return this.#_nobleState;\n }\n set #nobleState(newNobleState) {\n _console.assertTypeWithError(newNobleState, \"string\");\n if (this.#nobleState == newNobleState) {\n _console.log(\"duplicate nobleState assignment\");\n return;\n }\n this.#_nobleState = newNobleState;\n _console.log({ newNobleState });\n this.dispatchEvent(\"isScanningAvailable\", {\n isScanningAvailable: this.isScanningAvailable,\n });\n }\n\n // NOBLE LISTENERS\n #boundNobleListeners = {\n scanStart: this.#onNobleScanStart.bind(this),\n scanStop: this.#onNobleScanStop.bind(this),\n stateChange: this.#onNobleStateChange.bind(this),\n discover: this.#onNobleDiscover.bind(this),\n };\n #onNobleScanStart() {\n _console.log(\"OnNobleScanStart\");\n this.#isScanning = true;\n }\n #onNobleScanStop() {\n _console.log(\"OnNobleScanStop\");\n this.#isScanning = false;\n }\n #onNobleStateChange(state: NobleState) {\n _console.log(\"onNobleStateChange\", state);\n this.#nobleState = state;\n }\n #onNobleDiscover(noblePeripheral: NoblePeripheral) {\n _console.log(\"advertisement\", noblePeripheral.advertisement);\n if (filterManually) {\n const serviceUuid = noblePeripheral.advertisement.serviceUuids?.[0];\n _console.log(\"onNobleDiscover.filterManually\", { serviceUuid });\n if (serviceUuid != filterServiceUuid) {\n return;\n }\n }\n _console.log(\"onNobleDiscover\", noblePeripheral.id);\n if (!this.#noblePeripherals[noblePeripheral.id]) {\n noblePeripheral.scanner = this;\n this.#noblePeripherals[noblePeripheral.id] = noblePeripheral;\n }\n\n _console.log(\"advertisement\", noblePeripheral.advertisement);\n\n let deviceType;\n let ipAddress;\n let isWifiSecure;\n const { manufacturerData, serviceData } = noblePeripheral.advertisement;\n if (manufacturerData) {\n _console.log(\"manufacturerData\", manufacturerData);\n if (manufacturerData.byteLength >= 3) {\n const deviceTypeEnum = manufacturerData.readUint8(2);\n deviceType = DeviceTypes[deviceTypeEnum];\n _console;\n }\n if (manufacturerData.byteLength >= 3 + 4) {\n ipAddress = new Uint8Array(\n manufacturerData.buffer.slice(3, 3 + 4)\n ).join(\".\");\n _console.log({ ipAddress });\n }\n if (manufacturerData.byteLength >= 3 + 4 + 1) {\n isWifiSecure = manufacturerData.readUint8(3 + 4) != 0;\n _console.log({ isWifiSecure });\n }\n }\n if (serviceData) {\n _console.log(\"serviceData\", serviceData);\n const deviceTypeServiceData = serviceData.find((serviceDatum) => {\n return serviceDatum.uuid == serviceDataUUID;\n });\n _console.log(\"deviceTypeServiceData\", deviceTypeServiceData);\n if (deviceTypeServiceData) {\n const deviceTypeEnum = deviceTypeServiceData.data.readUint8(0);\n deviceType = DeviceTypes[deviceTypeEnum];\n }\n }\n if (deviceType == undefined) {\n _console.log(\"skipping device - no deviceType\");\n return;\n }\n\n const discoveredDevice: DiscoveredDevice = {\n name: noblePeripheral.advertisement.localName,\n bluetoothId: noblePeripheral.id,\n deviceType,\n rssi: noblePeripheral.rssi,\n ipAddress,\n isWifiSecure,\n };\n this.dispatchEvent(\"discoveredDevice\", { discoveredDevice });\n }\n\n // CONSTRUCTOR\n constructor() {\n super();\n addEventListeners(noble, this.#boundNobleListeners);\n addEventListeners(this, this.#boundBaseScannerListeners);\n }\n\n // AVAILABILITY\n get isScanningAvailable() {\n return this.#nobleState == \"poweredOn\";\n }\n\n // SCANNING\n startScan() {\n if (!super.startScan()) {\n return false;\n }\n noble.startScanningAsync(\n filterManually ? [] : (serviceUUIDs as string[]),\n true\n );\n return true;\n }\n stopScan() {\n if (!super.stopScan()) {\n return false;\n }\n noble.stopScanningAsync();\n return true;\n }\n\n // RESET\n get canReset() {\n return true;\n }\n reset() {\n super.reset();\n noble.reset();\n }\n\n // BASESCANNER LISTENERS\n #boundBaseScannerListeners = {\n expiredDiscoveredDevice: this.#onExpiredDiscoveredDevice.bind(this),\n };\n\n #onExpiredDiscoveredDevice(\n event: ScannerEventMap[\"expiredDiscoveredDevice\"]\n ) {\n const { discoveredDevice } = event.message;\n const noblePeripheral =\n this.#noblePeripherals[discoveredDevice.bluetoothId];\n if (noblePeripheral) {\n // disconnect?\n delete this.#noblePeripherals[discoveredDevice.bluetoothId];\n }\n }\n\n // DISCOVERED DEVICES\n #noblePeripherals: { [bluetoothId: string]: NoblePeripheral } = {};\n #assertValidNoblePeripheralId(noblePeripheralId: string) {\n _console.assertTypeWithError(noblePeripheralId, \"string\");\n _console.assertWithError(\n this.#noblePeripherals[noblePeripheralId],\n `no noblePeripheral found with id \"${noblePeripheralId}\"`\n );\n }\n\n // DEVICES\n #devices: { [bluetoothId: string]: Device } = {};\n async connectToDevice(\n deviceId: string,\n connectionType?: ClientConnectionType\n ) {\n super.connectToDevice(deviceId, connectionType);\n this.#assertValidNoblePeripheralId(deviceId);\n const noblePeripheral = this.#noblePeripherals[deviceId];\n _console.log(\"connecting to discoveredDevice...\", deviceId);\n\n let device = DeviceManager.AvailableDevices.filter(\n (device) => device.connectionType == \"noble\"\n ).find((device) => device.bluetoothId == deviceId);\n device = device ?? this.#devices[deviceId];\n\n if (!device) {\n _console.log(\"creating device for discoveredDevice...\", deviceId);\n device = this.#createDevice(noblePeripheral);\n this.#devices[deviceId] = device;\n const { ipAddress, isWifiSecure } =\n this.discoveredDevices[device.bluetoothId!];\n if (connectionType && connectionType != \"noble\" && ipAddress) {\n await device.connect({ type: connectionType, ipAddress, isWifiSecure });\n } else {\n await device.connect();\n }\n } else {\n const { ipAddress, isWifiSecure } =\n this.discoveredDevices[device.bluetoothId!];\n if (\n connectionType &&\n connectionType != \"noble\" &&\n connectionType != device.connectionType &&\n ipAddress\n ) {\n await device.connect({ type: connectionType, ipAddress, isWifiSecure });\n } else {\n await device.reconnect();\n }\n }\n }\n\n #createDevice(noblePeripheral: NoblePeripheral) {\n const device = new Device();\n const nobleConnectionManager = new NobleConnectionManager();\n nobleConnectionManager.noblePeripheral = noblePeripheral;\n device.connectionManager = nobleConnectionManager;\n return device;\n }\n}\n\nexport default NobleScanner;\n","import { createConsole } from \"../utils/Console.ts\";\nimport NobleScanner from \"./NobleScanner.ts\";\nimport BaseScanner from \"./BaseScanner.ts\";\n\nconst _console = createConsole(\"Scanner\", { log: false });\n\nlet scanner: BaseScanner | undefined;\n\nif (NobleScanner.isSupported) {\n _console.log(\"using NobleScanner\");\n scanner = new NobleScanner() as BaseScanner;\n} else {\n _console.log(\"Scanner not available\");\n}\n\nexport default scanner;\n","import { createConsole } from \"../utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport {\n createServerMessage,\n ServerMessageTypes,\n DeviceMessage,\n ServerMessage,\n ServerMessageType,\n createDeviceMessage,\n} from \"./ServerUtils.ts\";\nimport Device, {\n BoundDeviceEventListeners,\n DeviceEventMap,\n DeviceEventType,\n RequiredInformationConnectionMessages,\n} from \"../Device.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../utils/EventUtils.ts\";\nimport scanner from \"../scanner/Scanner.ts\";\nimport { parseMessage, parseStringFromDataView } from \"../utils/ParseUtils.ts\";\nimport {\n ConnectionMessageType,\n ConnectionMessageTypes,\n ConnectionType,\n ConnectionTypes,\n} from \"../connection/BaseConnectionManager.ts\";\nimport {\n BoundScannerEventListeners,\n DiscoveredDevice,\n ScannerEventMap,\n} from \"../scanner/BaseScanner.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport DeviceManager, {\n DeviceManagerEventMap,\n BoundDeviceManagerEventListeners,\n} from \"../DeviceManager.ts\";\nimport { RequiredWifiMessageTypes } from \"../WifiManager.ts\";\nimport { DeviceInformationTypes } from \"../DeviceInformationManager.ts\";\n\nconst RequiredDeviceInformationMessageTypes: ConnectionMessageType[] = [\n ...DeviceInformationTypes,\n \"batteryLevel\",\n ...RequiredInformationConnectionMessages,\n];\n\nconst _console = createConsole(\"BaseServer\", { log: false });\n\nexport const ServerEventTypes = [\n \"clientConnected\",\n \"clientDisconnected\",\n] as const;\nexport type ServerEventType = (typeof ServerEventTypes)[number];\n\ninterface ServerEventMessages {\n clientConnected: { client: any };\n clientDisconnected: { client: any };\n}\n\nexport type ServerEventDispatcher = EventDispatcher<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type ServerEvent = Event<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type ServerEventMap = EventMap<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type BoundServerEventListeners = BoundEventListeners<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\n\nabstract class BaseServer {\n // EVENT DISPATCHER\n protected eventDispatcher: ServerEventDispatcher = new EventDispatcher(\n this as BaseServer,\n ServerEventTypes\n );\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n protected get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // CONSTRUCTOR\n\n constructor() {\n _console.assertWithError(scanner, \"no scanner defined\");\n\n addEventListeners(scanner, this.#boundScannerListeners);\n addEventListeners(DeviceManager, this.#boundDeviceManagerListeners);\n addEventListeners(this, this.#boundServerListeners);\n }\n\n get numberOfClients() {\n return 0;\n }\n\n static #ClearSensorConfigurationsWhenNoClients = true;\n static get ClearSensorConfigurationsWhenNoClients() {\n return this.#ClearSensorConfigurationsWhenNoClients;\n }\n static set ClearSensorConfigurationsWhenNoClients(newValue) {\n _console.assertTypeWithError(newValue, \"boolean\");\n this.#ClearSensorConfigurationsWhenNoClients = newValue;\n }\n\n #clearSensorConfigurationsWhenNoClients =\n BaseServer.#ClearSensorConfigurationsWhenNoClients;\n get clearSensorConfigurationsWhenNoClients() {\n return this.#clearSensorConfigurationsWhenNoClients;\n }\n set clearSensorConfigurationsWhenNoClients(newValue) {\n _console.assertTypeWithError(newValue, \"boolean\");\n this.#clearSensorConfigurationsWhenNoClients = newValue;\n }\n\n // SERVER LISTENERS\n #boundServerListeners: BoundServerEventListeners = {\n clientConnected: this.#onClientConnected.bind(this),\n clientDisconnected: this.#onClientDisconnected.bind(this),\n };\n #onClientConnected(event: ServerEventMap[\"clientConnected\"]) {\n const client = event.message.client;\n _console.log(\"onClientConnected\");\n }\n #onClientDisconnected(event: ServerEventMap[\"clientDisconnected\"]) {\n const client = event.message.client;\n _console.log(\"onClientDisconnected\");\n if (\n this.numberOfClients == 0 &&\n this.clearSensorConfigurationsWhenNoClients\n ) {\n DeviceManager.ConnectedDevices.forEach((device) => {\n device.clearSensorConfiguration();\n device.setTfliteInferencingEnabled(false);\n });\n }\n }\n\n // CLIENT MESSAGING\n broadcastMessage(message: ArrayBuffer) {\n _console.log(\"broadcasting\", message);\n }\n\n // SCANNER\n #boundScannerListeners: BoundScannerEventListeners = {\n isScanningAvailable: this.#onScannerIsAvailable.bind(this),\n isScanning: this.#onScannerIsScanning.bind(this),\n discoveredDevice: this.#onScannerDiscoveredDevice.bind(this),\n expiredDiscoveredDevice: this.#onExpiredDiscoveredDevice.bind(this),\n };\n\n #onScannerIsAvailable(event: ScannerEventMap[\"isScanningAvailable\"]) {\n this.broadcastMessage(this.#isScanningAvailableMessage);\n }\n get #isScanningAvailableMessage() {\n return createServerMessage({\n type: \"isScanningAvailable\",\n data: scanner!.isScanningAvailable,\n });\n }\n\n #onScannerIsScanning(event: ScannerEventMap[\"isScanning\"]) {\n this.broadcastMessage(this.#isScanningMessage);\n }\n get #isScanningMessage() {\n return createServerMessage({\n type: \"isScanning\",\n data: scanner!.isScanning,\n });\n }\n\n #onScannerDiscoveredDevice(event: ScannerEventMap[\"discoveredDevice\"]) {\n const { discoveredDevice } = event.message;\n _console.log(discoveredDevice);\n\n this.broadcastMessage(\n this.#createDiscoveredDeviceMessage(discoveredDevice)\n );\n }\n #createDiscoveredDeviceMessage(discoveredDevice: DiscoveredDevice) {\n return createServerMessage({\n type: \"discoveredDevice\",\n data: discoveredDevice,\n });\n }\n\n #onExpiredDiscoveredDevice(\n event: ScannerEventMap[\"expiredDiscoveredDevice\"]\n ) {\n const { discoveredDevice } = event.message;\n _console.log(\"expired\", discoveredDevice);\n this.broadcastMessage(\n this.#createExpiredDiscoveredDeviceMessage(discoveredDevice)\n );\n }\n #createExpiredDiscoveredDeviceMessage(discoveredDevice: DiscoveredDevice) {\n return createServerMessage({\n type: \"expiredDiscoveredDevice\",\n data: discoveredDevice.bluetoothId,\n });\n }\n\n get #discoveredDevicesMessage() {\n const serverMessages: ServerMessage[] = scanner!.discoveredDevicesArray\n .filter((discoveredDevice) => {\n const existingConnectedDevice = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == discoveredDevice.bluetoothId\n );\n return !existingConnectedDevice;\n })\n .map((discoveredDevice) => {\n return { type: \"discoveredDevice\", data: discoveredDevice };\n });\n return createServerMessage(...serverMessages);\n }\n\n get #connectedDevicesMessage() {\n return createServerMessage({\n type: \"connectedDevices\",\n data: JSON.stringify({\n connectedDevices: DeviceManager.ConnectedDevices.map(\n (device) => device.bluetoothId\n ),\n }),\n });\n }\n\n // DEVICE LISTENERS\n\n #boundDeviceListeners: BoundDeviceEventListeners = {\n connectionMessage: this.#onDeviceConnectionMessage.bind(this),\n };\n\n #createDeviceMessage(\n device: Device,\n messageType: ConnectionMessageType,\n dataView?: DataView\n ): DeviceMessage {\n return {\n type: messageType as DeviceEventType,\n data: dataView || device.latestConnectionMessages.get(messageType),\n };\n }\n\n #onDeviceConnectionMessage(deviceEvent: DeviceEventMap[\"connectionMessage\"]) {\n const { target: device, message } = deviceEvent;\n _console.log(\"onDeviceConnectionMessage\", deviceEvent.message);\n\n if (!device.isConnected) {\n return;\n }\n\n const { messageType, dataView } = message;\n\n this.broadcastMessage(\n this.#createDeviceServerMessage(\n device,\n this.#createDeviceMessage(device, messageType, dataView)\n )\n );\n }\n\n // STATIC DEVICE LISTENERS\n #boundDeviceManagerListeners: BoundDeviceManagerEventListeners = {\n deviceConnected: this.#onDeviceConnected.bind(this),\n deviceDisconnected: this.#onDeviceDisconnected.bind(this),\n deviceIsConnected: this.#onDeviceIsConnected.bind(this),\n };\n\n #onDeviceConnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceConnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceConnected\", device.bluetoothId);\n addEventListeners(device, this.#boundDeviceListeners);\n device.isServerSide = true;\n }\n\n #onDeviceDisconnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceDisconnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceDisconnected\", device.bluetoothId);\n removeEventListeners(device, this.#boundDeviceListeners);\n }\n\n #onDeviceIsConnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceIsConnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceIsConnected\", device.bluetoothId);\n this.broadcastMessage(this.#createDeviceIsConnectedMessage(device));\n }\n #createDeviceIsConnectedMessage(device: Device) {\n return this.#createDeviceServerMessage(device, {\n type: \"isConnected\",\n data: device.isConnected,\n });\n }\n\n #createDeviceServerMessage(device: Device, ...messages: DeviceMessage[]) {\n return createServerMessage({\n type: \"deviceMessage\",\n data: [device.bluetoothId!, createDeviceMessage(...messages)],\n });\n }\n\n // PARSING\n protected parseClientMessage(dataView: DataView) {\n let responseMessages: ArrayBuffer[] = [];\n\n parseMessage(\n dataView,\n ServerMessageTypes,\n this.#onClientMessage.bind(this),\n { responseMessages },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length > 0) {\n return concatenateArrayBuffers(responseMessages);\n }\n }\n\n #onClientMessage(\n messageType: ServerMessageType,\n dataView: DataView,\n context: { responseMessages: ArrayBuffer[] }\n ) {\n _console.log(\n `onClientMessage \"${messageType}\" (${dataView.byteLength} bytes)`\n );\n const { responseMessages } = context;\n switch (messageType) {\n case \"isScanningAvailable\":\n responseMessages.push(this.#isScanningAvailableMessage);\n break;\n case \"isScanning\":\n responseMessages.push(this.#isScanningMessage);\n break;\n case \"startScan\":\n scanner!.startScan();\n break;\n case \"stopScan\":\n scanner!.stopScan();\n break;\n case \"discoveredDevices\":\n responseMessages.push(this.#discoveredDevicesMessage);\n break;\n case \"connectToDevice\":\n {\n const { string: deviceId, byteOffset } =\n parseStringFromDataView(dataView);\n let connectionType = undefined;\n if (byteOffset < dataView.byteLength) {\n connectionType = ConnectionTypes[dataView.getUint8(byteOffset)];\n _console.log(`connectToDevice via ${connectionType}`);\n }\n scanner!.connectToDevice(deviceId, connectionType);\n }\n break;\n case \"disconnectFromDevice\":\n {\n const { string: deviceId } = parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n device.disconnect();\n }\n break;\n case \"connectedDevices\":\n responseMessages.push(this.#connectedDevicesMessage);\n break;\n case \"deviceMessage\":\n {\n const { string: deviceId, byteOffset } =\n parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n const _dataView = new DataView(\n dataView.buffer,\n dataView.byteOffset + byteOffset\n );\n const responseMessage = this.parseClientDeviceMessage(\n device,\n _dataView\n );\n if (responseMessage) {\n responseMessages.push(responseMessage);\n }\n }\n break;\n case \"requiredDeviceInformation\":\n {\n const { string: deviceId } = parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n\n const messages = RequiredDeviceInformationMessageTypes.map(\n (messageType) => this.#createDeviceMessage(device, messageType)\n );\n if (device.isWifiAvailable) {\n RequiredWifiMessageTypes.forEach((messageType) => {\n messages.push(this.#createDeviceMessage(device, messageType));\n });\n }\n const responseMessage = this.#createDeviceServerMessage(\n device,\n ...messages\n );\n if (responseMessage) {\n responseMessages.push(responseMessage);\n }\n }\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n protected parseClientDeviceMessage(device: Device, dataView: DataView) {\n _console.log(\"onDeviceMessage\", device.bluetoothId, dataView);\n\n let responseMessages: DeviceMessage[] = [];\n\n parseMessage(\n dataView,\n ConnectionMessageTypes,\n this.#parseClientDeviceMessageCallback.bind(this),\n { responseMessages, device },\n true\n );\n\n if (responseMessages.length > 0) {\n return this.#createDeviceServerMessage(device, ...responseMessages);\n }\n }\n\n #parseClientDeviceMessageCallback(\n messageType: ConnectionMessageType,\n dataView: DataView,\n context: { responseMessages: DeviceMessage[]; device: Device }\n ) {\n _console.log(\n `clientDeviceMessage ${messageType} (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"smp\":\n context.device.connectionManager!.sendSmpMessage(dataView.buffer);\n break;\n case \"tx\":\n context.device.connectionManager!.sendTxData(dataView.buffer);\n break;\n default:\n context.responseMessages.push(\n this.#createDeviceMessage(context.device, messageType)\n );\n break;\n }\n }\n}\n\nexport default BaseServer;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n concatenateArrayBuffers,\n dataToArrayBuffer,\n} from \"../../utils/ArrayBufferUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseServer from \"../BaseServer.ts\";\nimport {\n webSocketPingMessage,\n webSocketPongMessage,\n WebSocketMessageType,\n WebSocketMessageTypes,\n webSocketPingTimeout,\n createWebSocketMessage,\n} from \"./WebSocketUtils.ts\";\n\nconst _console = createConsole(\"WebSocketServer\", { log: false });\n\n/** NODE_START */\nimport type * as ws from \"ws\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\n/** NODE_END */\n\ninterface WebSocketClient extends ws.WebSocket {\n isAlive: boolean;\n pingClientTimer?: Timer;\n}\ninterface WebSocketServer extends ws.WebSocketServer {}\n\nclass WebSocketServer extends BaseServer {\n get numberOfClients() {\n return this.#server?.clients.size || 0;\n }\n\n // WEBSOCKET SERVER\n\n #server?: WebSocketServer;\n get server() {\n return this.#server;\n }\n set server(newServer) {\n if (this.#server == newServer) {\n _console.log(\"redundant WebSocket server assignment\");\n return;\n }\n _console.log(\"assigning WebSocket server...\");\n\n if (this.#server) {\n _console.log(\"clearing existing WebSocket server...\");\n removeEventListeners(this.#server, this.#boundWebSocketServerListeners);\n }\n\n addEventListeners(newServer, this.#boundWebSocketServerListeners);\n this.#server = newServer;\n\n _console.log(\"assigned WebSocket server\");\n }\n\n // WEBSOCKET SERVER LISTENERS\n\n #boundWebSocketServerListeners = {\n close: this.#onWebSocketServerClose.bind(this),\n connection: this.#onWebSocketServerConnection.bind(this),\n error: this.#onWebSocketServerError.bind(this),\n headers: this.#onWebSocketServerHeaders.bind(this),\n listening: this.#onWebSocketServerListening.bind(this),\n };\n\n #onWebSocketServerClose() {\n _console.log(\"server.close\");\n }\n #onWebSocketServerConnection(client: WebSocketClient) {\n _console.log(\"server.connection\");\n client.isAlive = true;\n client.pingClientTimer = new Timer(\n () => this.#pingClient(client),\n webSocketPingTimeout\n );\n client.pingClientTimer.start();\n addEventListeners(client, this.#boundWebSocketClientListeners);\n this.dispatchEvent(\"clientConnected\", { client });\n }\n #onWebSocketServerError(error: Error) {\n _console.error(error);\n }\n #onWebSocketServerHeaders() {\n //_console.log(\"server.headers\");\n }\n #onWebSocketServerListening() {\n _console.log(\"server.listening\");\n }\n\n // WEBSOCKET CLIENT LISTENERS\n\n #boundWebSocketClientListeners: { [eventType: string]: Function } = {\n open: this.#onWebSocketClientOpen.bind(this),\n message: this.#onWebSocketClientMessage.bind(this),\n close: this.#onWebSocketClientClose.bind(this),\n error: this.#onWebSocketClientError.bind(this),\n };\n #onWebSocketClientOpen(event: ws.Event) {\n _console.log(\"client.open\");\n }\n #onWebSocketClientMessage(event: ws.MessageEvent) {\n _console.log(\"client.message\");\n const client = event.target as WebSocketClient;\n client.isAlive = true;\n client.pingClientTimer!.restart();\n const dataView = new DataView(dataToArrayBuffer(event.data as Buffer));\n _console.log(`received ${dataView.byteLength} bytes`, dataView.buffer);\n this.#parseWebSocketClientMessage(client, dataView);\n }\n #onWebSocketClientClose(event: ws.CloseEvent) {\n _console.log(\"client.close\");\n const client = event.target as WebSocketClient;\n client.pingClientTimer!.stop();\n removeEventListeners(client, this.#boundWebSocketClientListeners);\n this.dispatchEvent(\"clientDisconnected\", { client });\n }\n #onWebSocketClientError(event: ws.ErrorEvent) {\n _console.error(\"client.error\", event.message);\n }\n\n // PARSING\n #parseWebSocketClientMessage(client: WebSocketClient, dataView: DataView) {\n let responseMessages: ArrayBuffer[] = [];\n\n parseMessage(\n dataView,\n WebSocketMessageTypes,\n this.#onClientMessage.bind(this),\n { responseMessages },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length == 0) {\n _console.log(\"nothing to send back\");\n return;\n }\n\n const responseMessage = concatenateArrayBuffers(responseMessages);\n _console.log(`sending ${responseMessage.byteLength} bytes to client...`);\n try {\n client.send(responseMessage);\n } catch (error) {\n _console.log(\"error sending message\", error);\n }\n }\n\n #onClientMessage(\n messageType: WebSocketMessageType,\n dataView: DataView,\n context: { responseMessages: (ArrayBuffer | undefined)[] }\n ) {\n const { responseMessages } = context;\n switch (messageType) {\n case \"ping\":\n responseMessages.push(webSocketPongMessage);\n break;\n case \"pong\":\n break;\n case \"serverMessage\":\n const responseMessage = this.parseClientMessage(dataView);\n if (responseMessage) {\n responseMessages.push(\n createWebSocketMessage({\n type: \"serverMessage\",\n data: responseMessage,\n })\n );\n }\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // CLIENT MESSAGING\n broadcastMessage(message: ArrayBuffer) {\n super.broadcastMessage(message);\n this.server!.clients.forEach((client) => {\n client.send(\n createWebSocketMessage({ type: \"serverMessage\", data: message })\n );\n });\n }\n\n // PING\n #pingClient(client: WebSocketClient) {\n if (!client.isAlive) {\n client.terminate();\n return;\n }\n client.isAlive = false;\n client.send(webSocketPingMessage);\n }\n}\n\nexport default WebSocketServer;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport { createMessage, Message } from \"../ServerUtils.ts\";\n\nconst _console = createConsole(\"UDPUtils\", { log: false });\n\nexport const pongUDPClientTimeout = 2_000;\nexport const removeUDPClientTimeout = 4_000;\n\nexport const UDPServerMessageTypes = [\n \"ping\",\n \"pong\",\n \"setRemoteReceivePort\",\n \"serverMessage\",\n] as const;\nexport type UDPServerMessageType = (typeof UDPServerMessageTypes)[number];\n\nexport type UDPServerMessage =\n | UDPServerMessageType\n | Message<UDPServerMessageType>;\nexport function createUDPServerMessage(...messages: UDPServerMessage[]) {\n _console.log(\"createUDPServerMessage\", ...messages);\n return createMessage(UDPServerMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const udpPingMessage = createUDPServerMessage(\"ping\");\nexport const udpPongMessage = createUDPServerMessage(\"pong\");\n","import {\n concatenateArrayBuffers,\n dataToArrayBuffer,\n} from \"../../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport BaseServer from \"../BaseServer.ts\";\nimport {\n createUDPServerMessage,\n pongUDPClientTimeout,\n removeUDPClientTimeout,\n udpPongMessage,\n UDPServerMessageType,\n UDPServerMessageTypes,\n} from \"./UDPUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\n\n/** NODE_START */\nimport type * as dgram from \"dgram\";\n/** NODE_END */\n\nconst _console = createConsole(\"UDPServer\", { log: false });\n\ninterface UDPClient extends dgram.RemoteInfo {\n receivePort?: number;\n isAlive?: boolean;\n removeSelfTimer: Timer;\n lastTimeSentData: number;\n}\n\ninterface UDPClientContext {\n client: UDPClient;\n responseMessages: (ArrayBuffer | undefined)[];\n}\n\nclass UDPServer extends BaseServer {\n // CLIENTS\n #clients: UDPClient[] = [];\n get numberOfClients() {\n return this.#clients.length;\n }\n\n #getClientByRemoteInfo(\n remoteInfo: dgram.RemoteInfo,\n createIfNotFound = false\n ) {\n const { address, port } = remoteInfo;\n let client = this.#clients.find(\n (client) => client.address == address && client.port == port\n );\n if (!client && createIfNotFound) {\n client = {\n ...remoteInfo,\n isAlive: true,\n removeSelfTimer: new Timer(() => {\n _console.log(\"removing client due to timeout...\");\n this.#removeClient(client!);\n }, removeUDPClientTimeout),\n lastTimeSentData: 0,\n };\n _console.log(\"created new client\", client);\n\n this.#clients.push(client);\n _console.log(`currently have ${this.numberOfClients} clients`);\n this.dispatchEvent(\"clientConnected\", { client });\n }\n return client;\n }\n\n #remoteInfoToString(client: dgram.RemoteInfo) {\n const { address, port } = client;\n return `${address}:${port}`;\n }\n #clientToString(client: UDPClient) {\n const { address, port, receivePort } = client;\n return `${address}:${port}=>${receivePort}`;\n }\n\n // UDP SOCKET\n\n #socket?: dgram.Socket;\n get socket() {\n return this.#socket;\n }\n set socket(newSocket) {\n if (this.#socket == newSocket) {\n _console.log(\"redundant udp socket assignment\");\n return;\n }\n _console.log(\"assigning udp socket...\");\n\n if (this.#socket) {\n _console.log(\"clearing existing udp socket...\");\n removeEventListeners(this.#socket, this.#boundSocketListeners);\n }\n\n addEventListeners(newSocket, this.#boundSocketListeners);\n this.#socket = newSocket;\n\n _console.log(\"assigned udp socket\");\n }\n\n // UDP SOCKET LISTENERS\n\n #boundSocketListeners = {\n close: this.#onSocketClose.bind(this),\n connect: this.#onSocketConnect.bind(this),\n error: this.#onSocketError.bind(this),\n listening: this.#onSocketListening.bind(this),\n message: this.#onSocketMessage.bind(this),\n };\n\n #onSocketClose() {\n _console.log(\"socket close\");\n }\n #onSocketConnect() {\n _console.log(\"socket connect\");\n }\n #onSocketError(error: Error) {\n _console.error(\"socket error\", error);\n }\n #onSocketListening() {\n const address = this.#socket!.address();\n _console.log(`socket listening on port ${address.address}:${address.port}`);\n }\n #onSocketMessage(message: Buffer, remoteInfo: dgram.RemoteInfo) {\n _console.log(\n `received ${message.length} bytes from ${this.#remoteInfoToString(\n remoteInfo\n )}`\n );\n const client = this.#getClientByRemoteInfo(remoteInfo, true);\n if (!client) {\n _console.error(\"no client found\");\n return;\n }\n client.removeSelfTimer.restart();\n const dataView = new DataView(dataToArrayBuffer(message));\n this.#onClientData(client, dataView);\n }\n\n // PARSING\n #onClientData(client: UDPClient, dataView: DataView) {\n _console.log(\n `parsing ${dataView.byteLength} bytes from ${this.#clientToString(\n client\n )}`,\n dataView.buffer\n );\n let responseMessages: ArrayBuffer[] = [];\n parseMessage(\n dataView,\n UDPServerMessageTypes,\n this.#onClientUDPMessage.bind(this),\n { responseMessages, client },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length == 0) {\n _console.log(\"no response to send\");\n return;\n }\n\n if (client.receivePort == undefined) {\n _console.log(\"client has no defined receivePort\");\n return;\n }\n\n const response = concatenateArrayBuffers(responseMessages);\n _console.log(`responding with ${response.byteLength} bytes...`, response);\n this.#sendToClient(client, response);\n }\n #onClientUDPMessage(\n messageType: UDPServerMessageType,\n dataView: DataView,\n context: UDPClientContext\n ) {\n const { client, responseMessages } = context;\n _console.log(\n `received \"${messageType}\" message from ${client.address}:${client.port}`\n );\n switch (messageType) {\n case \"ping\":\n responseMessages.push(this.#createPongMessage(context));\n break;\n case \"pong\":\n break;\n case \"setRemoteReceivePort\":\n responseMessages.push(this.#parseRemoteReceivePort(dataView, client));\n break;\n case \"serverMessage\":\n const responseMessage = this.parseClientMessage(dataView);\n if (responseMessage) {\n responseMessages.push(\n createUDPServerMessage({\n type: \"serverMessage\",\n data: responseMessage,\n })\n );\n }\n break;\n\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n #createPongMessage(context: UDPClientContext) {\n const { client } = context;\n // TODO: - no need to ping if streaming sensor data\n return udpPongMessage;\n }\n\n #parseRemoteReceivePort(dataView: DataView, client: UDPClient) {\n const receivePort = dataView.getUint16(0);\n client.receivePort = receivePort;\n _console.log(\n `updated ${client.address}:${client.port} receivePort to ${receivePort}`\n );\n const responseDataView = new DataView(new ArrayBuffer(2));\n responseDataView.setUint16(0, client.receivePort);\n return createUDPServerMessage({\n type: \"setRemoteReceivePort\",\n data: responseDataView,\n });\n }\n\n // CLIENT MESSAGING\n #sendToClient(client: UDPClient, message: ArrayBuffer) {\n _console.log(\n `sending ${message.byteLength} bytes to ${this.#clientToString(\n client\n )}...`\n );\n try {\n this.#socket!.send(\n new Uint8Array(message),\n client.receivePort,\n client.address,\n (error, bytes) => {\n if (error) {\n _console.error(\"error sending data\", error);\n return;\n }\n _console.log(`sent ${bytes} bytes`);\n client.lastTimeSentData = Date.now();\n }\n );\n } catch (error) {\n _console.error(\"serious error sending data\", error);\n }\n }\n broadcastMessage(message: ArrayBuffer) {\n super.broadcastMessage(message);\n this.#clients.forEach((client) => {\n this.#sendToClient(\n client,\n createUDPServerMessage({ type: \"serverMessage\", data: message })\n );\n });\n }\n\n // REMOVE CLIENT\n #removeClient(client: UDPClient) {\n _console.log(`removing client ${this.#clientToString(client)}...`);\n client.removeSelfTimer.stop();\n this.#clients = this.#clients.filter((_client) => _client != client);\n _console.log(`currently have ${this.numberOfClients} clients`);\n this.dispatchEvent(\"clientDisconnected\", { client });\n }\n}\n\nexport default UDPServer;\n","export {\n setAllConsoleLevelFlags,\n setConsoleLevelFlagsForType,\n} from \"./utils/Console.ts\";\nexport * as Environment from \"./utils/environment.ts\";\nexport { Vector2, Vector3, Quaternion, Euler } from \"./utils/MathUtils.ts\";\n\nexport {\n default as Device,\n DeviceEvent,\n DeviceEventMap,\n DeviceEventListenerMap,\n BoundDeviceEventListeners,\n} from \"./Device.ts\";\nexport {\n default as DeviceManager,\n DeviceManagerEvent,\n DeviceManagerEventMap,\n DeviceManagerEventListenerMap,\n BoundDeviceManagerEventListeners,\n} from \"./DeviceManager.ts\";\n\nexport { DeviceInformation } from \"./DeviceInformationManager.ts\";\nexport {\n DeviceType,\n DeviceTypes,\n MinNameLength,\n MaxNameLength,\n Sides,\n Side,\n} from \"./InformationManager.ts\";\nexport {\n MinWifiSSIDLength,\n MaxWifiSSIDLength,\n MinWifiPasswordLength,\n MaxWifiPasswordLength,\n} from \"./WifiManager.ts\";\nexport {\n SensorType,\n SensorTypes,\n ContinuousSensorType,\n ContinuousSensorTypes,\n} from \"./sensor/SensorDataManager.ts\";\nexport {\n MaxSensorRate,\n SensorRateStep,\n SensorConfiguration,\n} from \"./sensor/SensorConfigurationManager.ts\";\n\nexport {\n DefaultNumberOfPressureSensors,\n PressureData,\n} from \"./sensor/PressureSensorDataManager.ts\";\nexport { CenterOfPressure } from \"./utils/CenterOfPressureHelper.ts\";\nexport {\n VibrationConfiguration,\n VibrationLocation,\n VibrationLocations,\n VibrationType,\n VibrationTypes,\n MaxNumberOfVibrationWaveformEffectSegments,\n MaxVibrationWaveformSegmentDuration,\n MaxVibrationWaveformEffectSegmentDelay,\n MaxVibrationWaveformEffectSegmentLoopCount,\n MaxNumberOfVibrationWaveformSegments,\n MaxVibrationWaveformEffectSequenceLoopCount,\n} from \"./vibration/VibrationManager.ts\";\nexport {\n VibrationWaveformEffect,\n VibrationWaveformEffects,\n} from \"./vibration/VibrationWaveformEffects.ts\";\n\nexport {\n FileType,\n FileTypes,\n FileTransferDirection,\n FileTransferDirections,\n} from \"./FileTransferManager.ts\";\nexport {\n TfliteSensorType,\n TfliteSensorTypes,\n TfliteTask,\n TfliteTasks,\n TfliteFileConfiguration as TfliteFileConfiguration,\n} from \"./TfliteManager.ts\";\n\nexport {\n CameraConfiguration,\n CameraCommand,\n CameraCommands,\n CameraConfigurationType,\n CameraConfigurationTypes,\n} from \"./CameraManager.ts\";\n\nexport {\n MicrophoneConfiguration,\n MicrophoneCommand,\n MicrophoneCommands,\n MicrophoneConfigurationType,\n MicrophoneConfigurationTypes,\n MicrophoneConfigurationValues,\n} from \"./MicrophoneManager.ts\";\n\nexport {\n DisplayBrightness,\n DisplayBrightnesses,\n DisplaySize,\n DisplayBitmapColorPair,\n DisplayPixelDepths,\n DefaultNumberOfDisplayColors,\n MinSpriteSheetNameLength,\n MaxSpriteSheetNameLength,\n DisplayBitmap,\n DisplaySpriteColorPair,\n DisplayWireframeEdge,\n DisplayWireframe,\n DisplayBezierCurveType,\n DisplayBezierCurveTypes,\n} from \"./DisplayManager.ts\";\n\nexport { wait, Timer } from \"./utils/Timer.ts\";\n\nexport {\n DisplaySegmentCap,\n DisplaySegmentCaps,\n DisplayAlignment,\n DisplayAlignments,\n DisplayDirection,\n DisplayDirections,\n} from \"./utils/DisplayContextState.ts\";\n\nexport {\n maxDisplayScale,\n DisplayColorRGB,\n pixelDepthToNumberOfColors,\n displayCurveTypeToNumberOfControlPoints,\n mergeWireframes,\n intersectWireframes,\n isWireframePolygon,\n} from \"./utils/DisplayUtils.ts\";\n\n/** BROWSER_START */\nexport {\n svgToDisplayContextCommands,\n svgToSprite,\n svgToSpriteSheet,\n isValidSVG,\n getSvgStringFromDataUrl,\n} from \"./utils/SvgUtils.ts\";\n/** BROWSER_END */\n\nexport {\n DisplayContextCommand,\n DisplayContextCommandType,\n DisplayContextCommandTypes,\n DisplaySpriteContextCommandType,\n DisplaySpriteContextCommandTypes,\n} from \"./utils/DisplayContextCommand.ts\";\n\nexport {\n simplifyPoints,\n simplifyCurves,\n simplifyPointsAsCubicCurveControlPoints,\n} from \"./utils/PathUtils.ts\";\n\nexport {\n DisplaySprite,\n DisplaySpriteSheet,\n DisplaySpriteSheetPalette,\n DisplaySpritePaletteSwap,\n parseFont,\n getFontUnicodeRange,\n stringToSprites,\n fontToSpriteSheet,\n getFontMetrics,\n DisplaySpriteSubLine,\n DisplaySpriteLine,\n DisplaySpriteLines,\n getFontMaxHeight,\n getMaxSpriteSheetSize,\n englishRegex,\n FontToSpriteSheetOptions,\n} from \"./utils/DisplaySpriteSheetUtils.ts\";\n\n/** BROWSER_START */\nexport {\n default as DisplayCanvasHelper,\n DisplayCanvasHelperEvent,\n DisplayCanvasHelperEventMap,\n DisplayCanvasHelperEventListenerMap,\n} from \"./utils/DisplayCanvasHelper.ts\";\n/** BROWSER_END */\n\n/** BROWSER_START */\nexport { Font, Glyph } from \"opentype.js\";\n/** BROWSER_END */\n\n/** BROWSER_START */\nexport {\n resizeAndQuantizeImage,\n quantizeImage,\n imageToSprite,\n imageToSpriteSheet,\n canvasToSprite,\n canvasToSpriteSheet,\n resizeImage,\n imageToBitmaps,\n canvasToBitmaps,\n} from \"./utils/DisplayBitmapUtils.ts\";\n/** BROWSER_END */\n\nexport { rgbToHex, hexToRGB } from \"./utils/ColorUtils.ts\";\n\nexport {\n default as DevicePair,\n DevicePairEvent,\n DevicePairEventMap,\n DevicePairEventListenerMap,\n BoundDevicePairEventListeners,\n DevicePairType,\n DevicePairTypes,\n} from \"./devicePair/DevicePair.ts\";\n\nimport { addEventListeners, removeEventListeners } from \"./utils/EventUtils.ts\";\nexport const EventUtils = {\n addEventListeners,\n removeEventListeners,\n};\n\nimport { throttle, debounce } from \"./utils/ThrottleUtils.ts\";\nexport const ThrottleUtils = {\n throttle,\n debounce,\n};\n\nexport { DiscoveredDevice } from \"./scanner/BaseScanner.ts\";\n/** NODE_START */\nexport { default as Scanner } from \"./scanner/Scanner.ts\";\nexport { default as WebSocketServer } from \"./server/websocket/WebSocketServer.ts\";\nexport { default as UDPServer } from \"./server/udp/UDPServer.ts\";\n/** NODE_END */\n/** BROWSER_START */\nexport { default as WebSocketClient } from \"./server/websocket/WebSocketClient.ts\";\n/** BROWSER_END */\n/** LS_START */\nexport { default as WebSocketClient } from \"./server/websocket/WebSocketClient.ts\";\n/** LS_END */\n\nexport { default as RangeHelper, Range } from \"./utils/RangeHelper.ts\";\n"],"names":["_console"],"mappings":"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GF;AACE;;;;;;;;;;;;;;;;;;AA+BF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACA;;AAEF;AACE;;AAEF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACE;AACA;;AAGF;AACA;AACA;;AAEF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACA;AACA;;;;AAKA;;;;AAKF;;;;;;;AAYI;;;AAIF;AACE;;;;AAIA;;;AAGF;AAEA;;AAGE;;;AAGA;;AAEF;AACA;AAEA;AACE;;AAMI;;;;;;AAMF;AACE;;AAIE;;;AAKF;AACA;AACF;;;AAIA;AACA;AACA;;;;AAGF;AACA;AACA;;AAGF;AAEA;;;AAKF;;AAGE;;AAGF;AACE;;;;;AAYA;;AAGF;;AAGE;AACE;AACA;;;;AAKF;AACA;;;;AAMA;AACA;AACA;;AAEH;;AC7ZD;AAqBM;AACJ;AACA;AACA;AACA;AACE;AACF;AACF;AAEM;AACJ;AACA;AACA;AACA;AACE;AACF;AACF;;ACjCA;AAIA;AAuCA;AACE;;;AAMF;AAEA;AAGE;AACF;AAEA;AACE;AACF;AA6BA;AACE;AACE;AACE;AACA;AACE;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACF;AACF;AACD;AACE;AACA;AACE;AACE;AACD;AACF;AACF;AACD;AACE;AACA;;;AAGC;AACF;AACD;AACE;AACA;AACE;AACD;AACF;AACF;AACF;AAEM;AACA;AACL;AACA;AACA;;AAEK;AAED;;;AAOJ;;;AAGE;;;;;;;AAOF;AACF;AAEO;AACA;AAGA;AAEP;AACE;;;;AAMA;;;AAGI;AACA;;AAEF;AACA;AACF;AACF;AAIM;;AAKJ;AACA;;;;;AAQI;;;;;;;AAOF;AACA;AACF;AACA;AACF;AAEM;AAGJ;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AAKA;AACA;AACA;AACE;;;;AAMF;AACA;AACA;AACE;;;;AAMF;AACE;;;;AAMF;AACE;;;AAIJ;AACF;AAEO;;ACpRP;AAIA;AACE;AAEE;;;;AASA;AACE;;;;;;AAMM;;;;AAQR;;;;AAKA;AACA;;;;;AAKH;;ACpBD;AAWA;AAGA;AACE;AACF;AASA;AACE;AACE;;AAGF;;;AAIA;;;AAKA;;;AAKA;AACE;;AAEF;AACE;;AAGF;AACA;;;;AAIE;AACE;;;AAGF;;;;AAOE;;AAEF;;AAGF;AACE;;AAEF;AACE;;AAGF;AACA;AAGA;AACE;;AAEE;;AAGF;AACE;AACE;;AAED;AAED;AACA;AAEA;;;AAIA;AAEA;AAEA;AACA;;;AAEA;AACA;AACA;AACA;AACA;;;AAGJ;;AAGE;;;AAKA;AACA;AACE;AACA;;;AAMA;AACA;;AAEA;AACA;AACA;AACA;AACE;AAGA;;AAIA;;AAOA;;AAEA;AAIA;;AAGA;AACE;AAGA;;AAEF;AACE;AACA;AACA;AACE;;;;;;AAMV;AACE;;;AAOA;;AAGI;AAIA;;AAGA;AACE;AAGA;;AAEJ;AAGF;;AAEF;AACE;;AAEE;;AAEF;AACA;AACA;AACA;;AAGF;AACE;AAEA;AACA;;AAGF;AACE;AAEA;AACA;AAKA;AAGA;;;AAUA;AACE;;;AAEA;;;AAIJ;AAIE;;;;AAQA;;AAGA;AACE;AACA;;;AAEA;AACA;;AAEF;;AAGE;AACA;AACA;AACE;;;;;AAMJ;AACA;;AAGF;AACE;;AAEF;AACE;;AAEE;;AAEF;AACE;;;AAEA;AACA;AACA;;AAGF;AACE;AACA;AACA;AACA;;;AAEA;AACA;AACA;;;;;AAMF;;AAEH;;ACtUD;AACA;AACA;AAEO;AACP;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO;AACP;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEO;AACP;AACA;AACA;;ACzYA;AAEO;AAEP;AACA;AACA;AACA;AAGA;AACA;AACA;AAOA;AAIA;AAEA;AACA;AAIA;AACA;AACA;AAMA;AACA;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAEA;AAGA;AACA;AACA;AAEA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;;ACnbA;AAEO;AAGA;AACL;;;;;;AAQK;AAyBP;AACE;AAEA;;;;AAKA;AACA;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;;AAIA;;AAGE;AACE;;;AAGF;AACE;;;;AAKJ;;AAIA;AAEA;;AAEA;;AAIA;AAEA;;;AAIF;;;AAGA;AACE;AACA;AACE;;;AAIF;;AAEA;;AAKF;AACA;;;;;;AAMA;AACE;AACA;;AAEF;;AAGE;AACA;AAEA;;AAGF;AACE;;;AAGE;;;;AAIA;;;;AAIA;;;;AAMF;;AAGA;;AAGF;;;AAIE;AACA;AAEA;AAEA;AACA;;AAGF;AACE;;;AAGE;;;;AAMF;;AAGA;;;AAIA;;AAIA;AACA;AAEA;;AAGF;;AAGE;AACA;AAEA;;AAIF;AACA;;;;AAIE;AACA;;AAIF;;AAGE;;AAGA;AAC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ACA;AACE;AACA;;;AAEA;AACA;;;AAIJ;AACE;;AAEF;AACE;;AAEE;;;AAGF;;AAIF;AACE;;;;;AAcE;;;AAGA;;AAEA;AACE;;AAEF;;;AAGA;;AAKM;AACF;;AAGJ;AACE;;AAEF;AACE;;;;AAMN;;AAEE;;AAEE;;;;AAGE;;AAED;;AAEH;AACE;;;;AAIF;AACA;;;AAIA;AACA;;;AAGF;;AAIE;;;;AAKA;AACA;AACA;;;;AAKA;;AAEH;;;AClOD;AAEO;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAkDK;;;;;;;;;;;;;;AAmBP;AACE;AACE;;AAGF;AACE;;AAGF;AACE;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACE;;;;;AAKF;AACA;AACE;;;;AAIE;;AAEE;AACD;AACD;;;AAEA;;;AAIA;;AAEE;AACD;AACD;;;AAEA;;;AAIA;;AAEE;AACD;AACD;;;AAEA;;AAEJ;AACA;AACE;;;;AAIE;;;AAGA;;AAEJ;AACA;AACE;;;;;;AAMA;AACE;AACE;;;AAGN;AACA;AACE;;;AAGA;AACE;;AAEJ;;;;AAII;AACE;AACE;AACA;;AAED;;;AAKP;;;;AAII;AACE;AACE;AACE;AACA;AACD;;;;AAMT;AACA;;AAEE;;;;AAIA;;;AAIA;;;;AAIA;;;AAIJ;;;;AAQA;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAKF;AACA;;;;AAIE;AACE;;;AAIF;AACE;;;AAGA;AACE;AACF;AACE;AACF;AACE;;AAGJ;;;;AAKF;;;;;AAME;AACE;;;AAGF;AACE;;;AAIF;;AAEE;AACE;AACE;AACE;;;AAGJ;;;AAGI;;AAEK;AAC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F;AACE;;AAEF;AACE;;AAEF;AACE;;;;;AAKF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;;;AAKA;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;AACE;;AAGF;AACE;;AAGF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;A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","x_google_ignoreList":[20]}
|
|
1
|
+
{"version":3,"file":"brilliantsole.node.module.js","sources":["../brilliantsole/utils/environment.ts","../brilliantsole/utils/Console.ts","../brilliantsole/utils/EventDispatcher.ts","../brilliantsole/utils/Timer.ts","../brilliantsole/utils/checksum.ts","../brilliantsole/utils/Text.ts","../brilliantsole/utils/ArrayBufferUtils.ts","../brilliantsole/FileTransferManager.ts","../brilliantsole/utils/MathUtils.ts","../brilliantsole/utils/RangeHelper.ts","../brilliantsole/utils/CenterOfPressureHelper.ts","../brilliantsole/utils/ArrayUtils.ts","../brilliantsole/sensor/PressureSensorDataManager.ts","../brilliantsole/sensor/MotionSensorDataManager.ts","../brilliantsole/sensor/BarometerSensorDataManager.ts","../brilliantsole/utils/ParseUtils.ts","../brilliantsole/CameraManager.ts","../brilliantsole/utils/AudioUtils.ts","../brilliantsole/MicrophoneManager.ts","../brilliantsole/sensor/SensorDataManager.ts","../node_modules/auto-bind/index.js","../brilliantsole/sensor/SensorConfigurationManager.ts","../brilliantsole/TfliteManager.ts","../brilliantsole/DeviceInformationManager.ts","../brilliantsole/InformationManager.ts","../brilliantsole/vibration/VibrationWaveformEffects.ts","../brilliantsole/vibration/VibrationManager.ts","../brilliantsole/WifiManager.ts","../brilliantsole/utils/ColorUtils.ts","../brilliantsole/utils/DisplayContextState.ts","../brilliantsole/utils/ObjectUtils.ts","../brilliantsole/utils/DisplayContextStateHelper.ts","../brilliantsole/utils/DisplayUtils.ts","../brilliantsole/utils/DisplayContextCommand.ts","../brilliantsole/utils/PathUtils.ts","../brilliantsole/utils/SvgUtils.ts","../brilliantsole/utils/stringUtils.ts","../brilliantsole/utils/DisplaySpriteSheetUtils.ts","../brilliantsole/utils/DisplayBitmapUtils.ts","../brilliantsole/utils/DisplayManagerInterface.ts","../brilliantsole/DisplayManager.ts","../brilliantsole/connection/BaseConnectionManager.ts","../brilliantsole/utils/EventUtils.ts","../brilliantsole/connection/bluetooth/bluetoothUUIDs.ts","../brilliantsole/connection/bluetooth/BluetoothConnectionManager.ts","../brilliantsole/connection/bluetooth/WebBluetoothConnectionManager.ts","../brilliantsole/utils/cbor.js","../brilliantsole/utils/mcumgr.js","../brilliantsole/FirmwareManager.ts","../brilliantsole/DeviceManager.ts","../brilliantsole/server/ServerUtils.ts","../brilliantsole/server/websocket/WebSocketUtils.ts","../brilliantsole/connection/websocket/WebSocketConnectionManager.ts","../brilliantsole/connection/udp/UDPConnectionManager.ts","../brilliantsole/Device.ts","../brilliantsole/devicePair/DevicePairPressureSensorDataManager.ts","../brilliantsole/devicePair/DevicePairSensorDataManager.ts","../brilliantsole/devicePair/DevicePair.ts","../brilliantsole/utils/ThrottleUtils.ts","../brilliantsole/scanner/BaseScanner.ts","../brilliantsole/connection/bluetooth/NobleConnectionManager.ts","../brilliantsole/scanner/NobleScanner.ts","../brilliantsole/scanner/Scanner.ts","../brilliantsole/server/BaseServer.ts","../brilliantsole/server/websocket/WebSocketServer.ts","../brilliantsole/server/udp/UDPUtils.ts","../brilliantsole/server/udp/UDPServer.ts","../brilliantsole/BS.ts"],"sourcesContent":["type ENVIRONMENT_FLAG = \"__BRILLIANTSOLE__DEV__\" | \"__BRILLIANTSOLE__PROD__\";\nconst __BRILLIANTSOLE__ENVIRONMENT__: ENVIRONMENT_FLAG =\n \"__BRILLIANTSOLE__DEV__\";\n\n//@ts-expect-error\nconst isInProduction =\n __BRILLIANTSOLE__ENVIRONMENT__ == \"__BRILLIANTSOLE__PROD__\";\nconst isInDev = __BRILLIANTSOLE__ENVIRONMENT__ == \"__BRILLIANTSOLE__DEV__\";\n\n// https://github.com/flexdinesh/browser-or-node/blob/master/src/index.ts\nconst isInBrowser =\n typeof window !== \"undefined\" && typeof window?.document !== \"undefined\";\nconst isInNode =\n typeof process !== \"undefined\" && process?.versions?.node != null;\n\nconst userAgent = (isInBrowser && navigator.userAgent) || \"\";\n\nlet isBluetoothSupported = false;\nif (isInBrowser) {\n isBluetoothSupported = Boolean(navigator.bluetooth);\n} else if (isInNode) {\n isBluetoothSupported = true;\n}\n\nconst isInBluefy = isInBrowser && /Bluefy/i.test(userAgent);\nconst isInWebBLE = isInBrowser && /WebBLE/i.test(userAgent);\n\nconst isAndroid = isInBrowser && /Android/i.test(userAgent);\nconst isSafari =\n isInBrowser && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);\n\nconst isIOS = isInBrowser && /iPad|iPhone|iPod/i.test(userAgent);\nconst isMac = isInBrowser && /Macintosh/i.test(userAgent);\n\n// @ts-expect-error\nconst isInLensStudio =\n !isInBrowser &&\n !isInNode &&\n typeof global !== \"undefined\" &&\n typeof Studio !== \"undefined\";\n\nexport {\n isInDev,\n isInProduction,\n isInBrowser,\n isInNode,\n isAndroid,\n isInBluefy,\n isInWebBLE,\n isSafari,\n isInLensStudio,\n isIOS,\n isMac,\n isBluetoothSupported,\n};\n","import { isInDev, isInLensStudio, isInNode } from \"./environment.ts\";\n\ndeclare var Studio: any | undefined;\n\nexport type LogFunction = (...data: any[]) => void;\nexport type AssertLogFunction = (condition: boolean, ...data: any[]) => void;\n\nexport interface ConsoleLevelFlags {\n log?: boolean;\n warn?: boolean;\n error?: boolean;\n assert?: boolean;\n table?: boolean;\n}\n\ninterface ConsoleLike {\n log?: LogFunction;\n warn?: LogFunction;\n error?: LogFunction;\n assert?: AssertLogFunction;\n table?: LogFunction;\n}\n\nvar __console: ConsoleLike;\nif (isInLensStudio) {\n const log = function (...args: any[]) {\n Studio.log(args.map((value) => new String(value)).join(\",\"));\n };\n __console = {};\n __console.log = log;\n __console.warn = log.bind(__console, \"WARNING\");\n __console.error = log.bind(__console, \"ERROR\");\n} else {\n __console = console;\n}\n\nfunction getCallerFunctionPath(): string {\n const stack = new Error().stack;\n if (!stack) return \"\";\n\n const lines = stack.split(\"\\n\");\n const callerLine = lines[3] || lines[2];\n\n const match = callerLine.match(/at (.*?) \\(/) || callerLine.match(/at (.*)/);\n if (!match) return \"\";\n\n const fullFn = match[1].trim();\n return `[${fullFn}]`;\n}\n\nfunction wrapWithLocation(fn: LogFunction): LogFunction {\n return (...args: any[]) => {\n if (isInNode) {\n const functionPath = getCallerFunctionPath();\n fn(functionPath, ...args);\n } else {\n fn(...args);\n }\n };\n}\n\n// console.assert not supported in WebBLE\nif (!__console.assert) {\n const assert: AssertLogFunction = (condition, ...data) => {\n if (!condition) {\n __console.warn!(...data);\n }\n };\n __console.assert = assert;\n}\n\n// console.table not supported in WebBLE\nif (!__console.table) {\n const table: LogFunction = (...data) => {\n __console.log!(...data);\n };\n __console.table = table;\n}\n\nfunction emptyFunction() {}\n\nconst log: LogFunction = isInNode\n ? wrapWithLocation(__console.log!.bind(__console))\n : __console.log!.bind(__console);\nconst warn: LogFunction = isInNode\n ? wrapWithLocation(__console.warn!.bind(__console))\n : __console.warn!.bind(__console);\nconst error: LogFunction = isInNode\n ? wrapWithLocation(__console.error!.bind(__console))\n : __console.error!.bind(__console);\nconst table: LogFunction = isInNode\n ? wrapWithLocation(__console.table!.bind(__console))\n : __console.table!.bind(__console);\nconst assert: AssertLogFunction = __console.assert.bind(__console);\n\nclass Console {\n static #consoles: { [type: string]: Console } = {};\n\n constructor(type: string) {\n if (Console.#consoles[type]) {\n throw new Error(`\"${type}\" console already exists`);\n }\n Console.#consoles[type] = this;\n }\n\n #levelFlags: ConsoleLevelFlags = {\n log: isInDev,\n warn: isInDev,\n assert: true,\n error: true,\n table: true,\n };\n\n setLevelFlags(levelFlags: ConsoleLevelFlags) {\n Object.assign(this.#levelFlags, levelFlags);\n }\n\n /** @throws {Error} if no console with type \"type\" is found */\n static setLevelFlagsForType(type: string, levelFlags: ConsoleLevelFlags) {\n if (!this.#consoles[type]) {\n throw new Error(`no console found with type \"${type}\"`);\n }\n this.#consoles[type].setLevelFlags(levelFlags);\n }\n\n static setAllLevelFlags(levelFlags: ConsoleLevelFlags) {\n for (const type in this.#consoles) {\n this.#consoles[type].setLevelFlags(levelFlags);\n }\n }\n\n static create(type: string, levelFlags?: ConsoleLevelFlags): Console {\n const console = this.#consoles[type] || new Console(type);\n if (isInDev && levelFlags) {\n console.setLevelFlags(levelFlags);\n }\n return console;\n }\n\n get log() {\n return this.#levelFlags.log ? log : emptyFunction;\n }\n\n get warn() {\n return this.#levelFlags.warn ? warn : emptyFunction;\n }\n\n get error() {\n return this.#levelFlags.error ? error : emptyFunction;\n }\n\n get assert() {\n return this.#levelFlags.assert ? assert : emptyFunction;\n }\n\n get table() {\n return this.#levelFlags.table ? table : emptyFunction;\n }\n\n /** @throws {Error} if condition is not met */\n assertWithError(condition: any, message: string) {\n if (!Boolean(condition)) {\n throw new Error(message);\n }\n }\n\n /** @throws {Error} if value's type doesn't match */\n assertTypeWithError(value: any, type: string) {\n this.assertWithError(\n typeof value == type,\n `value ${value} of type \"${typeof value}\" not of type \"${type}\"`\n );\n }\n\n /** @throws {Error} if value's type doesn't match */\n assertEnumWithError(value: string, enumeration: readonly string[]) {\n this.assertWithError(\n enumeration.includes(value),\n `invalid enum \"${value}\"`\n );\n }\n\n /** @throws {Error} if value is not within some range */\n assertRangeWithError(name: string, value: number, min: number, max: number) {\n this.assertWithError(\n value >= min && value <= max,\n `${name} ${value} must be within ${min}-${max}`\n );\n }\n}\n\nexport function createConsole(\n type: string,\n levelFlags?: ConsoleLevelFlags\n): Console {\n return Console.create(type, levelFlags);\n}\n\n/** @throws {Error} if no console with type is found */\nexport function setConsoleLevelFlagsForType(\n type: string,\n levelFlags: ConsoleLevelFlags\n) {\n Console.setLevelFlagsForType(type, levelFlags);\n}\n\nexport function setAllConsoleLevelFlags(levelFlags: ConsoleLevelFlags) {\n Console.setAllLevelFlags(levelFlags);\n}\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"EventDispatcher\", { log: false });\n\nexport type EventMap<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [T in keyof EventMessages]: {\n type: T;\n target: Target;\n message: EventMessages[T];\n };\n};\nexport type EventListenerMap<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [T in keyof EventMessages]: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void;\n};\n\nexport type Event<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = EventMap<Target, EventType, EventMessages>[keyof EventMessages];\n\ntype SpecificEvent<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>,\n SpecificEventType extends EventType\n> = {\n type: SpecificEventType;\n target: Target;\n message: EventMessages[SpecificEventType];\n};\n\nexport type BoundEventListeners<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> = {\n [SpecificEventType in keyof EventMessages]?: (\n // @ts-expect-error\n event: SpecificEvent<Target, EventType, EventMessages, SpecificEventType>\n ) => void;\n};\n\nclass EventDispatcher<\n Target extends any,\n EventType extends string,\n EventMessages extends Partial<Record<EventType, any>>\n> {\n private listeners: {\n [T in EventType]?: {\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void;\n once?: boolean;\n shouldRemove?: boolean;\n }[];\n } = {};\n\n constructor(\n private target: Target,\n private validEventTypes: readonly EventType[]\n ) {\n this.addEventListener = this.addEventListener.bind(this);\n this.removeEventListener = this.removeEventListener.bind(this);\n this.removeEventListeners = this.removeEventListeners.bind(this);\n this.removeAllEventListeners = this.removeAllEventListeners.bind(this);\n this.dispatchEvent = this.dispatchEvent.bind(this);\n this.waitForEvent = this.waitForEvent.bind(this);\n }\n\n private isValidEventType(type: any): type is EventType {\n return this.validEventTypes.includes(type);\n }\n\n private updateEventListeners(type: EventType) {\n if (!this.listeners[type]) return;\n this.listeners[type] = this.listeners[type]!.filter((listenerObj) => {\n if (listenerObj.shouldRemove) {\n _console.log(`removing \"${type}\" eventListener`, listenerObj);\n }\n return !listenerObj.shouldRemove;\n });\n }\n\n addEventListener<T extends EventType>(\n type: T,\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void,\n options: { once?: boolean } = { once: false }\n ): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) {\n this.listeners[type] = [];\n _console.log(`creating \"${type}\" listeners array`, this.listeners[type]!);\n }\n const alreadyAdded = this.listeners[type].find((listenerObject) => {\n return (\n listenerObject.listener == listener &&\n listenerObject.once == options.once\n );\n });\n if (alreadyAdded) {\n _console.log(\"already added listener\");\n return;\n }\n _console.log(`adding \"${type}\" listener`, listener, options);\n this.listeners[type]!.push({ listener, once: options.once });\n\n _console.log(\n `currently have ${this.listeners[type]!.length} \"${type}\" listeners`\n );\n }\n\n removeEventListener<T extends EventType>(\n type: T,\n listener: (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => void\n ): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n _console.log(`removing \"${type}\" listener...`, listener);\n this.listeners[type]!.forEach((listenerObj) => {\n const isListenerToRemove = listenerObj.listener === listener;\n if (isListenerToRemove) {\n _console.log(`flagging \"${type}\" listener`, listener);\n listenerObj.shouldRemove = true;\n }\n });\n\n this.updateEventListeners(type);\n }\n\n removeEventListeners<T extends EventType>(type: T): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n _console.log(`removing \"${type}\" listeners...`);\n this.listeners[type] = [];\n }\n\n removeAllEventListeners(): void {\n _console.log(`removing listeners...`);\n this.listeners = {};\n }\n\n dispatchEvent<T extends EventType>(type: T, message: EventMessages[T]): void {\n if (!this.isValidEventType(type)) {\n throw new Error(`Invalid event type: ${type}`);\n }\n\n if (!this.listeners[type]) return;\n\n // Take a snapshot of listeners at this moment\n const listenersSnapshot = [...this.listeners[type]!];\n\n listenersSnapshot.forEach((listenerObj) => {\n if (listenerObj.shouldRemove) {\n return;\n }\n\n _console.log(`dispatching \"${type}\" listener`, listenerObj);\n try {\n listenerObj.listener({ type, target: this.target, message });\n } catch (error) {\n console.error(error);\n }\n\n if (listenerObj.once) {\n _console.log(`flagging \"${type}\" listener`, listenerObj);\n listenerObj.shouldRemove = true;\n }\n });\n\n this.updateEventListeners(type);\n }\n\n waitForEvent<T extends EventType>(\n type: T\n ): Promise<{ type: T; target: Target; message: EventMessages[T] }> {\n return new Promise((resolve) => {\n const onceListener = (event: {\n type: T;\n target: Target;\n message: EventMessages[T];\n }) => {\n resolve(event);\n };\n\n this.addEventListener(type, onceListener, { once: true });\n });\n }\n}\n\nexport default EventDispatcher;\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"Timer\", { log: false });\n\nexport async function wait(delay: number) {\n _console.log(`waiting for ${delay}ms`);\n return new Promise((resolve: Function) => {\n setTimeout(() => resolve(), delay);\n });\n}\n\nexport class Timer {\n #callback!: Function;\n get callback() {\n return this.#callback;\n }\n set callback(newCallback) {\n _console.assertTypeWithError(newCallback, \"function\");\n _console.log({ newCallback });\n this.#callback = newCallback;\n if (this.isRunning) {\n this.restart();\n }\n }\n\n #interval!: number;\n get interval() {\n return this.#interval;\n }\n set interval(newInterval) {\n _console.assertTypeWithError(newInterval, \"number\");\n _console.assertWithError(newInterval > 0, \"interval must be above 0\");\n _console.log({ newInterval });\n this.#interval = newInterval;\n if (this.isRunning) {\n this.restart();\n }\n }\n\n constructor(callback: Function, interval: number) {\n this.interval = interval;\n this.callback = callback;\n }\n\n #intervalId: number | undefined;\n get isRunning() {\n return this.#intervalId != undefined;\n }\n\n start(immediately = false) {\n if (this.isRunning) {\n _console.log(\"interval already running\");\n return;\n }\n _console.log(`starting interval every ${this.#interval}ms`);\n this.#intervalId = setInterval(this.#callback, this.#interval);\n if (immediately) {\n this.#callback();\n }\n }\n stop() {\n if (!this.isRunning) {\n _console.log(\"interval already not running\");\n return;\n }\n _console.log(\"stopping interval\");\n clearInterval(this.#intervalId);\n this.#intervalId = undefined;\n }\n restart(startImmediately = false) {\n this.stop();\n this.start(startImmediately);\n }\n}\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"checksum\", { log: false });\n\n// https://github.com/googlecreativelab/tiny-motion-trainer/blob/5fceb49f018ae0c403bf9f0ccc437309c2acb507/frontend/src/tf4micro-motion-kit/modules/bleFileTransfer#L195\n\n// See http://home.thep.lu.se/~bjorn/crc/ for more information on simple CRC32 calculations.\nexport function crc32ForByte(r: number) {\n for (let j = 0; j < 8; ++j) {\n r = (r & 1 ? 0 : 0xedb88320) ^ (r >>> 1);\n }\n return r ^ 0xff000000;\n}\n\nconst tableSize = 256;\nconst crc32Table = new Uint32Array(tableSize);\nfor (let i = 0; i < tableSize; ++i) {\n crc32Table[i] = crc32ForByte(i);\n}\n\nexport function crc32(dataIterable: ArrayBuffer | number[]) {\n let dataBytes = new Uint8Array(dataIterable);\n let crc = 0;\n for (let i = 0; i < dataBytes.byteLength; ++i) {\n const crcLowByte = crc & 0x000000ff;\n const dataByte = dataBytes[i];\n const tableIndex = crcLowByte ^ dataByte;\n // The last >>> is to convert this into an unsigned 32-bit integer.\n crc = (crc32Table[tableIndex] ^ (crc >>> 8)) >>> 0;\n }\n return crc;\n}\n\n// This is a small test function for the CRC32 implementation, not normally called but left in\n// for debugging purposes. We know the expected CRC32 of [97, 98, 99, 100, 101] is 2240272485,\n// or 0x8587d865, so if anything else is output we know there's an error in the implementation.\nexport function testCrc32() {\n const testArray = [97, 98, 99, 100, 101];\n const testArrayCrc32 = crc32(testArray);\n _console.log(\"CRC32 for [97, 98, 99, 100, 101] is 0x\" + testArrayCrc32.toString(16) + \" (\" + testArrayCrc32 + \")\");\n}\n","var _TextEncoder;\nif (typeof TextEncoder == \"undefined\") {\n _TextEncoder = class {\n encode(string: string) {\n const encoding = Array.from(string).map((char) => char.charCodeAt(0));\n return Uint8Array.from(encoding);\n }\n };\n} else {\n _TextEncoder = TextEncoder;\n}\n\nvar _TextDecoder;\nif (typeof TextDecoder == \"undefined\") {\n _TextDecoder = class {\n decode(data: ArrayBuffer) {\n const byteArray = Array.from(new Uint8Array(data));\n return byteArray\n .map((value) => {\n return String.fromCharCode(value);\n })\n .join(\"\");\n }\n };\n} else {\n _TextDecoder = TextDecoder;\n}\n\nexport const textEncoder = new _TextEncoder();\nexport const textDecoder = new _TextDecoder();\n","import { createConsole } from \"./Console.ts\";\nimport { textEncoder } from \"./Text.ts\";\n\nconst _console = createConsole(\"ArrayBufferUtils\", { log: false });\n\nexport function concatenateArrayBuffers(...arrayBuffers: any[]): ArrayBuffer {\n arrayBuffers = arrayBuffers.filter(\n (arrayBuffer) => arrayBuffer != undefined || arrayBuffer != null\n );\n arrayBuffers = arrayBuffers.map((arrayBuffer) => {\n if (typeof arrayBuffer == \"number\") {\n const number = arrayBuffer;\n return Uint8Array.from([Math.floor(number)]);\n } else if (typeof arrayBuffer == \"boolean\") {\n const boolean = arrayBuffer;\n return Uint8Array.from([boolean ? 1 : 0]);\n } else if (typeof arrayBuffer == \"string\") {\n const string = arrayBuffer;\n return stringToArrayBuffer(string);\n } else if (arrayBuffer instanceof Array) {\n const array = arrayBuffer;\n return concatenateArrayBuffers(...array);\n } else if (arrayBuffer instanceof ArrayBuffer) {\n return arrayBuffer;\n } else if (\n \"buffer\" in arrayBuffer &&\n arrayBuffer.buffer instanceof ArrayBuffer\n ) {\n const bufferContainer = arrayBuffer;\n return bufferContainer.buffer;\n } else if (arrayBuffer instanceof DataView) {\n const dataView = arrayBuffer;\n return dataView.buffer;\n } else if (typeof arrayBuffer == \"object\") {\n const object = arrayBuffer;\n return objectToArrayBuffer(object);\n } else {\n return arrayBuffer;\n }\n });\n arrayBuffers = arrayBuffers.filter(\n (arrayBuffer) => arrayBuffer && \"byteLength\" in arrayBuffer\n );\n const length = arrayBuffers.reduce(\n (length, arrayBuffer) => length + arrayBuffer.byteLength,\n 0\n );\n const uint8Array = new Uint8Array(length);\n let byteOffset = 0;\n arrayBuffers.forEach((arrayBuffer) => {\n uint8Array.set(new Uint8Array(arrayBuffer), byteOffset);\n byteOffset += arrayBuffer.byteLength;\n });\n return uint8Array.buffer;\n}\n\nexport function dataToArrayBuffer(data: Buffer) {\n return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);\n}\n\nexport function stringToArrayBuffer(string: string) {\n const encoding = textEncoder.encode(string);\n return concatenateArrayBuffers(encoding.byteLength, encoding);\n}\n\nexport function objectToArrayBuffer(object: object) {\n return stringToArrayBuffer(JSON.stringify(object));\n}\n\nexport function sliceDataView(\n dataView: DataView,\n begin: number,\n length?: number\n) {\n let end;\n if (length != undefined) {\n end = dataView.byteOffset + begin + length;\n }\n _console.log({ dataView, begin, end, length });\n return new DataView(dataView.buffer.slice(dataView.byteOffset + begin, end));\n}\n\nexport type FileLike = number[] | ArrayBuffer | DataView | URL | string | File;\n\nexport async function getFileBuffer(file: FileLike) {\n let fileBuffer;\n if (file instanceof Array) {\n fileBuffer = Uint8Array.from(file);\n } else if (file instanceof DataView) {\n fileBuffer = file.buffer;\n } else if (typeof file == \"string\" || file instanceof URL) {\n const response = await fetch(file);\n fileBuffer = await response.arrayBuffer();\n } else if (file instanceof File) {\n fileBuffer = await file.arrayBuffer();\n } else if (file instanceof ArrayBuffer) {\n fileBuffer = file;\n } else {\n throw { error: \"invalid file type\", file };\n }\n return fileBuffer;\n}\n\nexport function UInt8ByteBuffer(value: number) {\n return Uint8Array.from([value]).buffer;\n}\n","import { createConsole } from \"./utils/Console.ts\";\nimport { crc32 } from \"./utils/checksum.ts\";\nimport { getFileBuffer, UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"FileTransferManager\", { log: false });\n\nexport const FileTransferMessageTypes = [\n \"getFileTypes\",\n \"maxFileLength\",\n \"getFileType\",\n \"setFileType\",\n \"getFileLength\",\n \"setFileLength\",\n \"getFileChecksum\",\n \"setFileChecksum\",\n \"setFileTransferCommand\",\n \"fileTransferStatus\",\n \"getFileBlock\",\n \"setFileBlock\",\n \"fileBytesTransferred\",\n] as const;\nexport type FileTransferMessageType = (typeof FileTransferMessageTypes)[number];\n\nexport const FileTypes = [\n \"tflite\",\n \"wifiServerCert\",\n \"wifiServerKey\",\n \"spriteSheet\",\n] as const;\nexport type FileType = (typeof FileTypes)[number];\n\nexport const FileTransferStatuses = [\"idle\", \"sending\", \"receiving\"] as const;\nexport type FileTransferStatus = (typeof FileTransferStatuses)[number];\n\nexport const FileTransferCommands = [\n \"startSend\",\n \"startReceive\",\n \"cancel\",\n] as const;\nexport type FileTransferCommand = (typeof FileTransferCommands)[number];\n\nexport const FileTransferDirections = [\"sending\", \"receiving\"] as const;\nexport type FileTransferDirection = (typeof FileTransferDirections)[number];\n\nexport const FileTransferEventTypes = [\n ...FileTransferMessageTypes,\n \"fileTransferProgress\",\n \"fileTransferComplete\",\n \"fileReceived\",\n] as const;\nexport type FileTransferEventType = (typeof FileTransferEventTypes)[number];\n\nexport const RequiredFileTransferMessageTypes: FileTransferMessageType[] = [\n \"maxFileLength\",\n \"getFileLength\",\n \"getFileChecksum\",\n \"getFileType\",\n \"fileTransferStatus\",\n];\n\nexport interface FileConfiguration {\n file: FileLike;\n type: FileType;\n}\n\nexport interface FileTransferEventMessages {\n getFileTypes: { fileTypes: FileType[] };\n maxFileLength: { maxFileLength: number };\n getFileType: { fileType: FileType };\n getFileLength: { fileLength: number };\n getFileChecksum: { fileChecksum: number };\n fileTransferStatus: {\n fileType: FileType;\n fileTransferStatus: FileTransferStatus;\n };\n getFileBlock: { fileTransferBlock: DataView };\n fileTransferProgress: { fileType: FileType; progress: number };\n fileTransferComplete: {\n fileType: FileType;\n direction: FileTransferDirection;\n };\n fileReceived: { fileType: FileType; file: File | Blob };\n}\n\nexport type FileTransferEventDispatcher = EventDispatcher<\n Device,\n FileTransferEventType,\n FileTransferEventMessages\n>;\nexport type SendFileTransferMessageCallback =\n SendMessageCallback<FileTransferMessageType>;\n\nexport type SendFileCallback = (\n type: FileType,\n file: FileLike,\n override?: boolean\n) => Promise<boolean>;\n\nclass FileTransferManager {\n constructor() {\n autoBind(this);\n }\n sendMessage!: SendFileTransferMessageCallback;\n\n eventDispatcher!: FileTransferEventDispatcher;\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #assertValidType(type: FileType) {\n _console.assertEnumWithError(type, FileTypes);\n }\n #isValidType(type: FileType) {\n return FileTypes.includes(type);\n }\n #assertValidTypeEnum(typeEnum: number) {\n _console.assertWithError(\n typeEnum in FileTypes,\n `invalid typeEnum ${typeEnum}`\n );\n }\n\n #assertValidStatusEnum(statusEnum: number) {\n _console.assertWithError(\n statusEnum in FileTransferStatuses,\n `invalid statusEnum ${statusEnum}`\n );\n }\n #assertValidCommand(command: FileTransferCommand) {\n _console.assertEnumWithError(command, FileTransferCommands);\n }\n\n #fileTypes: FileType[] = [];\n get fileTypes() {\n return this.#fileTypes;\n }\n #parseFileTypes(dataView: DataView) {\n const fileTypes = Array.from(new Uint8Array(dataView.buffer))\n .map((index) => FileTypes[index])\n .filter(Boolean);\n this.#fileTypes = fileTypes;\n _console.log(\"fileTypes\", fileTypes);\n this.#dispatchEvent(\"getFileTypes\", {\n fileTypes: this.#fileTypes,\n });\n }\n\n static #MaxLength = 0; // kB\n static get MaxLength() {\n return this.#MaxLength;\n }\n #maxLength = FileTransferManager.MaxLength;\n /** kB */\n get maxLength() {\n return this.#maxLength;\n }\n #parseMaxLength(dataView: DataView) {\n _console.log(\"parseFileMaxLength\", dataView);\n const maxLength = dataView.getUint32(0, true);\n _console.log(`maxLength: ${maxLength / 1024}kB`);\n this.#updateMaxLength(maxLength);\n }\n #updateMaxLength(maxLength: number) {\n _console.log({ maxLength });\n this.#maxLength = maxLength;\n this.#dispatchEvent(\"maxFileLength\", { maxFileLength: maxLength });\n }\n #assertValidLength(length: number) {\n _console.assertWithError(\n length <= this.maxLength,\n `file length ${length}kB too large - must be ${this.maxLength}kB or less`\n );\n }\n\n #type: FileType | undefined;\n get type() {\n return this.#type;\n }\n #parseType(dataView: DataView) {\n _console.log(\"parseFileType\", dataView);\n const typeEnum = dataView.getUint8(0);\n this.#assertValidTypeEnum(typeEnum);\n const type = FileTypes[typeEnum];\n this.#updateType(type);\n }\n #updateType(type: FileType) {\n _console.log({ fileTransferType: type });\n this.#type = type;\n this.#dispatchEvent(\"getFileType\", { fileType: type });\n }\n async #setType(newType: FileType, sendImmediately?: boolean) {\n this.#assertValidType(newType);\n if (this.type == newType) {\n _console.log(`redundant type assignment ${newType}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileType\");\n\n const typeEnum = FileTypes.indexOf(newType);\n\n this.sendMessage(\n [{ type: \"setFileType\", data: UInt8ByteBuffer(typeEnum) }],\n sendImmediately\n );\n\n await promise;\n }\n\n #length = 0;\n get length() {\n return this.#length;\n }\n #parseLength(dataView: DataView) {\n _console.log(\"parseFileLength\", dataView);\n const length = dataView.getUint32(0, true);\n\n this.#updateLength(length);\n }\n #updateLength(length: number) {\n _console.log(`length: ${length / 1024}kB`);\n this.#length = length;\n this.#dispatchEvent(\"getFileLength\", { fileLength: length });\n }\n async #setLength(newLength: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newLength, \"number\");\n this.#assertValidLength(newLength);\n if (this.length == newLength) {\n _console.log(`redundant length assignment ${newLength}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileLength\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, newLength, true);\n this.sendMessage(\n [{ type: \"setFileLength\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #checksum = 0;\n get checksum() {\n return this.#checksum;\n }\n #parseChecksum(dataView: DataView) {\n _console.log(\"checksum\", dataView);\n const checksum = dataView.getUint32(0, true);\n this.#updateChecksum(checksum);\n }\n #updateChecksum(checksum: number) {\n _console.log({ checksum });\n this.#checksum = checksum;\n this.#dispatchEvent(\"getFileChecksum\", { fileChecksum: checksum });\n }\n async #setChecksum(newChecksum: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newChecksum, \"number\");\n if (this.checksum == newChecksum) {\n _console.log(`redundant checksum assignment ${newChecksum}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getFileChecksum\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, newChecksum, true);\n this.sendMessage(\n [{ type: \"setFileChecksum\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n async #setCommand(command: FileTransferCommand, sendImmediately?: boolean) {\n this.#assertValidCommand(command);\n\n const promise = this.waitForEvent(\"fileTransferStatus\");\n _console.log(`setting command ${command}`);\n const commandEnum = FileTransferCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"setFileTransferCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n\n #status: FileTransferStatus = \"idle\";\n get status() {\n return this.#status;\n }\n #parseStatus(dataView: DataView) {\n _console.log(\"parseFileStatus\", dataView);\n const statusEnum = dataView.getUint8(0);\n this.#assertValidStatusEnum(statusEnum);\n const status = FileTransferStatuses[statusEnum];\n this.#updateStatus(status);\n }\n #updateStatus(status: FileTransferStatus) {\n _console.log({ status });\n this.#status = status;\n this.#receivedBlocks.length = 0;\n this.#isCancelling = false;\n this.#buffer = undefined;\n this.#bytesTransferred = 0;\n this.#dispatchEvent(\"fileTransferStatus\", {\n fileTransferStatus: status,\n fileType: this.type!,\n });\n }\n #assertIsIdle() {\n _console.assertWithError(this.#status == \"idle\", \"status is not idle\");\n }\n #assertIsNotIdle() {\n _console.assertWithError(this.#status != \"idle\", \"status is idle\");\n }\n\n // BLOCK\n\n #receivedBlocks: ArrayBuffer[] = [];\n\n async #parseBlock(dataView: DataView) {\n _console.log(\"parseFileBlock\", dataView);\n this.#receivedBlocks.push(dataView.buffer);\n\n const bytesReceived = this.#receivedBlocks.reduce(\n (sum, arrayBuffer) => (sum += arrayBuffer.byteLength),\n 0\n );\n const progress = bytesReceived / this.#length;\n\n _console.log(\n `received ${bytesReceived} of ${this.#length} bytes (${progress * 100}%)`\n );\n\n this.#dispatchEvent(\"fileTransferProgress\", {\n progress,\n fileType: this.type!,\n });\n\n if (bytesReceived != this.#length) {\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint32(0, bytesReceived, true);\n\n if (this.isServerSide) {\n return;\n }\n await this.sendMessage([\n { type: \"fileBytesTransferred\", data: dataView.buffer },\n ]);\n return;\n }\n\n _console.log(\"file transfer complete\");\n\n let fileName = new Date().toLocaleString();\n switch (this.type) {\n case \"tflite\":\n fileName += \".tflite\";\n break;\n case \"wifiServerCert\":\n fileName += \"_server.crt\";\n break;\n case \"wifiServerKey\":\n fileName += \"_server.key\";\n break;\n }\n\n let file: File | Blob;\n if (typeof File !== \"undefined\") {\n file = new File(this.#receivedBlocks, fileName);\n } else {\n file = new Blob(this.#receivedBlocks);\n }\n\n const arrayBuffer = await file.arrayBuffer();\n const checksum = crc32(arrayBuffer);\n _console.log({ checksum });\n\n if (checksum != this.#checksum) {\n _console.error(\n `wrong checksum - expected ${this.#checksum}, got ${checksum}`\n );\n return;\n }\n\n _console.log(\"received file\", file);\n\n this.#dispatchEvent(\"getFileBlock\", { fileTransferBlock: dataView });\n this.#dispatchEvent(\"fileTransferComplete\", {\n direction: \"receiving\",\n fileType: this.type!,\n });\n this.#dispatchEvent(\"fileReceived\", { file, fileType: this.type! });\n }\n\n parseMessage(messageType: FileTransferMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getFileTypes\":\n this.#parseFileTypes(dataView);\n break;\n case \"maxFileLength\":\n this.#parseMaxLength(dataView);\n break;\n case \"getFileType\":\n case \"setFileType\":\n this.#parseType(dataView);\n break;\n case \"getFileLength\":\n case \"setFileLength\":\n this.#parseLength(dataView);\n break;\n case \"getFileChecksum\":\n case \"setFileChecksum\":\n this.#parseChecksum(dataView);\n break;\n case \"fileTransferStatus\":\n this.#parseStatus(dataView);\n break;\n case \"getFileBlock\":\n this.#parseBlock(dataView);\n break;\n case \"fileBytesTransferred\":\n this.#parseBytesTransferred(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n async send(type: FileType, file: FileLike, override?: boolean) {\n if (true) {\n this.#assertIsIdle();\n this.#assertValidType(type);\n } else {\n if (this.status != \"idle\") {\n _console.warn(`cannot send file - status is ${this.status}`);\n return false;\n }\n if (!this.#isValidType(type)) {\n _console.warn(`invalid fileType ${type}`);\n return false;\n }\n }\n\n const fileBuffer = await getFileBuffer(file);\n const fileLength = fileBuffer.byteLength;\n const checksum = crc32(fileBuffer);\n this.#assertValidLength(fileLength);\n\n if (!override) {\n if (type != this.type) {\n _console.log(\"different fileTypes - sending\");\n } else if (fileLength != this.length) {\n _console.log(\"different fileLengths - sending\");\n } else if (checksum != this.checksum) {\n _console.log(\"different fileChecksums - sending\");\n } else {\n _console.log(\"already sent file\");\n return false;\n }\n }\n\n const promises: Promise<any>[] = [];\n\n promises.push(this.#setType(type, false));\n promises.push(this.#setLength(fileLength, false));\n promises.push(this.#setChecksum(checksum, false));\n promises.push(this.#setCommand(\"startSend\", false));\n\n this.sendMessage();\n\n await Promise.all(promises);\n\n if (this.#buffer) {\n return false;\n }\n if (this.#length != fileLength) {\n return false;\n }\n if (this.#checksum != checksum) {\n return false;\n }\n\n await this.#send(fileBuffer);\n\n return true;\n }\n\n #buffer?: ArrayBuffer;\n #bytesTransferred = 0;\n async #send(buffer: ArrayBuffer) {\n this.#buffer = buffer;\n return this.#sendBlock();\n }\n\n mtu!: number;\n async #sendBlock(): Promise<void> {\n if (this.status != \"sending\") {\n return;\n }\n if (this.#isCancelling) {\n _console.error(\"not sending block - busy cancelling\");\n return;\n }\n if (!this.#buffer) {\n if (!this.isServerSide) {\n _console.error(\"no buffer defined\");\n }\n return;\n }\n\n const buffer = this.#buffer;\n let offset = this.#bytesTransferred;\n\n _console.log(\"sending block\", { buffer, offset, mtu: this.mtu });\n\n const slicedBuffer = buffer.slice(offset, offset + (this.mtu - 3 - 3));\n _console.log(\"slicedBuffer\", slicedBuffer);\n const bytesLeft = buffer.byteLength - offset;\n\n const progress = 1 - bytesLeft / buffer.byteLength;\n _console.log(\n `sending bytes ${offset}-${offset + slicedBuffer.byteLength} of ${\n buffer.byteLength\n } bytes (${progress * 100}%)`\n );\n this.#dispatchEvent(\"fileTransferProgress\", {\n progress,\n fileType: this.type!,\n });\n if (slicedBuffer.byteLength == 0) {\n _console.log(\"finished sending buffer\");\n this.#dispatchEvent(\"fileTransferComplete\", {\n direction: \"sending\",\n fileType: this.type!,\n });\n } else {\n await this.sendMessage([{ type: \"setFileBlock\", data: slicedBuffer }]);\n this.#bytesTransferred = offset + slicedBuffer.byteLength;\n //return this.#sendBlock(buffer, offset + slicedBuffer.byteLength);\n }\n }\n\n async #parseBytesTransferred(dataView: DataView) {\n _console.log(\"parseBytesTransferred\", dataView);\n const bytesTransferred = dataView.getUint32(0, true);\n _console.log({ bytesTransferred });\n if (this.status != \"sending\") {\n _console.error(`not currently sending file`);\n return;\n }\n if (!this.isServerSide && this.#bytesTransferred != bytesTransferred) {\n _console.error(\n `bytesTransferred are not equal - got ${bytesTransferred}, expected ${\n this.#bytesTransferred\n }`\n );\n this.cancel();\n return;\n }\n this.#sendBlock();\n }\n\n async receive(type: FileType) {\n this.#assertIsIdle();\n\n this.#assertValidType(type);\n\n await this.#setType(type);\n await this.#setCommand(\"startReceive\");\n }\n\n #isCancelling = false;\n async cancel() {\n this.#assertIsNotIdle();\n _console.log(\"cancelling file transfer...\");\n this.#isCancelling = true;\n await this.#setCommand(\"cancel\");\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n _console.log(\"redundant isServerSide assignment\");\n return;\n }\n _console.log({ newIsServerSide });\n this.#isServerSide = newIsServerSide;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required fileTransfer information\");\n const messages = RequiredFileTransferMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n clear() {\n this.#receivedBlocks.length = 0;\n this.#isCancelling = false;\n this.#buffer = undefined;\n this.#bytesTransferred = 0;\n this.#isServerSide = false;\n this.#checksum = 0;\n this.#fileTypes.length = 0;\n this.#type = undefined;\n this.#length = 0;\n this.#checksum = 0;\n this.#status = \"idle\";\n // @ts-expect-error\n this.mtu = undefined;\n }\n}\n\nexport default FileTransferManager;\n","import { PressureSensorPosition } from \"../sensor/PressureSensorDataManager.ts\";\nimport { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"MathUtils\", { log: false });\n\nexport function getInterpolation(\n value: number,\n min: number,\n max: number,\n span: number\n) {\n if (span == undefined) {\n span = max - min;\n }\n return (value - min) / span;\n}\n\nexport const Uint16Max = 2 ** 16;\nexport const Int16Max = 2 ** 15;\nexport const Int16Min = -(2 ** 15) - 1;\n\nfunction removeLower2Bytes(number: number) {\n const lower2Bytes = number % Uint16Max;\n return number - lower2Bytes;\n}\n\nconst timestampThreshold = 60_000;\n\nexport function parseTimestamp(dataView: DataView, byteOffset: number) {\n const now = Date.now();\n const nowWithoutLower2Bytes = removeLower2Bytes(now);\n const lower2Bytes = dataView.getUint16(byteOffset, true);\n let timestamp = nowWithoutLower2Bytes + lower2Bytes;\n if (Math.abs(now - timestamp) > timestampThreshold) {\n _console.log(\"correcting timestamp delta\");\n timestamp += Uint16Max * Math.sign(now - timestamp);\n }\n return timestamp;\n}\n\nexport interface Vector2 {\n x: number;\n y: number;\n}\n\nexport function getVector2Length(vector: Vector2) {\n const { x, y } = vector;\n return Math.sqrt(x ** 2 + y ** 2);\n}\n\nexport function getVector2Distance(a: Vector2, b: Vector2) {\n return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);\n}\n\nexport function getVector2DistanceSquared(a: Vector2, b: Vector2) {\n return (b.x - a.x) ** 2 + (b.y - a.y) ** 2;\n}\n\nexport function getVector2Angle(vector: Vector2) {\n const { x, y } = vector;\n return Math.atan2(y, x);\n}\n\nexport function getVector2Midpoint(a: Vector2, b: Vector2): Vector2 {\n return {\n x: (a.x + b.x) / 2,\n y: (a.y + b.y) / 2,\n };\n}\n\nexport function multiplyVector2ByScalar(\n vector: Vector2,\n scalar: number\n): Vector2 {\n let { x, y } = vector;\n x *= scalar;\n y *= scalar;\n return { x, y };\n}\nexport function normalizedVector2(vector: Vector2): Vector2 {\n return multiplyVector2ByScalar(vector, 1 / getVector2Length(vector));\n}\n\nexport interface Vector3 extends Vector2 {\n z: number;\n}\n\nexport interface Quaternion {\n x: number;\n y: number;\n z: number;\n w: number;\n}\n\nexport interface Euler {\n heading: number;\n pitch: number;\n roll: number;\n}\n\nexport function computeVoronoiWeights(\n points: PressureSensorPosition[],\n sampleCount = 100000\n) {\n const n = points.length;\n const counts = new Array(n).fill(0);\n\n for (let i = 0; i < sampleCount; i++) {\n const x = Math.random();\n const y = Math.random();\n\n // Find the closest input point\n let minDist = Infinity;\n let closestIndex = -1;\n\n for (let j = 0; j < n; j++) {\n const { x: px, y: py } = points[j];\n const dist = (px - x) ** 2 + (py - y) ** 2; // Squared Euclidean distance\n if (dist < minDist) {\n minDist = dist;\n closestIndex = j;\n }\n }\n\n // Increment count for the closest point\n counts[closestIndex]++;\n }\n\n // Convert counts to weights (sum to 1)\n return counts.map((c) => c / sampleCount);\n}\n\nexport function getVector3Length(vector: Vector3) {\n const { x, y, z } = vector;\n return Math.sqrt(x ** 2 + y ** 2 + z ** 2);\n}\n\nexport function clamp(value: number, min: number = 0, max: number = 1) {\n return Math.min(Math.max(value, min), max);\n}\n\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\nexport const twoPi = Math.PI * 2;\nexport function normalizeRadians(rad: number): number {\n return ((rad % twoPi) + twoPi) % twoPi;\n}\n\nexport function pointInPolygon(pt: Vector2, polygon: Vector2[]): boolean {\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const xi = polygon[i].x,\n yi = polygon[i].y;\n const xj = polygon[j].x,\n yj = polygon[j].y;\n\n const intersect =\n yi > pt.y !== yj > pt.y &&\n pt.x < ((xj - xi) * (pt.y - yi)) / (yj - yi) + xi;\n if (intersect) inside = !inside;\n }\n return inside;\n}\n","import { getInterpolation } from \"./MathUtils.ts\";\n\nexport interface Range {\n min: number;\n max: number;\n span: number;\n}\n\nconst initialRange: Range = { min: Infinity, max: -Infinity, span: 0 };\n\nclass RangeHelper {\n #range: Range = structuredClone(initialRange);\n get min() {\n return this.#range.min;\n }\n get max() {\n return this.#range.max;\n }\n get span() {\n return this.#range.span;\n }\n\n get range() {\n return structuredClone(this.#range);\n }\n\n set min(newMin) {\n this.#range.min = newMin;\n this.#range.max = Math.max(newMin, this.#range.max);\n this.#updateSpan();\n }\n set max(newMax) {\n this.#range.max = newMax;\n this.#range.min = Math.min(newMax, this.#range.min);\n this.#updateSpan();\n }\n\n #updateSpan() {\n this.#range.span = this.#range.max - this.#range.min;\n }\n\n reset() {\n Object.assign(this.#range, initialRange);\n }\n\n update(value: number) {\n this.#range.min = Math.min(value, this.#range.min);\n this.#range.max = Math.max(value, this.#range.max);\n this.#updateSpan();\n }\n\n getNormalization(value: number, weightByRange: boolean) {\n let normalization = getInterpolation(\n value,\n this.#range.min,\n this.#range.max,\n this.#range.span\n );\n if (weightByRange) {\n normalization *= this.#range.span;\n }\n return normalization || 0;\n }\n\n updateAndGetNormalization(value: number, weightByRange: boolean) {\n this.update(value);\n return this.getNormalization(value, weightByRange);\n }\n}\n\nexport default RangeHelper;\n","import RangeHelper from \"./RangeHelper.ts\";\n\nimport { Vector2 } from \"./MathUtils.ts\";\n\nexport type CenterOfPressure = Vector2;\n\nexport interface CenterOfPressureRange {\n x: RangeHelper;\n y: RangeHelper;\n}\n\nclass CenterOfPressureHelper {\n #range: CenterOfPressureRange = {\n x: new RangeHelper(),\n y: new RangeHelper(),\n };\n reset() {\n this.#range.x.reset();\n this.#range.y.reset();\n }\n\n update(centerOfPressure: CenterOfPressure) {\n this.#range.x.update(centerOfPressure.x);\n this.#range.y.update(centerOfPressure.y);\n }\n getNormalization(centerOfPressure: CenterOfPressure, weightByRange: boolean): CenterOfPressure {\n return {\n x: this.#range.x.getNormalization(centerOfPressure.x, weightByRange),\n y: this.#range.y.getNormalization(centerOfPressure.y, weightByRange),\n };\n }\n\n updateAndGetNormalization(centerOfPressure: CenterOfPressure, weightByRange: boolean) {\n this.update(centerOfPressure);\n return this.getNormalization(centerOfPressure, weightByRange);\n }\n}\n\nexport default CenterOfPressureHelper;\n","export function createArray(arrayLength: number, objectOrCallback: ((index: number) => any) | object) {\n return new Array(arrayLength).fill(1).map((_, index) => {\n if (typeof objectOrCallback == \"function\") {\n const callback = objectOrCallback;\n return callback(index);\n } else {\n const object = objectOrCallback;\n return Object.assign({}, object);\n }\n });\n}\n\nexport function arrayWithoutDuplicates(array: any[]) {\n return array.filter((value, index) => array.indexOf(value) == index);\n}\n","import { createConsole } from \"../utils/Console.ts\";\nimport CenterOfPressureHelper from \"../utils/CenterOfPressureHelper.ts\";\nimport RangeHelper from \"../utils/RangeHelper.ts\";\nimport { createArray } from \"../utils/ArrayUtils.ts\";\n\nconst _console = createConsole(\"PressureDataManager\", { log: false });\n\nexport const PressureSensorTypes = [\"pressure\"] as const;\nexport type PressureSensorType = (typeof PressureSensorTypes)[number];\n\nexport const ContinuousPressureSensorTypes = PressureSensorTypes;\nexport type ContinuousPressureSensorType =\n (typeof ContinuousPressureSensorTypes)[number];\n\nimport { computeVoronoiWeights, Vector2 } from \"../utils/MathUtils.ts\";\nexport type PressureSensorPosition = Vector2;\n\nimport { CenterOfPressure } from \"../utils/CenterOfPressureHelper.ts\";\n\nexport interface PressureSensorValue {\n position: PressureSensorPosition;\n rawValue: number;\n scaledValue: number;\n normalizedValue: number;\n weightedValue: number;\n}\n\nexport interface PressureData {\n sensors: PressureSensorValue[];\n scaledSum: number;\n normalizedSum: number;\n center?: CenterOfPressure;\n normalizedCenter?: CenterOfPressure;\n}\n\nexport interface PressureDataEventMessages {\n pressure: { pressure: PressureData };\n}\n\nexport const DefaultNumberOfPressureSensors = 8;\n\nclass PressureSensorDataManager {\n #positions: PressureSensorPosition[] = [];\n get positions() {\n return this.#positions;\n }\n\n get numberOfSensors() {\n return this.positions.length;\n }\n\n parsePositions(dataView: DataView) {\n const positions: PressureSensorPosition[] = [];\n\n for (\n let pressureSensorIndex = 0, byteOffset = 0;\n byteOffset < dataView.byteLength;\n pressureSensorIndex++, byteOffset += 2\n ) {\n positions.push({\n x: dataView.getUint8(byteOffset) / 2 ** 8,\n y: dataView.getUint8(byteOffset + 1) / 2 ** 8,\n });\n }\n\n _console.log({ positions });\n\n this.#positions = positions;\n\n this.#sensorRangeHelpers = createArray(\n this.numberOfSensors,\n () => new RangeHelper()\n );\n\n this.resetRange();\n }\n\n #sensorRangeHelpers!: RangeHelper[];\n #normalizedSumRangeHelper = new RangeHelper();\n\n #centerOfPressureHelper = new CenterOfPressureHelper();\n\n resetRange() {\n this.#sensorRangeHelpers?.forEach((rangeHelper) => rangeHelper.reset());\n this.#centerOfPressureHelper.reset();\n this.#normalizedSumRangeHelper.reset();\n }\n\n parseData(dataView: DataView, scalar: number) {\n const pressure: PressureData = {\n sensors: [],\n scaledSum: 0,\n normalizedSum: 0,\n };\n for (\n let index = 0, byteOffset = 0;\n byteOffset < dataView.byteLength;\n index++, byteOffset += 2\n ) {\n const rawValue = dataView.getUint16(byteOffset, true);\n let scaledValue = (rawValue * scalar) / this.numberOfSensors;\n const rangeHelper = this.#sensorRangeHelpers[index];\n const normalizedValue = rangeHelper.updateAndGetNormalization(\n scaledValue,\n false\n );\n //scaledValue -= rangeHelper.min;\n\n const position = this.positions[index];\n pressure.sensors[index] = {\n rawValue,\n scaledValue,\n normalizedValue,\n position,\n weightedValue: 0,\n };\n\n pressure.scaledSum += scaledValue;\n //pressure.normalizedSum += normalizedValue;\n }\n pressure.normalizedSum =\n this.#normalizedSumRangeHelper.updateAndGetNormalization(\n pressure.scaledSum,\n false\n );\n\n if (pressure.scaledSum > 0) {\n pressure.center = { x: 0, y: 0 };\n pressure.sensors.forEach((sensor) => {\n sensor.weightedValue = sensor.scaledValue / pressure.scaledSum;\n pressure.center!.x += sensor.position.x * sensor.weightedValue;\n pressure.center!.y += sensor.position.y * sensor.weightedValue;\n });\n pressure.normalizedCenter =\n this.#centerOfPressureHelper.updateAndGetNormalization(\n pressure.center,\n false\n );\n }\n\n _console.log({ pressure });\n return pressure;\n }\n}\n\nexport default PressureSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\n\nconst _console = createConsole(\"MotionSensorDataManager\", { log: false });\n\nexport const MotionSensorTypes = [\n \"acceleration\",\n \"gravity\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n \"gameRotation\",\n \"rotation\",\n \"orientation\",\n \"activity\",\n \"stepCounter\",\n \"stepDetector\",\n \"deviceOrientation\",\n \"tapDetector\",\n] as const;\nexport type MotionSensorType = (typeof MotionSensorTypes)[number];\n\nexport const ContinuousMotionTypes = [\n \"acceleration\",\n \"gravity\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n \"gameRotation\",\n \"rotation\",\n \"orientation\",\n] as const;\nexport type ContinuousMotionType = (typeof ContinuousMotionTypes)[number];\n\nimport { Vector3, Quaternion, Euler } from \"../utils/MathUtils.ts\";\nimport { ValueOf } from \"../utils/TypeScriptUtils.ts\";\n\nexport const Vector2Size = 2 * 2;\nexport const Vector3Size = 3 * 2;\nexport const QuaternionSize = 4 * 2;\n\nexport const ActivityTypes = [\n \"still\",\n \"walking\",\n \"running\",\n \"bicycle\",\n \"vehicle\",\n \"tilting\",\n] as const;\nexport type ActivityType = (typeof ActivityTypes)[number];\n\nexport interface Activity {\n still: boolean;\n walking: boolean;\n running: boolean;\n bicycle: boolean;\n vehicle: boolean;\n tilting: boolean;\n}\n\nexport const DeviceOrientations = [\n \"portraitUpright\",\n \"landscapeLeft\",\n \"portraitUpsideDown\",\n \"landscapeRight\",\n \"unknown\",\n] as const;\nexport type DeviceOrientation = (typeof DeviceOrientations)[number];\n\nexport interface MotionSensorDataEventMessages {\n acceleration: { acceleration: Vector3 };\n gravity: { gravity: Vector3 };\n linearAcceleration: { linearAcceleration: Vector3 };\n gyroscope: { gyroscope: Vector3 };\n magnetometer: { magnetometer: Vector3 };\n gameRotation: { gameRotation: Quaternion };\n rotation: { rotation: Quaternion };\n orientation: { orientation: Euler };\n stepDetector: { stepDetector: Object };\n stepCounter: { stepCounter: number };\n activity: { activity: Activity };\n deviceOrientation: { deviceOrientation: DeviceOrientation };\n tapDetector: { tapDetector: Object };\n}\n\nexport type MotionSensorDataEventMessage =\n ValueOf<MotionSensorDataEventMessages>;\n\nclass MotionSensorDataManager {\n parseVector3(dataView: DataView, scalar: number): Vector3 {\n let [x, y, z] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n ].map((value) => value * scalar);\n\n const vector: Vector3 = { x, y, z };\n\n _console.log({ vector });\n return vector;\n }\n\n parseQuaternion(dataView: DataView, scalar: number): Quaternion {\n let [x, y, z, w] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n dataView.getInt16(6, true),\n ].map((value) => value * scalar);\n\n const quaternion: Quaternion = { x, y, z, w };\n\n _console.log({ quaternion });\n return quaternion;\n }\n\n parseEuler(dataView: DataView, scalar: number): Euler {\n let [heading, pitch, roll] = [\n dataView.getInt16(0, true),\n dataView.getInt16(2, true),\n dataView.getInt16(4, true),\n ].map((value) => value * scalar);\n\n pitch *= -1;\n heading *= -1;\n if (heading < 0) {\n heading += 360;\n }\n\n const euler: Euler = { heading, pitch, roll };\n\n _console.log({ euler });\n return euler;\n }\n\n parseStepCounter(dataView: DataView) {\n _console.log(\"parseStepCounter\", dataView);\n const stepCount = dataView.getUint32(0, true);\n _console.log({ stepCount });\n return stepCount;\n }\n\n parseActivity(dataView: DataView) {\n _console.log(\"parseActivity\", dataView);\n const activity: Partial<Activity> = {};\n\n const activityBitfield = dataView.getUint8(0);\n _console.log(\"activityBitfield\", activityBitfield.toString(2));\n ActivityTypes.forEach((activityType, index) => {\n activity[activityType] = Boolean(activityBitfield & (1 << index));\n });\n\n _console.log(\"activity\", activity);\n\n return activity as Activity;\n }\n\n parseDeviceOrientation(dataView: DataView) {\n _console.log(\"parseDeviceOrientation\", dataView);\n const index = dataView.getUint8(0);\n const deviceOrientation = DeviceOrientations[index];\n _console.assertWithError(deviceOrientation, \"undefined deviceOrientation\");\n _console.log({ deviceOrientation });\n return deviceOrientation;\n }\n}\n\nexport default MotionSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\n\nexport const BarometerSensorTypes = [\"barometer\"] as const;\nexport type BarometerSensorType = (typeof BarometerSensorTypes)[number];\n\nexport const ContinuousBarometerSensorTypes = BarometerSensorTypes;\nexport type ContinuousBarometerSensorType = (typeof ContinuousBarometerSensorTypes)[number];\n\nexport interface BarometerSensorDataEventMessages {\n barometer: {\n barometer: number;\n //altitude: number;\n };\n}\n\nconst _console = createConsole(\"BarometerSensorDataManager\", { log: false });\n\nclass BarometerSensorDataManager {\n #calculcateAltitude(pressure: number) {\n const P0 = 101325; // Standard atmospheric pressure at sea level in Pascals\n const T0 = 288.15; // Standard temperature at sea level in Kelvin\n const L = 0.0065; // Temperature lapse rate in K/m\n const R = 8.3144598; // Universal gas constant in J/(mol·K)\n const g = 9.80665; // Acceleration due to gravity in m/s²\n const M = 0.0289644; // Molar mass of Earth's air in kg/mol\n\n const exponent = (R * L) / (g * M);\n const h = (T0 / L) * (1 - Math.pow(pressure / P0, exponent));\n\n return h;\n }\n\n parseData(dataView: DataView, scalar: number) {\n const pressure = dataView.getUint32(0, true) * scalar;\n const altitude = this.#calculcateAltitude(pressure);\n _console.log({ pressure, altitude });\n return { pressure };\n }\n}\n\nexport default BarometerSensorDataManager;\n","import { sliceDataView } from \"./ArrayBufferUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { textDecoder } from \"./Text.ts\";\n\nconst _console = createConsole(\"ParseUtils\", { log: false });\n\nexport function parseStringFromDataView(\n dataView: DataView,\n byteOffset: number = 0\n) {\n const stringLength = dataView.getUint8(byteOffset++);\n const string = textDecoder.decode(\n dataView.buffer.slice(\n dataView.byteOffset + byteOffset,\n dataView.byteOffset + byteOffset + stringLength\n )\n );\n byteOffset += stringLength;\n return { string, byteOffset };\n}\n\nexport function parseMessage<MessageType extends string>(\n dataView: DataView,\n messageTypes: readonly MessageType[],\n callback: (\n messageType: MessageType,\n dataView: DataView,\n context?: any\n ) => void,\n context?: any,\n parseMessageLengthAsUint16: boolean = false\n) {\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const messageTypeEnum = dataView.getUint8(byteOffset++);\n _console.assertWithError(\n messageTypeEnum in messageTypes,\n `invalid messageTypeEnum ${messageTypeEnum}`\n );\n const messageType = messageTypes[messageTypeEnum];\n\n let messageLength: number;\n if (parseMessageLengthAsUint16) {\n messageLength = dataView.getUint16(byteOffset, true);\n byteOffset += 2;\n } else {\n messageLength = dataView.getUint8(byteOffset++);\n }\n\n _console.log({\n messageTypeEnum,\n messageType,\n messageLength,\n dataView,\n byteOffset,\n });\n\n const _dataView = sliceDataView(dataView, byteOffset, messageLength);\n _console.log({ _dataView });\n\n callback(messageType, _dataView, context);\n\n byteOffset += messageLength;\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInNode } from \"./utils/environment.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport { parseMessage } from \"./utils/ParseUtils.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\n\nconst _console = createConsole(\"CameraManager\", { log: false });\n\nexport const CameraSensorTypes = [\"camera\"] as const;\nexport type CameraSensorType = (typeof CameraSensorTypes)[number];\n\nexport const CameraCommands = [\n \"focus\",\n \"takePicture\",\n \"stop\",\n \"sleep\",\n \"wake\",\n] as const;\nexport type CameraCommand = (typeof CameraCommands)[number];\n\nexport const CameraStatuses = [\n \"idle\",\n \"focusing\",\n \"takingPicture\",\n \"asleep\",\n] as const;\nexport type CameraStatus = (typeof CameraStatuses)[number];\n\nexport const CameraDataTypes = [\n \"headerSize\",\n \"header\",\n \"imageSize\",\n \"image\",\n \"footerSize\",\n \"footer\",\n] as const;\nexport type CameraDataType = (typeof CameraDataTypes)[number];\n\nexport const CameraConfigurationTypes = [\n \"resolution\",\n \"qualityFactor\",\n \"shutter\",\n \"gain\",\n \"redGain\",\n \"greenGain\",\n \"blueGain\",\n] as const;\nexport type CameraConfigurationType = (typeof CameraConfigurationTypes)[number];\n\nexport const CameraMessageTypes = [\n \"cameraStatus\",\n \"cameraCommand\",\n \"getCameraConfiguration\",\n \"setCameraConfiguration\",\n \"cameraData\",\n] as const;\nexport type CameraMessageType = (typeof CameraMessageTypes)[number];\n\nexport type CameraConfiguration = {\n [cameraConfigurationType in CameraConfigurationType]?: number;\n};\nexport type CameraConfigurationRanges = {\n [cameraConfigurationType in CameraConfigurationType]: {\n min: number;\n max: number;\n };\n};\n\nexport const RequiredCameraMessageTypes: CameraMessageType[] = [\n \"getCameraConfiguration\",\n \"cameraStatus\",\n] as const;\n\nexport const CameraEventTypes = [\n ...CameraMessageTypes,\n \"cameraImageProgress\",\n \"cameraImage\",\n] as const;\nexport type CameraEventType = (typeof CameraEventTypes)[number];\n\nexport interface CameraEventMessages {\n cameraStatus: {\n cameraStatus: CameraStatus;\n previousCameraStatus: CameraStatus;\n };\n getCameraConfiguration: { cameraConfiguration: CameraConfiguration };\n cameraImageProgress: { progress: number; type: CameraDataType };\n cameraImage: { blob: Blob; url: string };\n}\n\nexport type CameraEventDispatcher = EventDispatcher<\n Device,\n CameraEventType,\n CameraEventMessages\n>;\nexport type SendCameraMessageCallback = SendMessageCallback<CameraMessageType>;\n\nclass CameraManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendCameraMessageCallback;\n\n eventDispatcher!: CameraEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required camera information\");\n const messages = RequiredCameraMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // CAMERA STATUS\n #cameraStatus!: CameraStatus;\n get cameraStatus() {\n return this.#cameraStatus;\n }\n #parseCameraStatus(dataView: DataView) {\n const cameraStatusIndex = dataView.getUint8(0);\n const newCameraStatus = CameraStatuses[cameraStatusIndex];\n this.#updateCameraStatus(newCameraStatus);\n }\n #updateCameraStatus(newCameraStatus: CameraStatus) {\n _console.assertEnumWithError(newCameraStatus, CameraStatuses);\n if (newCameraStatus == this.#cameraStatus) {\n _console.log(`redundant cameraStatus ${newCameraStatus}`);\n return;\n }\n const previousCameraStatus = this.#cameraStatus;\n this.#cameraStatus = newCameraStatus;\n _console.log(`updated cameraStatus to \"${this.cameraStatus}\"`);\n this.#dispatchEvent(\"cameraStatus\", {\n cameraStatus: this.cameraStatus,\n previousCameraStatus,\n });\n\n if (\n this.#cameraStatus != \"takingPicture\" &&\n this.#imageProgress > 0 &&\n !this.#didBuildImage\n ) {\n this.#buildImage();\n }\n }\n\n // CAMERA COMMAND\n async #sendCameraCommand(command: CameraCommand, sendImmediately?: boolean) {\n _console.assertEnumWithError(command, CameraCommands);\n _console.log(`sending camera command \"${command}\"`);\n\n const promise = this.waitForEvent(\"cameraStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = CameraCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"cameraCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsAsleep() {\n _console.assertWithError(\n this.#cameraStatus == \"asleep\",\n `camera is not asleep - currently ${this.#cameraStatus}`\n );\n }\n #assertIsAwake() {\n _console.assertWithError(\n this.#cameraStatus != \"asleep\",\n `camera is not awake - currently ${this.#cameraStatus}`\n );\n }\n async focus() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"focus\");\n }\n async takePicture() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"takePicture\");\n }\n async stop() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"stop\");\n }\n async sleep() {\n this.#assertIsAwake();\n await this.#sendCameraCommand(\"sleep\");\n }\n async wake() {\n this.#assertIsAsleep();\n await this.#sendCameraCommand(\"wake\");\n }\n\n // CAMERA DATA\n #parseCameraData(dataView: DataView) {\n _console.log(\"parsing camera data\", dataView);\n parseMessage(\n dataView,\n CameraDataTypes,\n this.#onCameraData.bind(this),\n null,\n true\n );\n }\n #onCameraData(cameraDataType: CameraDataType, dataView: DataView) {\n _console.log({ cameraDataType, dataView });\n switch (cameraDataType) {\n case \"headerSize\":\n this.#headerSize = dataView.getUint16(0, true);\n _console.log({ headerSize: this.#headerSize });\n this.#headerData = undefined;\n this.#headerProgress == 0;\n break;\n case \"header\":\n this.#headerData = concatenateArrayBuffers(this.#headerData, dataView);\n _console.log({ headerData: this.#headerData });\n this.#headerProgress = this.#headerData?.byteLength / this.#headerSize;\n _console.log({ headerProgress: this.#headerProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#headerProgress,\n type: \"header\",\n });\n if (this.#headerProgress == 1) {\n _console.log(\"finished getting header data\");\n }\n break;\n case \"imageSize\":\n this.#imageSize = dataView.getUint16(0, true);\n _console.log({ imageSize: this.#imageSize });\n this.#imageData = undefined;\n this.#imageProgress == 0;\n this.#didBuildImage = false;\n break;\n case \"image\":\n this.#imageData = concatenateArrayBuffers(this.#imageData, dataView);\n _console.log({ imageData: this.#imageData });\n this.#imageProgress = this.#imageData?.byteLength / this.#imageSize;\n _console.log({ imageProgress: this.#imageProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#imageProgress,\n type: \"image\",\n });\n if (this.#imageProgress == 1) {\n _console.log(\"finished getting image data\");\n if (this.#headerProgress == 1 && this.#footerProgress == 1) {\n this.#buildImage();\n }\n }\n break;\n case \"footerSize\":\n this.#footerSize = dataView.getUint16(0, true);\n _console.log({ footerSize: this.#footerSize });\n this.#footerData = undefined;\n this.#footerProgress == 0;\n break;\n case \"footer\":\n this.#footerData = concatenateArrayBuffers(this.#footerData, dataView);\n _console.log({ footerData: this.#footerData });\n this.#footerProgress = this.#footerData?.byteLength / this.#footerSize;\n _console.log({ footerProgress: this.#footerProgress });\n this.#dispatchEvent(\"cameraImageProgress\", {\n progress: this.#footerProgress,\n type: \"footer\",\n });\n if (this.#footerProgress == 1) {\n _console.log(\"finished getting footer data\");\n if (this.#imageProgress == 1) {\n this.#buildImage();\n }\n }\n break;\n }\n }\n\n #headerSize: number = 0;\n #headerData?: ArrayBuffer;\n #headerProgress: number = 0;\n\n #imageSize: number = 0;\n #imageData?: ArrayBuffer;\n #imageProgress: number = 0;\n\n #footerSize: number = 0;\n #footerData?: ArrayBuffer;\n #footerProgress: number = 0;\n\n #didBuildImage: boolean = false;\n #buildImage() {\n _console.log(\"building image...\");\n const imageData = concatenateArrayBuffers(\n this.#headerData,\n this.#imageData,\n this.#footerData\n );\n _console.log({ imageData });\n\n let blob = new Blob([imageData], { type: \"image/jpeg\" });\n _console.log(\"created blob\", blob);\n\n const url = URL.createObjectURL(blob);\n _console.log(\"created url\", url);\n\n this.#dispatchEvent(\"cameraImage\", { url, blob });\n\n this.#didBuildImage = true;\n }\n\n // CONFIG\n #cameraConfiguration: CameraConfiguration = {};\n get cameraConfiguration() {\n return this.#cameraConfiguration;\n }\n #availableCameraConfigurationTypes!: CameraConfigurationType[];\n get availableCameraConfigurationTypes() {\n return this.#availableCameraConfigurationTypes;\n }\n\n #cameraConfigurationRanges: CameraConfigurationRanges = {\n resolution: { min: 100, max: 720 },\n qualityFactor: { min: 15, max: 60 },\n shutter: { min: 4, max: 16383 },\n gain: { min: 1, max: 248 },\n redGain: { min: 0, max: 1023 },\n greenGain: { min: 0, max: 1023 },\n blueGain: { min: 0, max: 1023 },\n };\n get cameraConfigurationRanges() {\n return this.#cameraConfigurationRanges;\n }\n\n #parseCameraConfiguration(dataView: DataView) {\n const parsedCameraConfiguration: CameraConfiguration = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const cameraConfigurationTypeIndex = dataView.getUint8(byteOffset++);\n const cameraConfigurationType =\n CameraConfigurationTypes[cameraConfigurationTypeIndex];\n _console.assertWithError(\n cameraConfigurationType,\n `invalid cameraConfigurationTypeIndex ${cameraConfigurationTypeIndex}`\n );\n parsedCameraConfiguration[cameraConfigurationType] = dataView.getUint16(\n byteOffset,\n true\n );\n byteOffset += 2;\n }\n\n _console.log({ parsedCameraConfiguration });\n this.#availableCameraConfigurationTypes = Object.keys(\n parsedCameraConfiguration\n ) as CameraConfigurationType[];\n this.#cameraConfiguration = parsedCameraConfiguration;\n this.#dispatchEvent(\"getCameraConfiguration\", {\n cameraConfiguration: this.#cameraConfiguration,\n });\n }\n\n #isCameraConfigurationRedundant(cameraConfiguration: CameraConfiguration) {\n let cameraConfigurationTypes = Object.keys(\n cameraConfiguration\n ) as CameraConfigurationType[];\n return cameraConfigurationTypes.every((cameraConfigurationType) => {\n return (\n this.cameraConfiguration[cameraConfigurationType] ==\n cameraConfiguration[cameraConfigurationType]\n );\n });\n }\n async setCameraConfiguration(newCameraConfiguration: CameraConfiguration) {\n _console.log({ newCameraConfiguration });\n if (this.#isCameraConfigurationRedundant(newCameraConfiguration)) {\n _console.log(\"redundant camera configuration\");\n return;\n }\n const setCameraConfigurationData = this.#createData(newCameraConfiguration);\n _console.log({ setCameraConfigurationData });\n\n const promise = this.waitForEvent(\"getCameraConfiguration\");\n this.sendMessage([\n {\n type: \"setCameraConfiguration\",\n data: setCameraConfigurationData.buffer,\n },\n ]);\n await promise;\n }\n\n #assertAvailableCameraConfigurationType(\n cameraConfigurationType: CameraConfigurationType\n ) {\n _console.assertWithError(\n this.#availableCameraConfigurationTypes,\n \"must get initial cameraConfiguration\"\n );\n const isCameraConfigurationTypeAvailable =\n this.#availableCameraConfigurationTypes?.includes(\n cameraConfigurationType\n );\n _console.assertWithError(\n isCameraConfigurationTypeAvailable,\n `unavailable camera configuration type \"${cameraConfigurationType}\"`\n );\n return isCameraConfigurationTypeAvailable;\n }\n\n static AssertValidCameraConfigurationType(\n cameraConfigurationType: CameraConfigurationType\n ) {\n _console.assertEnumWithError(\n cameraConfigurationType,\n CameraConfigurationTypes\n );\n }\n static AssertValidCameraConfigurationTypeEnum(\n cameraConfigurationTypeEnum: number\n ) {\n _console.assertTypeWithError(cameraConfigurationTypeEnum, \"number\");\n _console.assertWithError(\n cameraConfigurationTypeEnum in CameraConfigurationTypes,\n `invalid cameraConfigurationTypeEnum ${cameraConfigurationTypeEnum}`\n );\n }\n\n #createData(cameraConfiguration: CameraConfiguration) {\n let cameraConfigurationTypes = Object.keys(\n cameraConfiguration\n ) as CameraConfigurationType[];\n cameraConfigurationTypes = cameraConfigurationTypes.filter(\n (cameraConfigurationType) =>\n this.#assertAvailableCameraConfigurationType(cameraConfigurationType)\n );\n\n const dataView = new DataView(\n new ArrayBuffer(cameraConfigurationTypes.length * 3)\n );\n cameraConfigurationTypes.forEach((cameraConfigurationType, index) => {\n CameraManager.AssertValidCameraConfigurationType(cameraConfigurationType);\n const cameraConfigurationTypeEnum = CameraConfigurationTypes.indexOf(\n cameraConfigurationType\n );\n dataView.setUint8(index * 3, cameraConfigurationTypeEnum);\n\n const value = cameraConfiguration[cameraConfigurationType]!;\n //this.#assertValidCameraConfigurationValue(cameraConfigurationType, value);\n dataView.setUint16(index * 3 + 1, value, true);\n });\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // MESSAGE\n parseMessage(messageType: CameraMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"cameraStatus\":\n this.#parseCameraStatus(dataView);\n break;\n case \"getCameraConfiguration\":\n case \"setCameraConfiguration\":\n this.#parseCameraConfiguration(dataView);\n break;\n case \"cameraData\":\n this.#parseCameraData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n // @ts-ignore\n this.#cameraStatus = undefined;\n this.#headerProgress = 0;\n this.#imageProgress = 0;\n this.#footerProgress = 0;\n }\n}\n\nexport default CameraManager;\n","import { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"AudioUtils\", { log: false });\n\nexport function float32ArrayToWav(\n audioData: Float32Array,\n sampleRate: number,\n numChannels: number\n): Blob {\n const wavBuffer = encodeWAV(audioData, sampleRate, numChannels);\n return new Blob([wavBuffer], { type: \"audio/wav\" });\n}\n\nfunction encodeWAV(\n interleaved: Float32Array,\n sampleRate: number,\n numChannels: number\n): ArrayBuffer {\n const buffer = new ArrayBuffer(44 + interleaved.length * 2); // 44 bytes for WAV header\n const view = new DataView(buffer);\n\n // RIFF identifier\n writeString(view, 0, \"RIFF\");\n // File length minus RIFF identifier length and file description length\n view.setUint32(4, 36 + interleaved.length * 2, true);\n // RIFF type\n writeString(view, 8, \"WAVE\");\n // Format chunk identifier\n writeString(view, 12, \"fmt \");\n // Format chunk length\n view.setUint32(16, 16, true);\n // Sample format (raw)\n view.setUint16(20, 1, true);\n // Channel count\n view.setUint16(22, numChannels, true);\n // Sample rate\n view.setUint32(24, sampleRate, true);\n // Byte rate (sample rate * block align)\n view.setUint32(28, sampleRate * numChannels * 2, true);\n // Block align (channel count * bytes per sample)\n view.setUint16(32, numChannels * 2, true);\n // Bits per sample\n view.setUint16(34, 16, true);\n // Data chunk identifier\n writeString(view, 36, \"data\");\n // Data chunk length\n view.setUint32(40, interleaved.length * 2, true);\n\n // Write interleaved audio data\n for (let i = 0; i < interleaved.length; i++) {\n view.setInt16(44 + i * 2, interleaved[i] * 0x7fff, true); // Convert float [-1, 1] to int16\n }\n\n return buffer;\n}\n\nexport function writeString(\n view: DataView,\n offset: number,\n string: string\n): void {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\nimport { float32ArrayToWav } from \"./utils/AudioUtils.ts\";\n\nconst _console = createConsole(\"MicrophoneManager\", { log: false });\n\nexport const MicrophoneSensorTypes = [\"microphone\"] as const;\nexport type MicrophoneSensorType = (typeof MicrophoneSensorTypes)[number];\n\nexport const MicrophoneCommands = [\"start\", \"stop\", \"vad\"] as const;\nexport type MicrophoneCommand = (typeof MicrophoneCommands)[number];\n\nexport const MicrophoneStatuses = [\"idle\", \"streaming\", \"vad\"] as const;\nexport type MicrophoneStatus = (typeof MicrophoneStatuses)[number];\n\nexport const MicrophoneConfigurationTypes = [\"sampleRate\", \"bitDepth\"] as const;\nexport type MicrophoneConfigurationType =\n (typeof MicrophoneConfigurationTypes)[number];\n\nexport const MicrophoneSampleRates = [\"8000\", \"16000\"] as const;\nexport type MicrophoneSampleRate = (typeof MicrophoneSampleRates)[number];\n\nexport const MicrophoneBitDepths = [\"8\", \"16\"] as const;\nexport type MicrophoneBitDepth = (typeof MicrophoneBitDepths)[number];\n\nexport const MicrophoneMessageTypes = [\n \"microphoneStatus\",\n \"microphoneCommand\",\n \"getMicrophoneConfiguration\",\n \"setMicrophoneConfiguration\",\n \"microphoneData\",\n] as const;\nexport type MicrophoneMessageType = (typeof MicrophoneMessageTypes)[number];\n\nexport type MicrophoneConfiguration = {\n sampleRate?: MicrophoneSampleRate;\n bitDepth?: MicrophoneBitDepth;\n};\n\nexport const MicrophoneConfigurationValues = {\n sampleRate: MicrophoneSampleRates,\n bitDepth: MicrophoneBitDepths,\n};\n\nexport const RequiredMicrophoneMessageTypes: MicrophoneMessageType[] = [\n \"getMicrophoneConfiguration\",\n \"microphoneStatus\",\n] as const;\n\nexport const MicrophoneEventTypes = [\n ...MicrophoneMessageTypes,\n \"isRecordingMicrophone\",\n \"microphoneRecording\",\n] as const;\nexport type MicrophoneEventType = (typeof MicrophoneEventTypes)[number];\n\nexport interface MicrophoneEventMessages {\n microphoneStatus: {\n microphoneStatus: MicrophoneStatus;\n previousMicrophoneStatus: MicrophoneStatus;\n };\n getMicrophoneConfiguration: {\n microphoneConfiguration: MicrophoneConfiguration;\n };\n microphoneData: {\n samples: Float32Array;\n sampleRate: MicrophoneSampleRate;\n bitDepth: MicrophoneBitDepth;\n };\n isRecordingMicrophone: {\n isRecordingMicrophone: boolean;\n };\n microphoneRecording: {\n samples: Float32Array;\n sampleRate: MicrophoneSampleRate;\n bitDepth: MicrophoneBitDepth;\n blob: Blob;\n url: string;\n };\n}\n\nexport type MicrophoneEventDispatcher = EventDispatcher<\n Device,\n MicrophoneEventType,\n MicrophoneEventMessages\n>;\nexport type SendMicrophoneMessageCallback =\n SendMessageCallback<MicrophoneMessageType>;\n\nclass MicrophoneManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendMicrophoneMessageCallback;\n\n eventDispatcher!: MicrophoneEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required microphone information\");\n const messages = RequiredMicrophoneMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // MICROPHONE STATUS\n #microphoneStatus!: MicrophoneStatus;\n get microphoneStatus() {\n return this.#microphoneStatus;\n }\n #parseMicrophoneStatus(dataView: DataView) {\n const microphoneStatusIndex = dataView.getUint8(0);\n const newMicrophoneStatus = MicrophoneStatuses[microphoneStatusIndex];\n this.#updateMicrophoneStatus(newMicrophoneStatus);\n }\n #updateMicrophoneStatus(newMicrophoneStatus: MicrophoneStatus) {\n _console.assertEnumWithError(newMicrophoneStatus, MicrophoneStatuses);\n if (newMicrophoneStatus == this.#microphoneStatus) {\n _console.log(`redundant microphoneStatus ${newMicrophoneStatus}`);\n return;\n }\n const previousMicrophoneStatus = this.#microphoneStatus;\n this.#microphoneStatus = newMicrophoneStatus;\n _console.log(`updated microphoneStatus to \"${this.microphoneStatus}\"`);\n this.#dispatchEvent(\"microphoneStatus\", {\n microphoneStatus: this.microphoneStatus,\n previousMicrophoneStatus,\n });\n }\n\n // MICROPHONE COMMAND\n async #sendMicrophoneCommand(\n command: MicrophoneCommand,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(command, MicrophoneCommands);\n _console.log(`sending microphone command \"${command}\"`);\n\n const promise = this.waitForEvent(\"microphoneStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = MicrophoneCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"microphoneCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsIdle() {\n _console.assertWithError(\n this.#microphoneStatus == \"idle\",\n `microphone is not idle - currently ${this.#microphoneStatus}`\n );\n }\n #assertIsNotIdle() {\n _console.assertWithError(\n this.#microphoneStatus != \"idle\",\n `microphone is idle`\n );\n }\n #assertIsStreaming() {\n _console.assertWithError(\n this.#microphoneStatus == \"streaming\",\n `microphone is not recording - currently ${this.#microphoneStatus}`\n );\n }\n\n async start() {\n await this.#sendMicrophoneCommand(\"start\");\n }\n async stop() {\n if (this.microphoneStatus == \"idle\") {\n _console.log(\"microphone is already idle\");\n return;\n }\n await this.#sendMicrophoneCommand(\"stop\");\n }\n async vad() {\n await this.#sendMicrophoneCommand(\"vad\");\n }\n async toggle() {\n switch (this.microphoneStatus) {\n case \"idle\":\n this.start();\n break;\n case \"streaming\":\n this.stop();\n break;\n }\n }\n\n // MICROPHONE DATA\n #assertValidBitDepth() {\n _console.assertEnumWithError(this.bitDepth!, MicrophoneBitDepths);\n }\n #fadeDuration = 0.001;\n #playbackTime = 0;\n #parseMicrophoneData(dataView: DataView) {\n this.#assertValidBitDepth();\n\n _console.log(\"parsing microphone data\", dataView);\n\n const numberOfSamples = dataView.byteLength / this.#bytesPerSample!;\n const samples = new Float32Array(numberOfSamples);\n\n for (let i = 0; i < numberOfSamples; i++) {\n let sample;\n switch (this.bitDepth) {\n case \"16\":\n sample = dataView.getInt16(i * 2, true);\n samples[i] = sample / 2 ** 15; // Normalize to [-1, 1]\n break;\n case \"8\":\n sample = dataView.getInt8(i);\n samples[i] = sample / 2 ** 7; // Normalize to [-1, 1]\n break;\n }\n }\n\n _console.log(\"samples\", samples);\n\n if (this.#isRecording && this.#microphoneRecordingData) {\n this.#microphoneRecordingData!.push(samples);\n }\n\n if (this.#audioContext) {\n if (this.#gainNode) {\n const audioBuffer = this.#audioContext.createBuffer(\n 1,\n samples.length,\n Number(this.sampleRate!)\n );\n audioBuffer.getChannelData(0).set(samples);\n\n const bufferSource = this.#audioContext.createBufferSource();\n bufferSource.buffer = audioBuffer;\n\n const channelData = audioBuffer.getChannelData(0);\n const sampleRate = Number(this.sampleRate!);\n\n for (let i = 0; i < this.#fadeDuration * sampleRate; i++) {\n channelData[i] *= i / (this.#fadeDuration * sampleRate);\n }\n\n for (\n let i = channelData.length - 1;\n i >= channelData.length - this.#fadeDuration * sampleRate;\n i--\n ) {\n channelData[i] *=\n (channelData.length - i) / (this.#fadeDuration * sampleRate);\n }\n\n bufferSource.connect(this.#gainNode!);\n\n if (this.#playbackTime < this.#audioContext.currentTime) {\n this.#playbackTime = this.#audioContext.currentTime;\n }\n bufferSource.start(this.#playbackTime);\n this.#playbackTime += audioBuffer.duration;\n }\n }\n\n this.#dispatchEvent(\"microphoneData\", {\n samples,\n sampleRate: this.sampleRate!,\n bitDepth: this.bitDepth!,\n });\n }\n get #bytesPerSample() {\n switch (this.bitDepth) {\n case \"8\":\n return 1;\n case \"16\":\n return 2;\n }\n }\n\n // CONFIG\n #microphoneConfiguration: MicrophoneConfiguration = {};\n get microphoneConfiguration() {\n return this.#microphoneConfiguration;\n }\n #availableMicrophoneConfigurationTypes!: MicrophoneConfigurationType[];\n get availableMicrophoneConfigurationTypes() {\n return this.#availableMicrophoneConfigurationTypes;\n }\n\n get bitDepth() {\n return this.#microphoneConfiguration.bitDepth;\n }\n get sampleRate() {\n return this.#microphoneConfiguration.sampleRate;\n }\n\n #parseMicrophoneConfiguration(dataView: DataView) {\n const parsedMicrophoneConfiguration: MicrophoneConfiguration = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const microphoneConfigurationTypeIndex = dataView.getUint8(byteOffset++);\n const microphoneConfigurationType =\n MicrophoneConfigurationTypes[microphoneConfigurationTypeIndex];\n _console.assertWithError(\n microphoneConfigurationType,\n `invalid microphoneConfigurationTypeIndex ${microphoneConfigurationTypeIndex}`\n );\n let rawValue = dataView.getUint8(byteOffset++);\n const values = MicrophoneConfigurationValues[microphoneConfigurationType];\n const value = values[rawValue];\n _console.assertEnumWithError(value, values);\n _console.log({ microphoneConfigurationType, value });\n // @ts-expect-error\n parsedMicrophoneConfiguration[microphoneConfigurationType] = value;\n }\n\n _console.log({ parsedMicrophoneConfiguration });\n this.#availableMicrophoneConfigurationTypes = Object.keys(\n parsedMicrophoneConfiguration\n ) as MicrophoneConfigurationType[];\n this.#microphoneConfiguration = parsedMicrophoneConfiguration;\n this.#dispatchEvent(\"getMicrophoneConfiguration\", {\n microphoneConfiguration: this.#microphoneConfiguration,\n });\n }\n\n #isMicrophoneConfigurationRedundant(\n microphoneConfiguration: MicrophoneConfiguration\n ) {\n let microphoneConfigurationTypes = Object.keys(\n microphoneConfiguration\n ) as MicrophoneConfigurationType[];\n return microphoneConfigurationTypes.every((microphoneConfigurationType) => {\n return (\n this.microphoneConfiguration[microphoneConfigurationType] ==\n microphoneConfiguration[microphoneConfigurationType]\n );\n });\n }\n async setMicrophoneConfiguration(\n newMicrophoneConfiguration: MicrophoneConfiguration\n ) {\n _console.log({ newMicrophoneConfiguration });\n if (this.#isMicrophoneConfigurationRedundant(newMicrophoneConfiguration)) {\n _console.log(\"redundant microphone configuration\");\n return;\n }\n const setMicrophoneConfigurationData = this.#createData(\n newMicrophoneConfiguration\n );\n _console.log({ setMicrophoneConfigurationData });\n\n const promise = this.waitForEvent(\"getMicrophoneConfiguration\");\n this.sendMessage([\n {\n type: \"setMicrophoneConfiguration\",\n data: setMicrophoneConfigurationData.buffer,\n },\n ]);\n await promise;\n }\n\n #assertAvailableMicrophoneConfigurationType(\n microphoneConfigurationType: MicrophoneConfigurationType\n ) {\n _console.assertWithError(\n this.#availableMicrophoneConfigurationTypes,\n \"must get initial microphoneConfiguration\"\n );\n const isMicrophoneConfigurationTypeAvailable =\n this.#availableMicrophoneConfigurationTypes?.includes(\n microphoneConfigurationType\n );\n _console.assertWithError(\n isMicrophoneConfigurationTypeAvailable,\n `unavailable microphone configuration type \"${microphoneConfigurationType}\"`\n );\n return isMicrophoneConfigurationTypeAvailable;\n }\n\n static AssertValidMicrophoneConfigurationType(\n microphoneConfigurationType: MicrophoneConfigurationType\n ) {\n _console.assertEnumWithError(\n microphoneConfigurationType,\n MicrophoneConfigurationTypes\n );\n }\n static AssertValidMicrophoneConfigurationTypeEnum(\n microphoneConfigurationTypeEnum: number\n ) {\n _console.assertTypeWithError(microphoneConfigurationTypeEnum, \"number\");\n _console.assertWithError(\n microphoneConfigurationTypeEnum in MicrophoneConfigurationTypes,\n `invalid microphoneConfigurationTypeEnum ${microphoneConfigurationTypeEnum}`\n );\n }\n\n #createData(microphoneConfiguration: MicrophoneConfiguration) {\n let microphoneConfigurationTypes = Object.keys(\n microphoneConfiguration\n ) as MicrophoneConfigurationType[];\n microphoneConfigurationTypes = microphoneConfigurationTypes.filter(\n (microphoneConfigurationType) =>\n this.#assertAvailableMicrophoneConfigurationType(\n microphoneConfigurationType\n )\n );\n\n const dataView = new DataView(\n new ArrayBuffer(microphoneConfigurationTypes.length * 2)\n );\n microphoneConfigurationTypes.forEach(\n (microphoneConfigurationType, index) => {\n MicrophoneManager.AssertValidMicrophoneConfigurationType(\n microphoneConfigurationType\n );\n const microphoneConfigurationTypeEnum =\n MicrophoneConfigurationTypes.indexOf(microphoneConfigurationType);\n dataView.setUint8(index * 2, microphoneConfigurationTypeEnum);\n\n let value = microphoneConfiguration[microphoneConfigurationType]!;\n if (typeof value == \"number\") {\n // @ts-ignore\n value = value.toString();\n }\n const values =\n MicrophoneConfigurationValues[microphoneConfigurationType];\n _console.assertEnumWithError(value, values);\n // @ts-expect-error\n const rawValue = values.indexOf(value);\n dataView.setUint8(index * 2 + 1, rawValue);\n }\n );\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // MESSAGE\n parseMessage(messageType: MicrophoneMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"microphoneStatus\":\n this.#parseMicrophoneStatus(dataView);\n break;\n case \"getMicrophoneConfiguration\":\n case \"setMicrophoneConfiguration\":\n this.#parseMicrophoneConfiguration(dataView);\n break;\n case \"microphoneData\":\n this.#parseMicrophoneData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n #audioContext?: AudioContext;\n get audioContext() {\n return this.#audioContext;\n }\n set audioContext(newAudioContext) {\n if (this.#audioContext == newAudioContext) {\n _console.log(\"redundant audioContext assignment\", this.#audioContext);\n return;\n }\n\n this.#audioContext = newAudioContext;\n\n _console.log(\"assigned new audioContext\", this.#audioContext);\n if (this.#audioContext) {\n this.#playbackTime = this.#audioContext.currentTime;\n } else {\n if (this.#mediaStreamDestination) {\n this.#mediaStreamDestination.disconnect();\n this.#mediaStreamDestination = undefined;\n }\n if (this.#gainNode) {\n this.#gainNode.disconnect();\n this.#gainNode = undefined;\n }\n }\n }\n\n #gainNode?: GainNode;\n get gainNode() {\n _console.assertWithError(\n this.#audioContext,\n \"audioContext assignment required for gainNode\"\n );\n if (!this.#gainNode) {\n _console.log(\"creating gainNode...\");\n this.#gainNode = this.#audioContext!.createGain();\n _console.log(\"created gainNode\", this.#gainNode);\n }\n return this.#gainNode;\n }\n\n #mediaStreamDestination?: MediaStreamAudioDestinationNode;\n get mediaStreamDestination() {\n _console.assertWithError(\n this.#audioContext,\n \"audioContext assignment required for mediaStreamDestination\"\n );\n if (!this.#mediaStreamDestination) {\n _console.log(\"creating mediaStreamDestination...\");\n this.#mediaStreamDestination =\n this.#audioContext!.createMediaStreamDestination();\n this.gainNode?.connect(this.#mediaStreamDestination);\n _console.log(\n \"created mediaStreamDestination\",\n this.#mediaStreamDestination\n );\n }\n return this.#mediaStreamDestination;\n }\n\n #isRecording = false;\n get isRecording() {\n return this.#isRecording;\n }\n #microphoneRecordingData?: Float32Array[];\n startRecording() {\n if (this.isRecording) {\n _console.log(\"already recording\");\n return;\n }\n this.#microphoneRecordingData = [];\n this.#isRecording = true;\n this.#dispatchEvent(\"isRecordingMicrophone\", {\n isRecordingMicrophone: this.isRecording,\n });\n }\n stopRecording() {\n if (!this.isRecording) {\n _console.log(\"already not recording\");\n return;\n }\n this.#isRecording = false;\n if (\n this.#microphoneRecordingData &&\n this.#microphoneRecordingData.length > 0\n ) {\n _console.log(\n \"parsing microphone data...\",\n this.#microphoneRecordingData.length\n );\n const arrayBuffer = concatenateArrayBuffers(\n ...this.#microphoneRecordingData\n );\n const samples = new Float32Array(arrayBuffer);\n\n const blob = float32ArrayToWav(samples, Number(this.sampleRate)!, 1);\n const url = URL.createObjectURL(blob);\n this.#dispatchEvent(\"microphoneRecording\", {\n samples,\n sampleRate: this.sampleRate!,\n bitDepth: this.bitDepth!,\n blob,\n url,\n });\n }\n this.#microphoneRecordingData = undefined;\n this.#dispatchEvent(\"isRecordingMicrophone\", {\n isRecordingMicrophone: this.isRecording,\n });\n }\n toggleRecording() {\n if (this.#isRecording) {\n this.stopRecording();\n } else {\n this.startRecording();\n }\n }\n\n clear() {\n // @ts-ignore\n this.#microphoneStatus = undefined;\n this.#microphoneConfiguration = {};\n if (this.isRecording) {\n this.stopRecording();\n }\n }\n}\n\nexport default MicrophoneManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport { parseTimestamp } from \"../utils/MathUtils.ts\";\nimport PressureSensorDataManager, {\n PressureDataEventMessages,\n} from \"./PressureSensorDataManager.ts\";\nimport MotionSensorDataManager, {\n MotionSensorDataEventMessages,\n} from \"./MotionSensorDataManager.ts\";\nimport BarometerSensorDataManager, {\n BarometerSensorDataEventMessages,\n} from \"./BarometerSensorDataManager.ts\";\nimport { parseMessage } from \"../utils/ParseUtils.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport {\n MotionSensorTypes,\n ContinuousMotionTypes,\n} from \"./MotionSensorDataManager.ts\";\nimport {\n PressureSensorTypes,\n ContinuousPressureSensorTypes,\n} from \"./PressureSensorDataManager.ts\";\nimport {\n BarometerSensorTypes,\n ContinuousBarometerSensorTypes,\n} from \"./BarometerSensorDataManager.ts\";\nimport Device from \"../Device.ts\";\nimport {\n AddKeysAsPropertyToInterface,\n ExtendInterfaceValues,\n ValueOf,\n} from \"../utils/TypeScriptUtils.ts\";\nimport { CameraSensorTypes } from \"../CameraManager.ts\";\nimport { MicrophoneSensorTypes } from \"../MicrophoneManager.ts\";\n\nconst _console = createConsole(\"SensorDataManager\", { log: false });\n\nexport const SensorTypes = [\n ...PressureSensorTypes,\n ...MotionSensorTypes,\n ...BarometerSensorTypes,\n ...CameraSensorTypes,\n ...MicrophoneSensorTypes,\n] as const;\nexport type SensorType = (typeof SensorTypes)[number];\n\nexport const ContinuousSensorTypes = [\n ...ContinuousPressureSensorTypes,\n ...ContinuousMotionTypes,\n ...ContinuousBarometerSensorTypes,\n] as const;\nexport type ContinuousSensorType = (typeof ContinuousSensorTypes)[number];\n\nexport const SensorDataMessageTypes = [\n \"getPressurePositions\",\n \"getSensorScalars\",\n \"sensorData\",\n] as const;\nexport type SensorDataMessageType = (typeof SensorDataMessageTypes)[number];\n\nexport const RequiredPressureMessageTypes: SensorDataMessageType[] = [\n \"getPressurePositions\",\n] as const;\n\nexport const SensorDataEventTypes = [\n ...SensorDataMessageTypes,\n ...SensorTypes,\n] as const;\nexport type SensorDataEventType = (typeof SensorDataEventTypes)[number];\n\ninterface BaseSensorDataEventMessage {\n timestamp: number;\n}\n\ntype BaseSensorDataEventMessages = BarometerSensorDataEventMessages &\n MotionSensorDataEventMessages &\n PressureDataEventMessages;\ntype _SensorDataEventMessages = ExtendInterfaceValues<\n AddKeysAsPropertyToInterface<BaseSensorDataEventMessages, \"sensorType\">,\n BaseSensorDataEventMessage\n>;\nexport type SensorDataEventMessage = ValueOf<_SensorDataEventMessages>;\ninterface AnySensorDataEventMessages {\n sensorData: SensorDataEventMessage;\n}\nexport type SensorDataEventMessages = _SensorDataEventMessages &\n AnySensorDataEventMessages;\n\nexport type SensorDataEventDispatcher = EventDispatcher<\n Device,\n SensorDataEventType,\n SensorDataEventMessages\n>;\n\nclass SensorDataManager {\n pressureSensorDataManager = new PressureSensorDataManager();\n motionSensorDataManager = new MotionSensorDataManager();\n barometerSensorDataManager = new BarometerSensorDataManager();\n\n #scalars: Map<SensorType, number> = new Map();\n\n static AssertValidSensorType(sensorType: SensorType) {\n _console.assertEnumWithError(sensorType, SensorTypes);\n }\n static AssertValidSensorTypeEnum(sensorTypeEnum: number) {\n _console.assertTypeWithError(sensorTypeEnum, \"number\");\n _console.assertWithError(\n sensorTypeEnum in SensorTypes,\n `invalid sensorTypeEnum ${sensorTypeEnum}`\n );\n }\n\n eventDispatcher!: SensorDataEventDispatcher;\n get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n parseMessage(messageType: SensorDataMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getSensorScalars\":\n this.parseScalars(dataView);\n break;\n case \"getPressurePositions\":\n this.pressureSensorDataManager.parsePositions(dataView);\n break;\n case \"sensorData\":\n this.parseData(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n parseScalars(dataView: DataView) {\n for (\n let byteOffset = 0;\n byteOffset < dataView.byteLength;\n byteOffset += 5\n ) {\n const sensorTypeIndex = dataView.getUint8(byteOffset);\n const sensorType = SensorTypes[sensorTypeIndex];\n if (!sensorType) {\n _console.warn(`unknown sensorType index ${sensorTypeIndex}`);\n continue;\n }\n const sensorScalar = dataView.getFloat32(byteOffset + 1, true);\n _console.log({ sensorType, sensorScalar });\n this.#scalars.set(sensorType, sensorScalar);\n }\n }\n\n private parseData(dataView: DataView) {\n _console.log(\"sensorData\", Array.from(new Uint8Array(dataView.buffer)));\n\n let byteOffset = 0;\n const timestamp = parseTimestamp(dataView, byteOffset);\n byteOffset += 2;\n\n const _dataView = new DataView(dataView.buffer, byteOffset);\n\n parseMessage(_dataView, SensorTypes, this.parseDataCallback.bind(this), {\n timestamp,\n });\n }\n\n private parseDataCallback(\n sensorType: SensorType,\n dataView: DataView,\n { timestamp }: { timestamp: number }\n ) {\n const scalar = this.#scalars.get(sensorType) || 1;\n\n let sensorData = null;\n switch (sensorType) {\n case \"pressure\":\n sensorData = this.pressureSensorDataManager.parseData(dataView, scalar);\n break;\n case \"acceleration\":\n case \"gravity\":\n case \"linearAcceleration\":\n case \"gyroscope\":\n case \"magnetometer\":\n sensorData = this.motionSensorDataManager.parseVector3(\n dataView,\n scalar\n );\n break;\n case \"gameRotation\":\n case \"rotation\":\n sensorData = this.motionSensorDataManager.parseQuaternion(\n dataView,\n scalar\n );\n break;\n case \"orientation\":\n sensorData = this.motionSensorDataManager.parseEuler(dataView, scalar);\n break;\n case \"stepCounter\":\n sensorData = this.motionSensorDataManager.parseStepCounter(dataView);\n break;\n case \"stepDetector\":\n sensorData = {};\n break;\n case \"activity\":\n sensorData = this.motionSensorDataManager.parseActivity(dataView);\n break;\n case \"deviceOrientation\":\n sensorData =\n this.motionSensorDataManager.parseDeviceOrientation(dataView);\n break;\n case \"tapDetector\":\n sensorData = {};\n break;\n case \"barometer\":\n sensorData = this.barometerSensorDataManager.parseData(\n dataView,\n scalar\n );\n break;\n case \"camera\":\n // we parse camera data using CameraManager\n return;\n case \"microphone\":\n // we parse microphone data using MicrophoneManager\n return;\n default:\n _console.error(`uncaught sensorType \"${sensorType}\"`);\n }\n\n _console.assertWithError(\n sensorData != null,\n `no sensorData defined for sensorType \"${sensorType}\"`\n );\n\n _console.log({ sensorType, sensorData });\n // @ts-expect-error\n this.dispatchEvent(sensorType, {\n sensorType,\n [sensorType]: sensorData,\n timestamp,\n });\n // @ts-expect-error\n this.dispatchEvent(\"sensorData\", {\n sensorType,\n [sensorType]: sensorData,\n timestamp,\n });\n }\n}\n\nexport default SensorDataManager;\n","// Gets all non-builtin properties up the prototype chain.\nconst getAllProperties = object => {\n\tconst properties = new Set();\n\n\tdo {\n\t\tfor (const key of Reflect.ownKeys(object)) {\n\t\t\tproperties.add([object, key]);\n\t\t}\n\t} while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n\n\treturn properties;\n};\n\nexport default function autoBind(self, {include, exclude} = {}) {\n\tconst filter = key => {\n\t\tconst match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);\n\n\t\tif (include) {\n\t\t\treturn include.some(match); // eslint-disable-line unicorn/no-array-callback-reference\n\t\t}\n\n\t\tif (exclude) {\n\t\t\treturn !exclude.some(match); // eslint-disable-line unicorn/no-array-callback-reference\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tfor (const [object, key] of getAllProperties(self.constructor.prototype)) {\n\t\tif (key === 'constructor' || !filter(key)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n\t\tif (descriptor && typeof descriptor.value === 'function') {\n\t\t\tself[key] = self[key].bind(self);\n\t\t}\n\t}\n\n\treturn self;\n}\n","import { createConsole } from \"../utils/Console.ts\";\nimport SensorDataManager, {\n SensorTypes,\n SensorType,\n} from \"./SensorDataManager.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport Device, { SendMessageCallback } from \"../Device.ts\";\nimport autoBind from \"../../node_modules/auto-bind/index.js\";\n\nconst _console = createConsole(\"SensorConfigurationManager\", { log: false });\n\nexport type SensorConfiguration = { [sensorType in SensorType]?: number };\n\nexport const MaxSensorRate = 2 ** 16 - 1;\nexport const SensorRateStep = 5;\n\nexport const SensorConfigurationMessageTypes = [\n \"getSensorConfiguration\",\n \"setSensorConfiguration\",\n] as const;\nexport type SensorConfigurationMessageType =\n (typeof SensorConfigurationMessageTypes)[number];\n\nexport const SensorConfigurationEventTypes = SensorConfigurationMessageTypes;\nexport type SensorConfigurationEventType =\n (typeof SensorConfigurationEventTypes)[number];\n\nexport interface SensorConfigurationEventMessages {\n getSensorConfiguration: { sensorConfiguration: SensorConfiguration };\n}\n\nexport type SensorConfigurationEventDispatcher = EventDispatcher<\n Device,\n SensorConfigurationEventType,\n SensorConfigurationEventMessages\n>;\n\nexport type SendSensorConfigurationMessageCallback =\n SendMessageCallback<SensorConfigurationMessageType>;\n\nclass SensorConfigurationManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendSensorConfigurationMessageCallback;\n\n eventDispatcher!: SensorConfigurationEventDispatcher;\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #availableSensorTypes!: SensorType[];\n #assertAvailableSensorType(sensorType: SensorType) {\n _console.assertWithError(\n this.#availableSensorTypes,\n \"must get initial sensorConfiguration\"\n );\n const isSensorTypeAvailable =\n this.#availableSensorTypes?.includes(sensorType);\n _console.log(\n isSensorTypeAvailable,\n `unavailable sensor type \"${sensorType}\"`\n );\n return isSensorTypeAvailable;\n }\n\n #configuration: SensorConfiguration = {};\n get configuration() {\n return this.#configuration;\n }\n\n #updateConfiguration(updatedConfiguration: SensorConfiguration) {\n this.#configuration = updatedConfiguration;\n _console.log({ updatedConfiguration: this.#configuration });\n this.#dispatchEvent(\"getSensorConfiguration\", {\n sensorConfiguration: this.configuration,\n });\n }\n\n clear() {\n this.#updateConfiguration({});\n }\n\n #isRedundant(sensorConfiguration: SensorConfiguration) {\n let sensorTypes = Object.keys(sensorConfiguration) as SensorType[];\n return sensorTypes.every((sensorType) => {\n return this.configuration[sensorType] == sensorConfiguration[sensorType];\n });\n }\n\n async setConfiguration(\n newSensorConfiguration: SensorConfiguration,\n clearRest?: boolean,\n sendImmediately?: boolean\n ) {\n if (clearRest) {\n newSensorConfiguration = Object.assign(\n structuredClone(this.zeroSensorConfiguration),\n newSensorConfiguration\n );\n }\n _console.log({ newSensorConfiguration });\n if (this.#isRedundant(newSensorConfiguration)) {\n _console.log(\"redundant sensor configuration\");\n return;\n }\n const setSensorConfigurationData = this.#createData(newSensorConfiguration);\n _console.log({ setSensorConfigurationData });\n\n const promise = this.waitForEvent(\"getSensorConfiguration\");\n this.sendMessage(\n [\n {\n type: \"setSensorConfiguration\",\n data: setSensorConfigurationData.buffer,\n },\n ],\n sendImmediately\n );\n await promise;\n }\n\n #parse(dataView: DataView) {\n const parsedSensorConfiguration: SensorConfiguration = {};\n for (\n let byteOffset = 0;\n byteOffset < dataView.byteLength;\n byteOffset += 3\n ) {\n const sensorTypeIndex = dataView.getUint8(byteOffset);\n const sensorType = SensorTypes[sensorTypeIndex];\n\n const sensorRate = dataView.getUint16(byteOffset + 1, true);\n _console.log({ sensorType, sensorRate });\n\n if (!sensorType) {\n _console.warn(`unknown sensorType index ${sensorTypeIndex}`);\n continue;\n }\n parsedSensorConfiguration[sensorType] = sensorRate;\n }\n _console.log({ parsedSensorConfiguration });\n this.#availableSensorTypes = Object.keys(\n parsedSensorConfiguration\n ) as SensorType[];\n return parsedSensorConfiguration;\n }\n\n static #AssertValidSensorRate(sensorRate: number) {\n _console.assertTypeWithError(sensorRate, \"number\");\n _console.assertWithError(\n sensorRate >= 0,\n `sensorRate must be 0 or greater (got ${sensorRate})`\n );\n _console.assertWithError(\n sensorRate < MaxSensorRate,\n `sensorRate must be 0 or greater (got ${sensorRate})`\n );\n _console.assertWithError(\n sensorRate % SensorRateStep == 0,\n `sensorRate must be multiple of ${SensorRateStep}`\n );\n }\n\n #assertValidSensorRate(sensorRate: number) {\n SensorConfigurationManager.#AssertValidSensorRate(sensorRate);\n }\n\n #createData(sensorConfiguration: SensorConfiguration) {\n let sensorTypes = Object.keys(sensorConfiguration) as SensorType[];\n sensorTypes = sensorTypes.filter((sensorType) =>\n this.#assertAvailableSensorType(sensorType)\n );\n\n const dataView = new DataView(new ArrayBuffer(sensorTypes.length * 3));\n sensorTypes.forEach((sensorType, index) => {\n SensorDataManager.AssertValidSensorType(sensorType);\n const sensorTypeEnum = SensorTypes.indexOf(sensorType);\n dataView.setUint8(index * 3, sensorTypeEnum);\n\n const sensorRate = sensorConfiguration[sensorType]!;\n this.#assertValidSensorRate(sensorRate);\n dataView.setUint16(index * 3 + 1, sensorRate, true);\n });\n _console.log({ sensorConfigurationData: dataView });\n return dataView;\n }\n\n // ZERO\n static #ZeroSensorConfiguration: SensorConfiguration = {};\n static get ZeroSensorConfiguration() {\n return this.#ZeroSensorConfiguration;\n }\n static {\n SensorTypes.forEach((sensorType) => {\n this.#ZeroSensorConfiguration[sensorType] = 0;\n });\n }\n get zeroSensorConfiguration() {\n const zeroSensorConfiguration: SensorConfiguration = {};\n this.#availableSensorTypes.forEach((sensorType) => {\n zeroSensorConfiguration[sensorType] = 0;\n });\n return zeroSensorConfiguration;\n }\n async clearSensorConfiguration() {\n return this.setConfiguration(this.zeroSensorConfiguration);\n }\n\n // MESSAGE\n parseMessage(\n messageType: SensorConfigurationMessageType,\n dataView: DataView\n ) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getSensorConfiguration\":\n case \"setSensorConfiguration\":\n const newSensorConfiguration = this.#parse(dataView);\n this.#updateConfiguration(newSensorConfiguration);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default SensorConfigurationManager;\n","import { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport SensorDataManager, { SensorTypes } from \"./sensor/SensorDataManager.ts\";\nimport { arrayWithoutDuplicates } from \"./utils/ArrayUtils.ts\";\nimport { SensorRateStep } from \"./sensor/SensorConfigurationManager.ts\";\nimport { parseTimestamp } from \"./utils/MathUtils.ts\";\nimport { SensorType } from \"./sensor/SensorDataManager.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport autoBind from \"auto-bind\";\nimport { FileConfiguration as BaseFileConfiguration } from \"./FileTransferManager.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\n\nconst _console = createConsole(\"TfliteManager\", { log: false });\n\nexport const TfliteMessageTypes = [\n \"getTfliteName\",\n \"setTfliteName\",\n \"getTfliteTask\",\n \"setTfliteTask\",\n \"getTfliteSampleRate\",\n \"setTfliteSampleRate\",\n \"getTfliteSensorTypes\",\n \"setTfliteSensorTypes\",\n \"tfliteIsReady\",\n \"getTfliteCaptureDelay\",\n \"setTfliteCaptureDelay\",\n \"getTfliteThreshold\",\n \"setTfliteThreshold\",\n \"getTfliteInferencingEnabled\",\n \"setTfliteInferencingEnabled\",\n \"tfliteInference\",\n] as const;\nexport type TfliteMessageType = (typeof TfliteMessageTypes)[number];\n\nexport const TfliteEventTypes = TfliteMessageTypes;\nexport type TfliteEventType = (typeof TfliteEventTypes)[number];\n\nexport const RequiredTfliteMessageTypes: TfliteMessageType[] = [\n \"getTfliteName\",\n \"getTfliteTask\",\n \"getTfliteSampleRate\",\n \"getTfliteSensorTypes\",\n \"tfliteIsReady\",\n \"getTfliteCaptureDelay\",\n \"getTfliteThreshold\",\n \"getTfliteInferencingEnabled\",\n];\n\nexport const TfliteTasks = [\"classification\", \"regression\"] as const;\nexport type TfliteTask = (typeof TfliteTasks)[number];\n\nexport interface TfliteEventMessages {\n getTfliteName: { tfliteName: string };\n getTfliteTask: { tfliteTask: TfliteTask };\n getTfliteSampleRate: { tfliteSampleRate: number };\n getTfliteSensorTypes: { tfliteSensorTypes: SensorType[] };\n tfliteIsReady: { tfliteIsReady: boolean };\n getTfliteCaptureDelay: { tfliteCaptureDelay: number };\n getTfliteThreshold: { tfliteThreshold: number };\n getTfliteInferencingEnabled: { tfliteInferencingEnabled: boolean };\n tfliteInference: { tfliteInference: TfliteInference };\n}\n\nexport interface TfliteInference {\n timestamp: number;\n values: number[];\n maxValue?: number;\n maxIndex?: number;\n maxClass?: string;\n classValues?: { [key: string]: number };\n}\n\nexport type TfliteEventDispatcher = EventDispatcher<\n Device,\n TfliteEventType,\n TfliteEventMessages\n>;\nexport type SendTfliteMessageCallback = SendMessageCallback<TfliteMessageType>;\n\nexport const TfliteSensorTypes = [\n \"pressure\",\n \"linearAcceleration\",\n \"gyroscope\",\n \"magnetometer\",\n] as const satisfies readonly SensorType[];\nexport type TfliteSensorType = (typeof TfliteSensorTypes)[number];\n\nexport interface TfliteFileConfiguration extends BaseFileConfiguration {\n type: \"tflite\";\n name: string;\n sensorTypes: TfliteSensorType[];\n task: TfliteTask;\n sampleRate: number;\n captureDelay?: number;\n threshold?: number;\n classes?: string[];\n}\n\nclass TfliteManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendTfliteMessageCallback;\n\n #assertValidTask(task: TfliteTask) {\n _console.assertEnumWithError(task, TfliteTasks);\n }\n #assertValidTaskEnum(taskEnum: number) {\n _console.assertWithError(\n taskEnum in TfliteTasks,\n `invalid taskEnum ${taskEnum}`\n );\n }\n\n eventDispatcher!: TfliteEventDispatcher;\n get addEventListenter() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // PROPERTIES\n\n #name!: string;\n get name() {\n return this.#name;\n }\n #parseName(dataView: DataView) {\n _console.log(\"parseName\", dataView);\n const name = textDecoder.decode(dataView.buffer);\n this.#updateName(name);\n }\n #updateName(name: string) {\n _console.log({ name });\n this.#name = name;\n this.#dispatchEvent(\"getTfliteName\", { tfliteName: name });\n }\n async setName(newName: string, sendImmediately?: boolean) {\n _console.assertTypeWithError(newName, \"string\");\n if (this.name == newName) {\n _console.log(`redundant name assignment ${newName}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteName\");\n\n const setNameData = textEncoder.encode(newName);\n this.sendMessage(\n [{ type: \"setTfliteName\", data: setNameData.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #task!: TfliteTask;\n get task() {\n return this.#task;\n }\n #parseTask(dataView: DataView) {\n _console.log(\"parseTask\", dataView);\n const taskEnum = dataView.getUint8(0);\n this.#assertValidTaskEnum(taskEnum);\n const task = TfliteTasks[taskEnum];\n this.#updateTask(task);\n }\n #updateTask(task: TfliteTask) {\n _console.log({ task });\n this.#task = task;\n this.#dispatchEvent(\"getTfliteTask\", { tfliteTask: task });\n }\n async setTask(newTask: TfliteTask, sendImmediately?: boolean) {\n this.#assertValidTask(newTask);\n if (this.task == newTask) {\n _console.log(`redundant task assignment ${newTask}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteTask\");\n\n const taskEnum = TfliteTasks.indexOf(newTask);\n this.sendMessage(\n [{ type: \"setTfliteTask\", data: UInt8ByteBuffer(taskEnum) }],\n sendImmediately\n );\n\n await promise;\n }\n\n #sampleRate!: number;\n get sampleRate() {\n return this.#sampleRate;\n }\n #parseSampleRate(dataView: DataView) {\n _console.log(\"parseSampleRate\", dataView);\n const sampleRate = dataView.getUint16(0, true);\n this.#updateSampleRate(sampleRate);\n }\n #updateSampleRate(sampleRate: number) {\n _console.log({ sampleRate });\n this.#sampleRate = sampleRate;\n this.#dispatchEvent(\"getTfliteSampleRate\", {\n tfliteSampleRate: sampleRate,\n });\n }\n async setSampleRate(newSampleRate: number, sendImmediately?: boolean) {\n _console.assertTypeWithError(newSampleRate, \"number\");\n newSampleRate -= newSampleRate % SensorRateStep;\n _console.assertWithError(\n newSampleRate >= SensorRateStep,\n `sampleRate must be multiple of ${SensorRateStep} greater than 0 (got ${newSampleRate})`\n );\n if (this.#sampleRate == newSampleRate) {\n _console.log(`redundant sampleRate assignment ${newSampleRate}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteSampleRate\");\n\n const dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, newSampleRate, true);\n this.sendMessage(\n [{ type: \"setTfliteSampleRate\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n static AssertValidSensorType(sensorType: SensorType) {\n SensorDataManager.AssertValidSensorType(sensorType);\n const tfliteSensorType = sensorType as TfliteSensorType;\n _console.assertWithError(\n TfliteSensorTypes.includes(tfliteSensorType),\n `invalid tflite sensorType \"${sensorType}\"`\n );\n }\n\n #sensorTypes: TfliteSensorType[] = [];\n get sensorTypes() {\n return this.#sensorTypes.slice();\n }\n #parseSensorTypes(dataView: DataView) {\n _console.log(\"parseSensorTypes\", dataView);\n const sensorTypes: TfliteSensorType[] = [];\n for (let index = 0; index < dataView.byteLength; index++) {\n const sensorTypeEnum = dataView.getUint8(index);\n const sensorType = SensorTypes[sensorTypeEnum] as TfliteSensorType;\n if (sensorType) {\n if (TfliteSensorTypes.includes(sensorType)) {\n sensorTypes.push(sensorType);\n } else {\n _console.error(`invalid tfliteSensorType ${sensorType}`);\n }\n } else {\n _console.error(`invalid sensorTypeEnum ${sensorTypeEnum}`);\n }\n }\n this.#updateSensorTypes(sensorTypes);\n }\n #updateSensorTypes(sensorTypes: TfliteSensorType[]) {\n _console.log({ sensorTypes });\n this.#sensorTypes = sensorTypes;\n this.#dispatchEvent(\"getTfliteSensorTypes\", {\n tfliteSensorTypes: sensorTypes,\n });\n }\n async setSensorTypes(\n newSensorTypes: SensorType[],\n sendImmediately?: boolean\n ) {\n newSensorTypes.forEach((sensorType) => {\n TfliteManager.AssertValidSensorType(sensorType);\n });\n\n const promise = this.waitForEvent(\"getTfliteSensorTypes\");\n\n newSensorTypes = arrayWithoutDuplicates(newSensorTypes);\n const newSensorTypeEnums = newSensorTypes\n .map((sensorType) => SensorTypes.indexOf(sensorType))\n .sort();\n _console.log(newSensorTypes, newSensorTypeEnums);\n this.sendMessage(\n [\n {\n type: \"setTfliteSensorTypes\",\n data: Uint8Array.from(newSensorTypeEnums).buffer,\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n\n #isReady!: boolean;\n get isReady() {\n return this.#isReady;\n }\n #parseIsReady(dataView: DataView) {\n _console.log(\"parseIsReady\", dataView);\n const isReady = Boolean(dataView.getUint8(0));\n this.#updateIsReady(isReady);\n }\n #updateIsReady(isReady: boolean) {\n _console.log({ isReady });\n this.#isReady = isReady;\n this.#dispatchEvent(\"tfliteIsReady\", { tfliteIsReady: isReady });\n }\n #assertIsReady() {\n _console.assertWithError(this.isReady, `tflite is not ready`);\n }\n\n #captureDelay!: number;\n get captureDelay() {\n return this.#captureDelay;\n }\n #parseCaptureDelay(dataView: DataView) {\n _console.log(\"parseCaptureDelay\", dataView);\n const captureDelay = dataView.getUint16(0, true);\n this.#updateCaptueDelay(captureDelay);\n }\n #updateCaptueDelay(captureDelay: number) {\n _console.log({ captureDelay });\n this.#captureDelay = captureDelay;\n this.#dispatchEvent(\"getTfliteCaptureDelay\", {\n tfliteCaptureDelay: captureDelay,\n });\n }\n async setCaptureDelay(newCaptureDelay: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newCaptureDelay, \"number\");\n if (this.#captureDelay == newCaptureDelay) {\n _console.log(`redundant captureDelay assignment ${newCaptureDelay}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteCaptureDelay\");\n\n const dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, newCaptureDelay, true);\n this.sendMessage(\n [{ type: \"setTfliteCaptureDelay\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #threshold!: number;\n get threshold() {\n return this.#threshold;\n }\n #parseThreshold(dataView: DataView) {\n _console.log(\"parseThreshold\", dataView);\n const threshold = dataView.getFloat32(0, true);\n this.#updateThreshold(threshold);\n }\n #updateThreshold(threshold: number) {\n _console.log({ threshold });\n this.#threshold = threshold;\n this.#dispatchEvent(\"getTfliteThreshold\", { tfliteThreshold: threshold });\n }\n async setThreshold(newThreshold: number, sendImmediately: boolean) {\n _console.assertTypeWithError(newThreshold, \"number\");\n _console.assertWithError(\n newThreshold >= 0,\n `threshold must be positive (got ${newThreshold})`\n );\n if (this.#threshold == newThreshold) {\n _console.log(`redundant threshold assignment ${newThreshold}`);\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteThreshold\");\n\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setFloat32(0, newThreshold, true);\n this.sendMessage(\n [{ type: \"setTfliteThreshold\", data: dataView.buffer }],\n sendImmediately\n );\n\n await promise;\n }\n\n #inferencingEnabled!: boolean;\n get inferencingEnabled() {\n return this.#inferencingEnabled;\n }\n #parseInferencingEnabled(dataView: DataView) {\n _console.log(\"parseInferencingEnabled\", dataView);\n const inferencingEnabled = Boolean(dataView.getUint8(0));\n this.#updateInferencingEnabled(inferencingEnabled);\n }\n #updateInferencingEnabled(inferencingEnabled: boolean) {\n _console.log({ inferencingEnabled });\n this.#inferencingEnabled = inferencingEnabled;\n this.#dispatchEvent(\"getTfliteInferencingEnabled\", {\n tfliteInferencingEnabled: inferencingEnabled,\n });\n }\n async setInferencingEnabled(\n newInferencingEnabled: boolean,\n sendImmediately: boolean = true\n ) {\n _console.assertTypeWithError(newInferencingEnabled, \"boolean\");\n if (!newInferencingEnabled && !this.isReady) {\n return;\n }\n this.#assertIsReady();\n if (this.#inferencingEnabled == newInferencingEnabled) {\n _console.log(\n `redundant inferencingEnabled assignment ${newInferencingEnabled}`\n );\n return;\n }\n\n const promise = this.waitForEvent(\"getTfliteInferencingEnabled\");\n\n this.sendMessage(\n [\n {\n type: \"setTfliteInferencingEnabled\",\n\n data: UInt8ByteBuffer(Number(newInferencingEnabled)),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n async toggleInferencingEnabled() {\n return this.setInferencingEnabled(!this.inferencingEnabled);\n }\n\n async enableInferencing() {\n if (this.inferencingEnabled) {\n return;\n }\n this.setInferencingEnabled(true);\n }\n async disableInferencing() {\n if (!this.inferencingEnabled) {\n return;\n }\n this.setInferencingEnabled(false);\n }\n\n #parseInference(dataView: DataView) {\n _console.log(\"parseInference\", dataView);\n\n const timestamp = parseTimestamp(dataView, 0);\n _console.log({ timestamp });\n\n const values: number[] = [];\n for (\n let index = 0, byteOffset = 2;\n byteOffset < dataView.byteLength;\n index++, byteOffset += 4\n ) {\n const value = dataView.getFloat32(byteOffset, true);\n values.push(value);\n }\n _console.log(\"values\", values);\n\n const inference: TfliteInference = {\n timestamp,\n values,\n };\n\n if (this.task == \"classification\") {\n let maxValue = 0;\n let maxIndex = 0;\n values.forEach((value, index) => {\n if (value > maxValue) {\n maxValue = value;\n maxIndex = index;\n }\n });\n _console.log({ maxIndex, maxValue });\n inference.maxIndex = maxIndex;\n inference.maxValue = maxValue;\n if (this.#configuration?.classes) {\n const { classes } = this.#configuration;\n inference.maxClass = classes[maxIndex];\n inference.classValues = {};\n values.forEach((value, index) => {\n const key = classes[index];\n inference.classValues![key] = value;\n });\n }\n }\n\n this.#dispatchEvent(\"tfliteInference\", { tfliteInference: inference });\n }\n\n parseMessage(messageType: TfliteMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getTfliteName\":\n case \"setTfliteName\":\n this.#parseName(dataView);\n break;\n case \"getTfliteTask\":\n case \"setTfliteTask\":\n this.#parseTask(dataView);\n break;\n case \"getTfliteSampleRate\":\n case \"setTfliteSampleRate\":\n this.#parseSampleRate(dataView);\n break;\n case \"getTfliteSensorTypes\":\n case \"setTfliteSensorTypes\":\n this.#parseSensorTypes(dataView);\n break;\n case \"tfliteIsReady\":\n this.#parseIsReady(dataView);\n break;\n case \"getTfliteCaptureDelay\":\n case \"setTfliteCaptureDelay\":\n this.#parseCaptureDelay(dataView);\n break;\n case \"getTfliteThreshold\":\n case \"setTfliteThreshold\":\n this.#parseThreshold(dataView);\n break;\n case \"getTfliteInferencingEnabled\":\n case \"setTfliteInferencingEnabled\":\n this.#parseInferencingEnabled(dataView);\n break;\n case \"tfliteInference\":\n this.#parseInference(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n #configuration?: TfliteFileConfiguration;\n get configuration() {\n return this.#configuration;\n }\n sendConfiguration(\n configuration: TfliteFileConfiguration,\n sendImmediately?: boolean\n ) {\n if (configuration == this.#configuration) {\n _console.log(\"redundant tflite configuration assignment\");\n return;\n }\n this.#configuration = configuration;\n _console.log(\"assigned new tflite configuration\", this.configuration);\n if (!this.configuration) {\n return;\n }\n const { name, task, captureDelay, sampleRate, threshold, sensorTypes } =\n this.configuration;\n this.setName(name, false);\n this.setTask(task, false);\n if (captureDelay != undefined) {\n this.setCaptureDelay(captureDelay, false);\n }\n this.setSampleRate(sampleRate, false);\n if (threshold != undefined) {\n this.setThreshold(threshold, false);\n }\n this.setSensorTypes(sensorTypes, sendImmediately);\n }\n\n clear() {\n this.#configuration = undefined;\n this.#inferencingEnabled = false;\n this.#sensorTypes = [];\n this.#sampleRate = 0;\n this.#isReady = false;\n // @ts-expect-error\n this.#name = undefined;\n // @ts-expect-error\n this.#task = undefined;\n // @ts-expect-error\n this.#sampleRate = undefined;\n this.#sensorTypes.length = 0;\n // @ts-expect-error\n this.#isReady = undefined;\n // @ts-expect-error\n this.#captureDelay = undefined;\n // @ts-expect-error\n this.#threshold = undefined;\n // @ts-expect-error\n this.#inferencingEnabled = undefined;\n this.#configuration = undefined;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required tflite information\");\n const messages = RequiredTfliteMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n}\n\nexport default TfliteManager;\n","import Device from \"./Device.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder } from \"./utils/Text.ts\";\n\nconst _console = createConsole(\"DeviceInformationManager\", { log: false });\n\nexport interface PnpId {\n source: \"Bluetooth\" | \"USB\";\n vendorId: number;\n productId: number;\n productVersion: number;\n}\n\nexport interface DeviceInformation {\n manufacturerName: string;\n modelNumber: string;\n softwareRevision: string;\n hardwareRevision: string;\n firmwareRevision: string;\n pnpId: PnpId;\n serialNumber: string;\n}\n\nexport const DeviceInformationTypes = [\n \"manufacturerName\",\n \"modelNumber\",\n \"hardwareRevision\",\n \"firmwareRevision\",\n \"softwareRevision\",\n \"pnpId\",\n \"serialNumber\",\n] as const;\nexport type DeviceInformationType = (typeof DeviceInformationTypes)[number];\n\nexport const DeviceInformationEventTypes = [\n ...DeviceInformationTypes,\n \"deviceInformation\",\n] as const;\nexport type DeviceInformationEventType =\n (typeof DeviceInformationEventTypes)[number];\n\nexport interface DeviceInformationEventMessages {\n manufacturerName: { manufacturerName: string };\n modelNumber: { modelNumber: string };\n softwareRevision: { softwareRevision: string };\n hardwareRevision: { hardwareRevision: string };\n firmwareRevision: { firmwareRevision: string };\n pnpId: { pnpId: PnpId };\n serialNumber: { serialNumber: string };\n deviceInformation: { deviceInformation: DeviceInformation };\n}\n\nexport type DeviceInformationEventDispatcher = EventDispatcher<\n Device,\n DeviceInformationEventType,\n DeviceInformationEventMessages\n>;\n\nclass DeviceInformationManager {\n eventDispatcher!: DeviceInformationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n #information: Partial<DeviceInformation> = {};\n get information() {\n return this.#information as DeviceInformation;\n }\n clear() {\n this.#information = {};\n }\n get #isComplete() {\n return DeviceInformationTypes.filter((key) => key != \"serialNumber\").every(\n (key) => key in this.#information\n );\n }\n\n #update(partialDeviceInformation: Partial<DeviceInformation>) {\n _console.log({ partialDeviceInformation });\n const deviceInformationNames = Object.keys(\n partialDeviceInformation\n ) as (keyof DeviceInformation)[];\n deviceInformationNames.forEach((deviceInformationName) => {\n // @ts-expect-error\n this.#dispatchEvent(deviceInformationName, {\n [deviceInformationName]:\n partialDeviceInformation[deviceInformationName],\n });\n });\n\n Object.assign(this.#information, partialDeviceInformation);\n _console.log({ deviceInformation: this.#information });\n if (this.#isComplete) {\n _console.log(\"completed deviceInformation\");\n this.#dispatchEvent(\"deviceInformation\", {\n deviceInformation: this.information,\n });\n }\n }\n\n parseMessage(messageType: DeviceInformationType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"manufacturerName\":\n const manufacturerName = textDecoder.decode(dataView.buffer);\n _console.log({ manufacturerName });\n this.#update({ manufacturerName });\n break;\n case \"modelNumber\":\n const modelNumber = textDecoder.decode(dataView.buffer);\n _console.log({ modelNumber });\n this.#update({ modelNumber });\n break;\n case \"softwareRevision\":\n const softwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ softwareRevision });\n this.#update({ softwareRevision });\n break;\n case \"hardwareRevision\":\n const hardwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ hardwareRevision });\n this.#update({ hardwareRevision });\n break;\n case \"firmwareRevision\":\n const firmwareRevision = textDecoder.decode(dataView.buffer);\n _console.log({ firmwareRevision });\n this.#update({ firmwareRevision });\n break;\n case \"pnpId\":\n const pnpId: PnpId = {\n source: dataView.getUint8(0) === 1 ? \"Bluetooth\" : \"USB\",\n productId: dataView.getUint16(3, true),\n productVersion: dataView.getUint16(5, true),\n vendorId: 0,\n };\n if (pnpId.source == \"Bluetooth\") {\n pnpId.vendorId = dataView.getUint16(1, true);\n } else {\n // no need to implement\n }\n _console.log({ pnpId });\n this.#update({ pnpId });\n break;\n case \"serialNumber\":\n const serialNumber = textDecoder.decode(dataView.buffer);\n _console.log({ serialNumber });\n // will only be used for node\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default DeviceInformationManager;\n","import { ConnectionType } from \"./connection/BaseConnectionManager.ts\";\nimport Device, { SendMessageCallback } from \"./Device.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { Uint16Max } from \"./utils/MathUtils.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"InformationManager\", { log: false });\n\nexport const DeviceTypes = [\n \"leftInsole\",\n \"rightInsole\",\n \"leftGlove\",\n \"rightGlove\",\n \"glasses\",\n \"generic\",\n] as const;\nexport type DeviceType = (typeof DeviceTypes)[number];\n\nexport const Sides = [\"left\", \"right\"] as const;\nexport type Side = (typeof Sides)[number];\n\nexport const MinNameLength = 2;\nexport const MaxNameLength = 30;\n\nexport const InformationMessageTypes = [\n \"isCharging\",\n \"getBatteryCurrent\",\n \"getMtu\",\n \"getId\",\n \"getName\",\n \"setName\",\n \"getType\",\n \"setType\",\n \"getCurrentTime\",\n \"setCurrentTime\",\n] as const;\nexport type InformationMessageType = (typeof InformationMessageTypes)[number];\n\nexport const InformationEventTypes = InformationMessageTypes;\nexport type InformationEventType = (typeof InformationEventTypes)[number];\n\nexport interface InformationEventMessages {\n isCharging: { isCharging: boolean };\n getBatteryCurrent: { batteryCurrent: number };\n getMtu: { mtu: number };\n getId: { id: string };\n getName: { name: string };\n getType: { type: DeviceType };\n getCurrentTime: { currentTime: number };\n}\n\nexport type InformationEventDispatcher = EventDispatcher<\n Device,\n InformationEventType,\n InformationEventMessages\n>;\nexport type SendInformationMessageCallback =\n SendMessageCallback<InformationMessageType>;\n\nclass InformationManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendInformationMessageCallback;\n\n eventDispatcher!: InformationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // PROPERTIES\n\n #isCharging = false;\n get isCharging() {\n return this.#isCharging;\n }\n #updateIsCharging(updatedIsCharging: boolean) {\n _console.assertTypeWithError(updatedIsCharging, \"boolean\");\n this.#isCharging = updatedIsCharging;\n _console.log({ isCharging: this.#isCharging });\n this.#dispatchEvent(\"isCharging\", { isCharging: this.#isCharging });\n }\n\n #batteryCurrent!: number;\n get batteryCurrent() {\n return this.#batteryCurrent;\n }\n async getBatteryCurrent() {\n _console.log(\"getting battery current...\");\n const promise = this.waitForEvent(\"getBatteryCurrent\");\n this.sendMessage([{ type: \"getBatteryCurrent\" }]);\n await promise;\n }\n #updateBatteryCurrent(updatedBatteryCurrent: number) {\n _console.assertTypeWithError(updatedBatteryCurrent, \"number\");\n this.#batteryCurrent = updatedBatteryCurrent;\n _console.log({ batteryCurrent: this.#batteryCurrent });\n this.#dispatchEvent(\"getBatteryCurrent\", {\n batteryCurrent: this.#batteryCurrent,\n });\n }\n\n #id!: string;\n get id() {\n return this.#id;\n }\n #updateId(updatedId: string) {\n _console.assertTypeWithError(updatedId, \"string\");\n this.#id = updatedId;\n _console.log({ id: this.#id });\n this.#dispatchEvent(\"getId\", { id: this.#id });\n }\n\n #name = \"\";\n get name() {\n return this.#name;\n }\n\n updateName(updatedName: string) {\n _console.assertTypeWithError(updatedName, \"string\");\n this.#name = updatedName;\n _console.log({ updatedName: this.#name });\n this.#dispatchEvent(\"getName\", { name: this.#name });\n }\n async setName(newName: string) {\n _console.assertTypeWithError(newName, \"string\");\n _console.assertRangeWithError(\n \"newName\",\n newName.length,\n MinNameLength,\n MaxNameLength\n );\n const setNameData = textEncoder.encode(newName);\n _console.log({ setNameData });\n\n const promise = this.waitForEvent(\"getName\");\n this.sendMessage([{ type: \"setName\", data: setNameData.buffer }]);\n await promise;\n }\n\n // TYPE\n #type!: DeviceType;\n get type() {\n return this.#type;\n }\n get typeEnum() {\n return DeviceTypes.indexOf(this.type);\n }\n #assertValidDeviceType(type: DeviceType) {\n _console.assertEnumWithError(type, DeviceTypes);\n }\n #assertValidDeviceTypeEnum(typeEnum: number) {\n _console.assertTypeWithError(typeEnum, \"number\");\n _console.assertWithError(\n typeEnum in DeviceTypes,\n `invalid typeEnum ${typeEnum}`\n );\n }\n updateType(updatedType: DeviceType) {\n this.#assertValidDeviceType(updatedType);\n // if (updatedType == this.type) {\n // _console.log(\"redundant type assignment\");\n // return;\n // }\n this.#type = updatedType;\n _console.log({ updatedType: this.#type });\n\n this.#dispatchEvent(\"getType\", { type: this.#type });\n }\n async #setTypeEnum(newTypeEnum: number) {\n this.#assertValidDeviceTypeEnum(newTypeEnum);\n\n const setTypeData = UInt8ByteBuffer(newTypeEnum);\n _console.log({ setTypeData });\n const promise = this.waitForEvent(\"getType\");\n this.sendMessage([{ type: \"setType\", data: setTypeData }]);\n await promise;\n }\n async setType(newType: DeviceType) {\n this.#assertValidDeviceType(newType);\n const newTypeEnum = DeviceTypes.indexOf(newType);\n this.#setTypeEnum(newTypeEnum);\n }\n\n get isInsole() {\n switch (this.type) {\n case \"leftInsole\":\n case \"rightInsole\":\n return true;\n default:\n return false;\n }\n }\n\n get isGlove() {\n switch (this.type) {\n case \"leftGlove\":\n case \"rightGlove\":\n return true;\n default:\n return false;\n }\n }\n\n get side(): Side {\n switch (this.type) {\n case \"leftInsole\":\n case \"leftGlove\":\n return \"left\";\n case \"rightInsole\":\n case \"rightGlove\":\n return \"right\";\n default:\n return \"left\";\n }\n }\n\n #mtu = 0;\n get mtu() {\n return this.#mtu;\n }\n #updateMtu(newMtu: number) {\n _console.assertTypeWithError(newMtu, \"number\");\n if (this.#mtu == newMtu) {\n _console.log(\"redundant mtu assignment\", newMtu);\n return;\n }\n this.#mtu = newMtu;\n\n this.#dispatchEvent(\"getMtu\", { mtu: this.#mtu });\n }\n\n #isCurrentTimeSet = false;\n get isCurrentTimeSet() {\n return this.#isCurrentTimeSet;\n }\n\n #onCurrentTime(currentTime: number) {\n _console.log({ currentTime });\n this.#isCurrentTimeSet =\n currentTime != 0 || Math.abs(Date.now() - currentTime) < Uint16Max;\n if (!this.#isCurrentTimeSet) {\n this.#setCurrentTime(false);\n }\n }\n async #setCurrentTime(sendImmediately?: boolean) {\n _console.log(\"setting current time...\");\n const dataView = new DataView(new ArrayBuffer(8));\n dataView.setBigUint64(0, BigInt(Date.now()), true);\n const promise = this.waitForEvent(\"getCurrentTime\");\n this.sendMessage(\n [{ type: \"setCurrentTime\", data: dataView.buffer }],\n sendImmediately\n );\n await promise;\n }\n\n // MESSAGE\n parseMessage(messageType: InformationMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"isCharging\":\n const isCharging = Boolean(dataView.getUint8(0));\n _console.log({ isCharging });\n this.#updateIsCharging(isCharging);\n break;\n case \"getBatteryCurrent\":\n const batteryCurrent = dataView.getFloat32(0, true);\n _console.log({ batteryCurrent });\n this.#updateBatteryCurrent(batteryCurrent);\n break;\n case \"getId\":\n const id = textDecoder.decode(dataView.buffer);\n _console.log({ id });\n this.#updateId(id);\n break;\n case \"getName\":\n case \"setName\":\n const name = textDecoder.decode(dataView.buffer);\n _console.log({ name });\n this.updateName(name);\n break;\n case \"getType\":\n case \"setType\":\n const typeEnum = dataView.getUint8(0);\n const type = DeviceTypes[typeEnum];\n _console.log({ typeEnum, type });\n this.updateType(type);\n break;\n case \"getMtu\":\n let mtu = dataView.getUint16(0, true);\n if (\n this.connectionType != \"webSocket\" &&\n this.connectionType != \"udp\"\n ) {\n mtu = Math.min(mtu, 512);\n }\n _console.log({ mtu });\n this.#updateMtu(mtu);\n break;\n case \"getCurrentTime\":\n case \"setCurrentTime\":\n const currentTime = Number(dataView.getBigUint64(0, true));\n this.#onCurrentTime(currentTime);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n this.#isCurrentTimeSet = false;\n this.#mtu = 0;\n }\n\n connectionType?: ConnectionType;\n}\n\nexport default InformationManager;\n","export const VibrationWaveformEffects = [\n \"none\",\n \"strongClick100\",\n \"strongClick60\",\n \"strongClick30\",\n \"sharpClick100\",\n \"sharpClick60\",\n \"sharpClick30\",\n \"softBump100\",\n \"softBump60\",\n \"softBump30\",\n \"doubleClick100\",\n \"doubleClick60\",\n \"tripleClick100\",\n \"softFuzz60\",\n \"strongBuzz100\",\n \"alert750ms\",\n \"alert1000ms\",\n \"strongClick1_100\",\n \"strongClick2_80\",\n \"strongClick3_60\",\n \"strongClick4_30\",\n \"mediumClick100\",\n \"mediumClick80\",\n \"mediumClick60\",\n \"sharpTick100\",\n \"sharpTick80\",\n \"sharpTick60\",\n \"shortDoubleClickStrong100\",\n \"shortDoubleClickStrong80\",\n \"shortDoubleClickStrong60\",\n \"shortDoubleClickStrong30\",\n \"shortDoubleClickMedium100\",\n \"shortDoubleClickMedium80\",\n \"shortDoubleClickMedium60\",\n \"shortDoubleSharpTick100\",\n \"shortDoubleSharpTick80\",\n \"shortDoubleSharpTick60\",\n \"longDoubleSharpClickStrong100\",\n \"longDoubleSharpClickStrong80\",\n \"longDoubleSharpClickStrong60\",\n \"longDoubleSharpClickStrong30\",\n \"longDoubleSharpClickMedium100\",\n \"longDoubleSharpClickMedium80\",\n \"longDoubleSharpClickMedium60\",\n \"longDoubleSharpTick100\",\n \"longDoubleSharpTick80\",\n \"longDoubleSharpTick60\",\n \"buzz100\",\n \"buzz80\",\n \"buzz60\",\n \"buzz40\",\n \"buzz20\",\n \"pulsingStrong100\",\n \"pulsingStrong60\",\n \"pulsingMedium100\",\n \"pulsingMedium60\",\n \"pulsingSharp100\",\n \"pulsingSharp60\",\n \"transitionClick100\",\n \"transitionClick80\",\n \"transitionClick60\",\n \"transitionClick40\",\n \"transitionClick20\",\n \"transitionClick10\",\n \"transitionHum100\",\n \"transitionHum80\",\n \"transitionHum60\",\n \"transitionHum40\",\n \"transitionHum20\",\n \"transitionHum10\",\n \"transitionRampDownLongSmooth2_100\",\n \"transitionRampDownLongSmooth1_100\",\n \"transitionRampDownMediumSmooth1_100\",\n \"transitionRampDownMediumSmooth2_100\",\n \"transitionRampDownShortSmooth1_100\",\n \"transitionRampDownShortSmooth2_100\",\n \"transitionRampDownLongSharp1_100\",\n \"transitionRampDownLongSharp2_100\",\n \"transitionRampDownMediumSharp1_100\",\n \"transitionRampDownMediumSharp2_100\",\n \"transitionRampDownShortSharp1_100\",\n \"transitionRampDownShortSharp2_100\",\n \"transitionRampUpLongSmooth1_100\",\n \"transitionRampUpLongSmooth2_100\",\n \"transitionRampUpMediumSmooth1_100\",\n \"transitionRampUpMediumSmooth2_100\",\n \"transitionRampUpShortSmooth1_100\",\n \"transitionRampUpShortSmooth2_100\",\n \"transitionRampUpLongSharp1_100\",\n \"transitionRampUpLongSharp2_100\",\n \"transitionRampUpMediumSharp1_100\",\n \"transitionRampUpMediumSharp2_100\",\n \"transitionRampUpShortSharp1_100\",\n \"transitionRampUpShortSharp2_100\",\n \"transitionRampDownLongSmooth1_50\",\n \"transitionRampDownLongSmooth2_50\",\n \"transitionRampDownMediumSmooth1_50\",\n \"transitionRampDownMediumSmooth2_50\",\n \"transitionRampDownShortSmooth1_50\",\n \"transitionRampDownShortSmooth2_50\",\n \"transitionRampDownLongSharp1_50\",\n \"transitionRampDownLongSharp2_50\",\n \"transitionRampDownMediumSharp1_50\",\n \"transitionRampDownMediumSharp2_50\",\n \"transitionRampDownShortSharp1_50\",\n \"transitionRampDownShortSharp2_50\",\n \"transitionRampUpLongSmooth1_50\",\n \"transitionRampUpLongSmooth2_50\",\n \"transitionRampUpMediumSmooth1_50\",\n \"transitionRampUpMediumSmooth2_50\",\n \"transitionRampUpShortSmooth1_50\",\n \"transitionRampUpShortSmooth2_50\",\n \"transitionRampUpLongSharp1_50\",\n \"transitionRampUpLongSharp2_50\",\n \"transitionRampUpMediumSharp1_50\",\n \"transitionRampUpMediumSharp2_50\",\n \"transitionRampUpShortSharp1_50\",\n \"transitionRampUpShortSharp2_50\",\n \"longBuzz100\",\n \"smoothHum50\",\n \"smoothHum40\",\n \"smoothHum30\",\n \"smoothHum20\",\n \"smoothHum10\",\n] as const;\n\nexport type VibrationWaveformEffect = (typeof VibrationWaveformEffects)[number];\n","import { createConsole } from \"../utils/Console.ts\";\nimport {\n VibrationWaveformEffect,\n VibrationWaveformEffects,\n} from \"./VibrationWaveformEffects.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport Device, { SendMessageCallback } from \"../Device.ts\";\nimport autoBind from \"auto-bind\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\n\nconst _console = createConsole(\"VibrationManager\", { log: false });\n\nexport const VibrationLocations = [\"front\", \"rear\"] as const;\nexport type VibrationLocation = (typeof VibrationLocations)[number];\n\nexport const VibrationTypes = [\"waveformEffect\", \"waveform\"] as const;\nexport type VibrationType = (typeof VibrationTypes)[number];\n\nexport interface VibrationWaveformEffectSegment {\n effect?: VibrationWaveformEffect;\n delay?: number;\n loopCount?: number;\n}\n\nexport interface VibrationWaveformSegment {\n duration: number;\n amplitude: number;\n}\n\nexport const VibrationMessageTypes = [\n \"getVibrationLocations\",\n \"triggerVibration\",\n] as const;\nexport type VibrationMessageType = (typeof VibrationMessageTypes)[number];\n\nexport const VibrationEventTypes = VibrationMessageTypes;\nexport type VibrationEventType = (typeof VibrationEventTypes)[number];\n\nexport interface VibrationEventMessages {\n getVibrationLocations: { vibrationLocations: VibrationLocation[] };\n}\n\nexport const MaxNumberOfVibrationWaveformEffectSegments = 8;\nexport const MaxVibrationWaveformSegmentDuration = 2550;\nexport const MaxVibrationWaveformEffectSegmentDelay = 1270;\nexport const MaxVibrationWaveformEffectSegmentLoopCount = 3;\nexport const MaxNumberOfVibrationWaveformSegments = 20;\nexport const MaxVibrationWaveformEffectSequenceLoopCount = 6;\n\ninterface BaseVibrationConfiguration {\n type: VibrationType;\n locations?: VibrationLocation[];\n}\n\nexport interface VibrationWaveformEffectConfiguration\n extends BaseVibrationConfiguration {\n type: \"waveformEffect\";\n segments: VibrationWaveformEffectSegment[];\n loopCount?: number;\n}\n\nexport interface VibrationWaveformConfiguration\n extends BaseVibrationConfiguration {\n type: \"waveform\";\n segments: VibrationWaveformSegment[];\n}\n\nexport type VibrationConfiguration =\n | VibrationWaveformEffectConfiguration\n | VibrationWaveformConfiguration;\n\nexport type SendVibrationMessageCallback =\n SendMessageCallback<VibrationMessageType>;\n\nexport type VibrationEventDispatcher = EventDispatcher<\n Device,\n VibrationEventType,\n VibrationEventMessages\n>;\n\nclass VibrationManager {\n constructor() {\n autoBind(this);\n }\n sendMessage!: SendVibrationMessageCallback;\n\n eventDispatcher!: VibrationEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n #verifyLocation(location: VibrationLocation) {\n _console.assertTypeWithError(location, \"string\");\n _console.assertWithError(\n VibrationLocations.includes(location),\n `invalid location \"${location}\"`\n );\n }\n #verifyLocations(locations: VibrationLocation[]) {\n this.#assertNonEmptyArray(locations);\n locations.forEach((location) => {\n this.#verifyLocation(location);\n });\n }\n #createLocationsBitmask(locations: VibrationLocation[]) {\n this.#verifyLocations(locations);\n\n let locationsBitmask = 0;\n locations.forEach((location) => {\n const locationIndex = VibrationLocations.indexOf(location);\n locationsBitmask |= 1 << locationIndex;\n });\n _console.log({ locationsBitmask });\n _console.assertWithError(\n locationsBitmask > 0,\n `locationsBitmask must not be zero`\n );\n return locationsBitmask;\n }\n\n #assertNonEmptyArray(array: any[]) {\n _console.assertWithError(Array.isArray(array), \"passed non-array\");\n _console.assertWithError(array.length > 0, \"passed empty array\");\n }\n\n #verifyWaveformEffect(waveformEffect: VibrationWaveformEffect) {\n _console.assertWithError(\n VibrationWaveformEffects.includes(waveformEffect),\n `invalid waveformEffect \"${waveformEffect}\"`\n );\n }\n\n #verifyWaveformEffectSegment(\n waveformEffectSegment: VibrationWaveformEffectSegment\n ) {\n if (waveformEffectSegment.effect != undefined) {\n const waveformEffect = waveformEffectSegment.effect;\n this.#verifyWaveformEffect(waveformEffect);\n } else if (waveformEffectSegment.delay != undefined) {\n const { delay } = waveformEffectSegment;\n _console.assertWithError(\n delay >= 0,\n `delay must be 0ms or greater (got ${delay})`\n );\n _console.assertWithError(\n delay <= MaxVibrationWaveformEffectSegmentDelay,\n `delay must be ${MaxVibrationWaveformEffectSegmentDelay}ms or less (got ${delay})`\n );\n } else {\n throw Error(\"no effect or delay found in waveformEffectSegment\");\n }\n\n if (waveformEffectSegment.loopCount != undefined) {\n const { loopCount } = waveformEffectSegment;\n this.#verifyWaveformEffectSegmentLoopCount(loopCount);\n }\n }\n\n #verifyWaveformEffectSegmentLoopCount(\n waveformEffectSegmentLoopCount: number\n ) {\n _console.assertTypeWithError(waveformEffectSegmentLoopCount, \"number\");\n _console.assertWithError(\n waveformEffectSegmentLoopCount >= 0,\n `waveformEffectSegmentLoopCount must be 0 or greater (got ${waveformEffectSegmentLoopCount})`\n );\n _console.assertWithError(\n waveformEffectSegmentLoopCount <=\n MaxVibrationWaveformEffectSegmentLoopCount,\n `waveformEffectSegmentLoopCount must be ${MaxVibrationWaveformEffectSegmentLoopCount} or fewer (got ${waveformEffectSegmentLoopCount})`\n );\n }\n\n #verifyWaveformEffectSegments(\n waveformEffectSegments: VibrationWaveformEffectSegment[]\n ) {\n this.#assertNonEmptyArray(waveformEffectSegments);\n _console.assertWithError(\n waveformEffectSegments.length <=\n MaxNumberOfVibrationWaveformEffectSegments,\n `must have ${MaxNumberOfVibrationWaveformEffectSegments} waveformEffectSegments or fewer (got ${waveformEffectSegments.length})`\n );\n waveformEffectSegments.forEach((waveformEffectSegment) => {\n this.#verifyWaveformEffectSegment(waveformEffectSegment);\n });\n }\n\n #verifyWaveformEffectSequenceLoopCount(\n waveformEffectSequenceLoopCount: number\n ) {\n _console.assertTypeWithError(waveformEffectSequenceLoopCount, \"number\");\n _console.assertWithError(\n waveformEffectSequenceLoopCount >= 0,\n `waveformEffectSequenceLoopCount must be 0 or greater (got ${waveformEffectSequenceLoopCount})`\n );\n _console.assertWithError(\n waveformEffectSequenceLoopCount <=\n MaxVibrationWaveformEffectSequenceLoopCount,\n `waveformEffectSequenceLoopCount must be ${MaxVibrationWaveformEffectSequenceLoopCount} or fewer (got ${waveformEffectSequenceLoopCount})`\n );\n }\n\n #verifyWaveformSegment(waveformSegment: VibrationWaveformSegment) {\n _console.assertTypeWithError(waveformSegment.amplitude, \"number\");\n _console.assertWithError(\n waveformSegment.amplitude >= 0,\n `amplitude must be 0 or greater (got ${waveformSegment.amplitude})`\n );\n _console.assertWithError(\n waveformSegment.amplitude <= 1,\n `amplitude must be 1 or less (got ${waveformSegment.amplitude})`\n );\n\n _console.assertTypeWithError(waveformSegment.duration, \"number\");\n _console.assertWithError(\n waveformSegment.duration > 0,\n `duration must be greater than 0ms (got ${waveformSegment.duration}ms)`\n );\n _console.assertWithError(\n waveformSegment.duration <= MaxVibrationWaveformSegmentDuration,\n `duration must be ${MaxVibrationWaveformSegmentDuration}ms or less (got ${waveformSegment.duration}ms)`\n );\n }\n\n #verifyWaveformSegments(waveformSegments: VibrationWaveformSegment[]) {\n this.#assertNonEmptyArray(waveformSegments);\n _console.assertWithError(\n waveformSegments.length <= MaxNumberOfVibrationWaveformSegments,\n `must have ${MaxNumberOfVibrationWaveformSegments} waveformSegments or fewer (got ${waveformSegments.length})`\n );\n waveformSegments.forEach((waveformSegment) => {\n this.#verifyWaveformSegment(waveformSegment);\n });\n }\n\n #createWaveformEffectsData(\n locations: VibrationLocation[],\n waveformEffectSegments: VibrationWaveformEffectSegment[],\n waveformEffectSequenceLoopCount: number = 0\n ) {\n this.#verifyWaveformEffectSegments(waveformEffectSegments);\n this.#verifyWaveformEffectSequenceLoopCount(\n waveformEffectSequenceLoopCount\n );\n\n let dataArray = [];\n let byteOffset = 0;\n\n const hasAtLeast1WaveformEffectWithANonzeroLoopCount =\n waveformEffectSegments.some((waveformEffectSegment) => {\n const { loopCount } = waveformEffectSegment;\n return loopCount != undefined && loopCount > 0;\n });\n\n const includeAllWaveformEffectSegments =\n hasAtLeast1WaveformEffectWithANonzeroLoopCount ||\n waveformEffectSequenceLoopCount != 0;\n\n for (\n let index = 0;\n index < waveformEffectSegments.length ||\n (includeAllWaveformEffectSegments &&\n index < MaxNumberOfVibrationWaveformEffectSegments);\n index++\n ) {\n const waveformEffectSegment = waveformEffectSegments[index] || {\n effect: \"none\",\n };\n if (waveformEffectSegment.effect != undefined) {\n const waveformEffect = waveformEffectSegment.effect;\n dataArray[byteOffset++] =\n VibrationWaveformEffects.indexOf(waveformEffect);\n } else if (waveformEffectSegment.delay != undefined) {\n const { delay } = waveformEffectSegment;\n dataArray[byteOffset++] = (1 << 7) | Math.floor(delay / 10); // set most significant bit to 1\n } else {\n throw Error(\"invalid waveformEffectSegment\");\n }\n }\n\n const includeAllWaveformEffectSegmentLoopCounts =\n waveformEffectSequenceLoopCount != 0;\n for (\n let index = 0;\n index < waveformEffectSegments.length ||\n (includeAllWaveformEffectSegmentLoopCounts &&\n index < MaxNumberOfVibrationWaveformEffectSegments);\n index++\n ) {\n const waveformEffectSegmentLoopCount =\n waveformEffectSegments[index]?.loopCount || 0;\n if (index == 0 || index == 4) {\n dataArray[byteOffset] = 0;\n }\n const bitOffset = 2 * (index % 4);\n dataArray[byteOffset] |= waveformEffectSegmentLoopCount << bitOffset;\n if (index == 3 || index == 7) {\n byteOffset++;\n }\n }\n\n if (waveformEffectSequenceLoopCount != 0) {\n dataArray[byteOffset++] = waveformEffectSequenceLoopCount;\n }\n const dataView = new DataView(Uint8Array.from(dataArray).buffer);\n _console.log({ dataArray, dataView });\n return this.#createData(locations, \"waveformEffect\", dataView);\n }\n #createWaveformData(\n locations: VibrationLocation[],\n waveformSegments: VibrationWaveformSegment[]\n ) {\n this.#verifyWaveformSegments(waveformSegments);\n const dataView = new DataView(new ArrayBuffer(waveformSegments.length * 2));\n waveformSegments.forEach((waveformSegment, index) => {\n dataView.setUint8(index * 2, Math.floor(waveformSegment.amplitude * 127));\n dataView.setUint8(\n index * 2 + 1,\n Math.floor(waveformSegment.duration / 10)\n );\n });\n _console.log({ dataView });\n return this.#createData(locations, \"waveform\", dataView);\n }\n\n #verifyVibrationType(vibrationType: VibrationType) {\n _console.assertTypeWithError(vibrationType, \"string\");\n _console.assertWithError(\n VibrationTypes.includes(vibrationType),\n `invalid vibrationType \"${vibrationType}\"`\n );\n }\n\n #createData(\n locations: VibrationLocation[],\n vibrationType: VibrationType,\n dataView: DataView\n ) {\n _console.assertWithError(dataView?.byteLength > 0, \"no data received\");\n const locationsBitmask = this.#createLocationsBitmask(locations);\n this.#verifyVibrationType(vibrationType);\n const vibrationTypeIndex = VibrationTypes.indexOf(vibrationType);\n _console.log({ locationsBitmask, vibrationTypeIndex, dataView });\n const data = concatenateArrayBuffers(\n locationsBitmask,\n vibrationTypeIndex,\n dataView.byteLength,\n dataView\n );\n _console.log({ data });\n return data;\n }\n\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately: boolean = true\n ) {\n let triggerVibrationData!: ArrayBuffer;\n vibrationConfigurations.forEach((vibrationConfiguration) => {\n const { type } = vibrationConfiguration;\n\n let { locations } = vibrationConfiguration;\n locations = locations || this.vibrationLocations.slice();\n locations = locations.filter((location) =>\n this.vibrationLocations.includes(location)\n );\n\n let arrayBuffer: ArrayBuffer;\n\n switch (type) {\n case \"waveformEffect\":\n {\n const { segments, loopCount } = vibrationConfiguration;\n arrayBuffer = this.#createWaveformEffectsData(\n locations,\n segments,\n loopCount\n );\n }\n break;\n case \"waveform\":\n {\n const { segments } = vibrationConfiguration;\n arrayBuffer = this.#createWaveformData(locations, segments);\n }\n break;\n default:\n throw Error(`invalid vibration type \"${type}\"`);\n }\n _console.log({ type, arrayBuffer });\n triggerVibrationData = concatenateArrayBuffers(\n triggerVibrationData,\n arrayBuffer\n );\n });\n await this.sendMessage(\n [{ type: \"triggerVibration\", data: triggerVibrationData }],\n sendImmediately\n );\n }\n\n #vibrationLocations: VibrationLocation[] = [];\n get vibrationLocations() {\n return this.#vibrationLocations;\n }\n #onVibrationLocations(vibrationLocations: VibrationLocation[]) {\n this.#vibrationLocations = vibrationLocations;\n _console.log(\"vibrationLocations\", vibrationLocations);\n this.#dispatchEvent(\"getVibrationLocations\", {\n vibrationLocations: this.#vibrationLocations,\n });\n }\n\n // MESSAGE\n parseMessage(messageType: VibrationMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"getVibrationLocations\":\n const vibrationLocations = Array.from(new Uint8Array(dataView.buffer))\n .map((index) => VibrationLocations[index])\n .filter(Boolean);\n this.#onVibrationLocations(vibrationLocations);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n}\n\nexport default VibrationManager;\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport { UInt8ByteBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInNode } from \"./utils/environment.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"WifiManager\", { log: false });\n\nexport const MinWifiSSIDLength = 1;\nexport const MaxWifiSSIDLength = 32;\n\nexport const MinWifiPasswordLength = 8;\nexport const MaxWifiPasswordLength = 64;\n\nexport const WifiMessageTypes = [\n \"isWifiAvailable\",\n \"getWifiSSID\",\n \"setWifiSSID\",\n \"getWifiPassword\",\n \"setWifiPassword\",\n \"getWifiConnectionEnabled\",\n \"setWifiConnectionEnabled\",\n \"isWifiConnected\",\n \"ipAddress\",\n \"isWifiSecure\",\n] as const;\nexport type WifiMessageType = (typeof WifiMessageTypes)[number];\n\nexport const RequiredWifiMessageTypes: WifiMessageType[] = [\n \"getWifiSSID\",\n \"getWifiPassword\",\n \"getWifiConnectionEnabled\",\n \"isWifiConnected\",\n \"ipAddress\",\n \"isWifiSecure\",\n] as const;\n\nexport const WifiEventTypes = WifiMessageTypes;\nexport type WifiEventType = (typeof WifiEventTypes)[number];\n\nexport interface WifiEventMessages {\n isWifiAvailable: { isWifiAvailable: boolean };\n getWifiSSID: { wifiSSID: string };\n getWifiPassword: { wifiPassword: string };\n getEnableWifiConnection: { wifiConnectionEnabled: boolean };\n isWifiConnected: { isWifiConnected: boolean };\n ipAddress: { ipAddress?: string };\n}\n\nexport type WifiEventDispatcher = EventDispatcher<\n Device,\n WifiEventType,\n WifiEventMessages\n>;\nexport type SendWifiMessageCallback = SendMessageCallback<WifiMessageType>;\n\nclass WifiManager {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendWifiMessageCallback;\n\n eventDispatcher!: WifiEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required wifi information\");\n const messages = RequiredWifiMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // PROPERTIES\n\n #isWifiAvailable = false;\n get isWifiAvailable() {\n return this.#isWifiAvailable;\n }\n #updateIsWifiAvailable(updatedIsWifiAvailable: boolean) {\n _console.assertTypeWithError(updatedIsWifiAvailable, \"boolean\");\n this.#isWifiAvailable = updatedIsWifiAvailable;\n _console.log({ isWifiAvailable: this.#isWifiAvailable });\n this.#dispatchEvent(\"isWifiAvailable\", {\n isWifiAvailable: this.#isWifiAvailable,\n });\n }\n\n #assertWifiIsAvailable() {\n _console.assertWithError(this.#isWifiAvailable, \"wifi is not available\");\n }\n\n // WIFI SSID\n #wifiSSID = \"\";\n get wifiSSID() {\n return this.#wifiSSID;\n }\n\n #updateWifiSSID(updatedWifiSSID: string) {\n _console.assertTypeWithError(updatedWifiSSID, \"string\");\n this.#wifiSSID = updatedWifiSSID;\n _console.log({ wifiSSID: this.#wifiSSID });\n this.#dispatchEvent(\"getWifiSSID\", { wifiSSID: this.#wifiSSID });\n }\n async setWifiSSID(newWifiSSID: string) {\n this.#assertWifiIsAvailable();\n if (this.#wifiConnectionEnabled) {\n _console.error(\"cannot change ssid while wifi connection is enabled\");\n return;\n }\n _console.assertTypeWithError(newWifiSSID, \"string\");\n _console.assertRangeWithError(\n \"wifiSSID\",\n newWifiSSID.length,\n MinWifiSSIDLength,\n MaxWifiSSIDLength\n );\n\n const setWifiSSIDData = textEncoder.encode(newWifiSSID);\n _console.log({ setWifiSSIDData });\n\n const promise = this.waitForEvent(\"getWifiSSID\");\n this.sendMessage([{ type: \"setWifiSSID\", data: setWifiSSIDData.buffer }]);\n await promise;\n }\n\n // WIFI PASSWORD\n #wifiPassword = \"\";\n get wifiPassword() {\n return this.#wifiPassword;\n }\n\n #updateWifiPassword(updatedWifiPassword: string) {\n _console.assertTypeWithError(updatedWifiPassword, \"string\");\n this.#wifiPassword = updatedWifiPassword;\n _console.log({ wifiPassword: this.#wifiPassword });\n this.#dispatchEvent(\"getWifiPassword\", {\n wifiPassword: this.#wifiPassword,\n });\n }\n async setWifiPassword(newWifiPassword: string) {\n this.#assertWifiIsAvailable();\n if (this.#wifiConnectionEnabled) {\n _console.error(\"cannot change password while wifi connection is enabled\");\n return;\n }\n _console.assertTypeWithError(newWifiPassword, \"string\");\n if (newWifiPassword.length > 0) {\n _console.assertRangeWithError(\n \"wifiPassword\",\n newWifiPassword.length,\n MinWifiPasswordLength,\n MaxWifiPasswordLength\n );\n }\n\n const setWifiPasswordData = textEncoder.encode(newWifiPassword);\n _console.log({ setWifiPasswordData });\n\n const promise = this.waitForEvent(\"getWifiPassword\");\n this.sendMessage([\n { type: \"setWifiPassword\", data: setWifiPasswordData.buffer },\n ]);\n await promise;\n }\n\n // ENABLE WIFI CONNECTION\n #wifiConnectionEnabled!: boolean;\n get wifiConnectionEnabled() {\n return this.#wifiConnectionEnabled;\n }\n #updateWifiConnectionEnabled(wifiConnectionEnabled: boolean) {\n _console.log({ wifiConnectionEnabled });\n this.#wifiConnectionEnabled = wifiConnectionEnabled;\n this.#dispatchEvent(\"getWifiConnectionEnabled\", {\n wifiConnectionEnabled: wifiConnectionEnabled,\n });\n }\n async setWifiConnectionEnabled(\n newWifiConnectionEnabled: boolean,\n sendImmediately: boolean = true\n ) {\n this.#assertWifiIsAvailable();\n _console.assertTypeWithError(newWifiConnectionEnabled, \"boolean\");\n if (this.#wifiConnectionEnabled == newWifiConnectionEnabled) {\n _console.log(\n `redundant wifiConnectionEnabled assignment ${newWifiConnectionEnabled}`\n );\n return;\n }\n\n const promise = this.waitForEvent(\"getWifiConnectionEnabled\");\n\n this.sendMessage(\n [\n {\n type: \"setWifiConnectionEnabled\",\n\n data: UInt8ByteBuffer(Number(newWifiConnectionEnabled)),\n },\n ],\n sendImmediately\n );\n await promise;\n }\n async toggleWifiConnection() {\n return this.setWifiConnectionEnabled(!this.wifiConnectionEnabled);\n }\n async enableWifiConnection() {\n return this.setWifiConnectionEnabled(true);\n }\n async disableWifiConnection() {\n return this.setWifiConnectionEnabled(false);\n }\n\n // IS WIFI CONNECTED\n #isWifiConnected = false;\n get isWifiConnected() {\n return this.#isWifiConnected;\n }\n #updateIsWifiConnected(updatedIsWifiConnected: boolean) {\n _console.assertTypeWithError(updatedIsWifiConnected, \"boolean\");\n this.#isWifiConnected = updatedIsWifiConnected;\n _console.log({ isWifiConnected: this.#isWifiConnected });\n this.#dispatchEvent(\"isWifiConnected\", {\n isWifiConnected: this.#isWifiConnected,\n });\n }\n\n // IP ADDRESS\n #ipAddress?: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n\n #updateIpAddress(updatedIpAddress?: string) {\n this.#ipAddress = updatedIpAddress;\n _console.log({ ipAddress: this.#ipAddress });\n this.#dispatchEvent(\"ipAddress\", {\n ipAddress: this.#ipAddress,\n });\n }\n\n // IS WIFI SECURE\n #isWifiSecure = false;\n get isWifiSecure() {\n return this.#isWifiSecure;\n }\n #updateIsWifiSecure(updatedIsWifiSecure: boolean) {\n _console.assertTypeWithError(updatedIsWifiSecure, \"boolean\");\n this.#isWifiSecure = updatedIsWifiSecure;\n _console.log({ isWifiSecure: this.#isWifiSecure });\n this.#dispatchEvent(\"isWifiSecure\", {\n isWifiSecure: this.#isWifiSecure,\n });\n }\n\n // MESSAGE\n parseMessage(messageType: WifiMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"isWifiAvailable\":\n const isWifiAvailable = Boolean(dataView.getUint8(0));\n _console.log({ isWifiAvailable });\n this.#updateIsWifiAvailable(isWifiAvailable);\n break;\n case \"getWifiSSID\":\n case \"setWifiSSID\":\n const ssid = textDecoder.decode(dataView.buffer);\n _console.log({ ssid });\n this.#updateWifiSSID(ssid);\n break;\n case \"getWifiPassword\":\n case \"setWifiPassword\":\n const password = textDecoder.decode(dataView.buffer);\n _console.log({ password });\n this.#updateWifiPassword(password);\n break;\n case \"getWifiConnectionEnabled\":\n case \"setWifiConnectionEnabled\":\n const enableWifiConnection = Boolean(dataView.getUint8(0));\n _console.log({ enableWifiConnection });\n this.#updateWifiConnectionEnabled(enableWifiConnection);\n break;\n case \"isWifiConnected\":\n const isWifiConnected = Boolean(dataView.getUint8(0));\n _console.log({ isWifiConnected });\n this.#updateIsWifiConnected(isWifiConnected);\n break;\n case \"ipAddress\":\n let ipAddress: string | undefined = undefined;\n if (dataView.byteLength == 4) {\n ipAddress = new Uint8Array(dataView.buffer.slice(0, 4)).join(\".\");\n }\n _console.log({ ipAddress });\n this.#updateIpAddress(ipAddress);\n break;\n case \"isWifiSecure\":\n const isWifiSecure = Boolean(dataView.getUint8(0));\n _console.log({ isWifiSecure });\n this.#updateIsWifiSecure(isWifiSecure);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n clear() {\n this.#wifiSSID = \"\";\n this.#wifiPassword = \"\";\n this.#ipAddress = \"\";\n this.#isWifiConnected = false;\n this.#isWifiAvailable = false;\n }\n}\n\nexport default WifiManager;\n","import { createConsole } from \"./Console.ts\";\nimport { DisplayColorRGB } from \"./DisplayUtils.ts\";\n\nconst _console = createConsole(\"ColorUtils\", { log: false });\n\nexport function hexToRGB(hex: string): DisplayColorRGB {\n hex = hex.replace(/^#/, \"\");\n\n if (hex.length == 3) {\n hex = hex\n .split(\"\")\n .map((char) => char + char)\n .join(\"\");\n }\n\n _console.assertWithError(\n hex.length == 6,\n `hex length must be 6 (got ${hex.length})`\n );\n\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n\n return { r, g, b };\n}\n\nexport const blackColor: DisplayColorRGB = { r: 0, g: 0, b: 0 };\nexport function colorNameToRGB(colorName: string): DisplayColorRGB {\n const temp = document.createElement(\"div\");\n temp.style.color = colorName;\n document.body.appendChild(temp);\n\n const computedColor = getComputedStyle(temp).color;\n document.body.removeChild(temp);\n\n // Match \"rgb(r, g, b)\" or \"rgba(r, g, b, a)\"\n const match = computedColor.match(/^rgba?\\((\\d+), (\\d+), (\\d+)/);\n if (!match) return blackColor;\n\n return {\n r: parseInt(match[1], 10),\n g: parseInt(match[2], 10),\n b: parseInt(match[3], 10),\n };\n}\n\nexport function stringToRGB(string: string): DisplayColorRGB {\n if (string.startsWith(\"#\")) {\n return hexToRGB(string);\n } else {\n return colorNameToRGB(string);\n }\n}\n\nexport function rgbToHex({ r, g, b }: DisplayColorRGB): string {\n const toHex = (value: number) =>\n value.toString(16).padStart(2, \"0\").toLowerCase();\n\n _console.assertWithError(\n [r, g, b].every((v) => v >= 0 && v <= 255),\n `RGB values must be between 0 and 255 (got r=${r}, g=${g}, b=${b})`\n );\n\n return `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n}\n\nexport function colorDistanceSq(\n a: DisplayColorRGB,\n b: DisplayColorRGB\n): number {\n return (a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2;\n}\n\nexport interface KMeansOptions {\n useInputColors?: boolean; // pick nearest input or average\n maxIterations?: number;\n}\nexport const defaultKMeansOptions: KMeansOptions = {\n useInputColors: true,\n maxIterations: 20,\n};\n\nexport interface KMeansResult {\n palette: string[]; // reduced colors\n mapping: Record<string, number>; // original -> palette index\n}\n\nexport function kMeansColors(\n colors: string[],\n k: number,\n options?: KMeansOptions\n): KMeansResult {\n _console.assertTypeWithError(k, \"number\");\n _console.assertWithError(k > 0, `invalid k ${k}`);\n options = { ...defaultKMeansOptions, ...options };\n const maxIter = options.maxIterations!;\n const useInputColors = options.useInputColors!;\n\n // cache parsed colors\n const colorMap = new Map<string, DisplayColorRGB>();\n for (const c of colors) {\n if (!colorMap.has(c)) {\n colorMap.set(c, stringToRGB(c));\n }\n }\n\n const uniqueColors = Array.from(colorMap.values());\n const uniqueKeys = Array.from(colorMap.keys());\n\n //_console.log({ uniqueColors, uniqueKeys });\n\n if (uniqueColors.length <= k) {\n const mapping: Record<string, number> = {};\n uniqueKeys.forEach((key, idx) => (mapping[key] = idx));\n return { palette: uniqueKeys, mapping };\n }\n\n // Initialize centroids\n let centroids: DisplayColorRGB[] = uniqueColors.slice(0, k);\n\n for (let iter = 0; iter < maxIter; iter++) {\n const clusters: number[][] = Array.from({ length: k }, () => []);\n //_console.log({ clusters, k });\n uniqueColors.forEach((p, idx) => {\n let best = 0;\n let bestDist = Infinity;\n centroids.forEach((c, ci) => {\n const d = colorDistanceSq(p, c);\n if (d < bestDist) {\n bestDist = d;\n best = ci;\n }\n });\n clusters[best].push(idx);\n });\n\n centroids = clusters.map((cluster) => {\n if (cluster.length === 0) return { ...blackColor };\n if (useInputColors) {\n let bestIdx = cluster[0];\n let bestDist = Infinity;\n cluster.forEach((idx) => {\n const d = colorDistanceSq(uniqueColors[idx], centroids[0]);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = idx;\n }\n });\n return uniqueColors[bestIdx];\n } else {\n const sum = cluster.reduce(\n (acc, idx) => {\n const p = uniqueColors[idx];\n return {\n r: acc.r + p.r,\n g: acc.g + p.g,\n b: acc.b + p.b,\n } as DisplayColorRGB;\n },\n { ...blackColor }\n );\n return {\n r: sum.r / cluster.length,\n g: sum.g / cluster.length,\n b: sum.b / cluster.length,\n };\n }\n });\n }\n\n const palette = centroids.map((c) => rgbToHex(c));\n\n // Build mapping: original color -> palette index\n const mapping: Record<string, number> = {};\n for (const [orig, DisplayColorRGB] of colorMap.entries()) {\n let bestIdx = 0;\n let bestDist = Infinity;\n centroids.forEach((c, ci) => {\n const d = colorDistanceSq(c, DisplayColorRGB);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = ci;\n }\n });\n mapping[orig] = bestIdx;\n }\n\n return { palette, mapping };\n}\n\nexport function mapToClosestPaletteIndex(\n colors: string[],\n palette: string[]\n): Record<string, number> {\n const paletteRGB: DisplayColorRGB[] = palette.map(stringToRGB);\n const mapping: Record<string, number> = {};\n\n for (const color of colors) {\n const rgb = stringToRGB(color);\n let bestIdx = 0;\n let bestDist = Infinity;\n\n paletteRGB.forEach((p, idx) => {\n const d = colorDistanceSq(rgb, p);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = idx;\n }\n });\n\n mapping[color] = bestIdx;\n }\n\n return mapping;\n}\n","export const DisplaySegmentCaps = [\"flat\", \"round\"] as const;\nexport type DisplaySegmentCap = (typeof DisplaySegmentCaps)[number];\n\nexport const DisplayAlignments = [\"start\", \"center\", \"end\"] as const;\nexport type DisplayAlignment = (typeof DisplayAlignments)[number];\n\nexport const DisplayAlignmentDirections = [\"horizontal\", \"vertical\"] as const;\nexport type DisplayAlignmentDirection =\n (typeof DisplayAlignmentDirections)[number];\n\nexport const DisplayDirections = [\"right\", \"left\", \"up\", \"down\"] as const;\nexport type DisplayDirection = (typeof DisplayDirections)[number];\n\nexport type DisplayContextState = {\n backgroundColorIndex: number;\n fillColorIndex: number;\n lineColorIndex: number;\n\n ignoreFill: boolean;\n ignoreLine: boolean;\n fillBackground: boolean;\n\n lineWidth: number;\n rotation: number;\n\n horizontalAlignment: DisplayAlignment;\n verticalAlignment: DisplayAlignment;\n\n segmentStartCap: DisplaySegmentCap;\n segmentEndCap: DisplaySegmentCap;\n\n segmentStartRadius: number;\n segmentEndRadius: number;\n\n cropTop: number;\n cropRight: number;\n cropBottom: number;\n cropLeft: number;\n\n rotationCropTop: number;\n rotationCropRight: number;\n rotationCropBottom: number;\n rotationCropLeft: number;\n\n bitmapColorIndices: number[];\n bitmapScaleX: number;\n bitmapScaleY: number;\n\n spriteColorIndices: number[];\n spriteScaleX: number;\n spriteScaleY: number;\n\n spriteSheetName?: string;\n\n spritesLineHeight: number;\n spritesDirection: DisplayDirection;\n spritesLineDirection: DisplayDirection;\n spritesSpacing: number;\n spritesLineSpacing: number;\n spritesAlignment: DisplayAlignment;\n spritesLineAlignment: DisplayAlignment;\n};\nexport type DisplayContextStateKey = keyof DisplayContextState;\nexport type PartialDisplayContextState = Partial<DisplayContextState>;\n\nexport const DefaultDisplayContextState: DisplayContextState = {\n backgroundColorIndex: 0,\n fillColorIndex: 1,\n lineColorIndex: 1,\n\n ignoreFill: false,\n ignoreLine: false,\n fillBackground: false,\n\n lineWidth: 0,\n rotation: 0,\n\n horizontalAlignment: \"center\",\n verticalAlignment: \"center\",\n\n segmentStartCap: \"flat\",\n segmentEndCap: \"flat\",\n\n segmentStartRadius: 1,\n segmentEndRadius: 1,\n\n cropTop: 0,\n cropRight: 0,\n cropBottom: 0,\n cropLeft: 0,\n\n rotationCropTop: 0,\n rotationCropRight: 0,\n rotationCropBottom: 0,\n rotationCropLeft: 0,\n\n bitmapColorIndices: new Array(0).fill(0),\n bitmapScaleX: 1,\n bitmapScaleY: 1,\n\n spriteColorIndices: new Array(0).fill(0),\n spriteScaleX: 1,\n spriteScaleY: 1,\n\n spriteSheetName: undefined,\n\n spritesLineHeight: 0,\n\n spritesDirection: \"right\",\n spritesLineDirection: \"down\",\n\n spritesSpacing: 0,\n spritesLineSpacing: 0,\n\n spritesAlignment: \"end\",\n spritesLineAlignment: \"start\",\n};\n\nexport function isDirectionPositive(direction: DisplayDirection) {\n switch (direction) {\n case \"right\":\n case \"down\":\n return true;\n case \"left\":\n case \"up\":\n return false;\n }\n}\nexport function isDirectionHorizontal(direction: DisplayDirection) {\n switch (direction) {\n case \"right\":\n case \"left\":\n return true;\n case \"down\":\n case \"up\":\n return false;\n }\n}\n","export function deepEqual(obj1: any, obj2: any): boolean {\n if (obj1 === obj2) {\n return true;\n }\n\n if (\n typeof obj1 !== \"object\" ||\n obj1 === null ||\n typeof obj2 !== \"object\" ||\n obj2 === null\n ) {\n return false;\n }\n\n const keys1 = Object.keys(obj1);\n const keys2 = Object.keys(obj2);\n\n if (keys1.length !== keys2.length) return false;\n\n for (let key of keys1) {\n if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function removeRedundancies(array: any[]) {\n return Array.from(new Set(array));\n}\n","import { createConsole } from \"./Console.ts\";\nimport {\n DefaultDisplayContextState,\n DisplayContextState,\n DisplayContextStateKey,\n PartialDisplayContextState,\n} from \"./DisplayContextState.ts\";\nimport { deepEqual } from \"./ObjectUtils.ts\";\n\nconst _console = createConsole(\"DisplayContextStateHelper\", { log: false });\n\nclass DisplayContextStateHelper {\n #state: DisplayContextState = Object.assign({}, DefaultDisplayContextState);\n get state() {\n return this.#state;\n }\n\n get isSegmentUniform() {\n return (\n this.state.segmentStartRadius == this.state.segmentEndRadius &&\n this.state.segmentStartCap == this.state.segmentEndCap\n );\n }\n\n diff(other: PartialDisplayContextState) {\n let differences: DisplayContextStateKey[] = [];\n const keys = Object.keys(other) as DisplayContextStateKey[];\n keys.forEach((key) => {\n const value = other[key]!;\n\n if (!deepEqual(this.#state[key], value)) {\n differences.push(key);\n }\n });\n _console.log(\"diff\", other, differences);\n return differences;\n }\n update(newState: PartialDisplayContextState) {\n let differences = this.diff(newState);\n if (differences.length == 0) {\n _console.log(\"redundant contextState\", newState);\n }\n differences.forEach((key) => {\n const value = newState[key]!;\n // @ts-expect-error\n this.#state[key] = value;\n });\n return differences;\n }\n reset() {\n Object.assign(this.#state, DefaultDisplayContextState);\n }\n}\n\nexport default DisplayContextStateHelper;\n","import {\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayBrightness,\n DisplayBrightnesses,\n DisplayPixelDepth,\n DisplayPixelDepths,\n DisplayPointDataType,\n DisplayPointDataTypes,\n displayPointDataTypeToRange,\n displayPointDataTypeToSize,\n DisplayWireframe,\n DisplayWireframeEdge,\n} from \"../DisplayManager.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { DisplayContextCommandType } from \"./DisplayContextCommand.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayAlignmentDirections,\n DisplayAlignments,\n DisplayContextStateKey,\n DisplayDirection,\n DisplayDirections,\n DisplaySegmentCap,\n DisplaySegmentCaps,\n} from \"./DisplayContextState.ts\";\nimport {\n getVector2Distance,\n Int16Max,\n Uint16Max,\n Vector2,\n} from \"./MathUtils.ts\";\nimport RangeHelper from \"./RangeHelper.ts\";\n\nconst _console = createConsole(\"DisplayUtils\", { log: false });\n\nexport function formatRotation(\n rotation: number,\n isRadians?: boolean,\n isSigned?: boolean\n) {\n if (isRadians) {\n const rotationRad = rotation;\n _console.log({ rotationRad });\n rotation %= 2 * Math.PI;\n rotation /= 2 * Math.PI;\n } else {\n const rotationDeg = rotation;\n _console.log({ rotationDeg });\n rotation %= 360;\n rotation /= 360;\n }\n if (isSigned) {\n rotation *= Int16Max;\n } else {\n rotation *= Uint16Max;\n }\n rotation = Math.floor(rotation);\n _console.log({ formattedRotation: rotation });\n return rotation;\n}\n\nexport function roundToStep(value: number, step: number) {\n const roundedValue = Math.round(value / step) * step;\n //_console.log(value, step, roundedValue);\n return roundedValue;\n}\n\nexport const minDisplayScale = -50;\nexport const maxDisplayScale = 50;\nexport const displayScaleStep = 0.002;\nexport function formatScale(bitmapScale: number) {\n bitmapScale /= displayScaleStep;\n //_console.log({ formattedBitmapScale: bitmapScale });\n return bitmapScale;\n}\nexport function roundScale(bitmapScale: number) {\n return roundToStep(bitmapScale, displayScaleStep);\n}\n\nexport function assertValidSegmentCap(segmentCap: DisplaySegmentCap) {\n _console.assertEnumWithError(segmentCap, DisplaySegmentCaps);\n}\n\nexport function assertValidDisplayBrightness(\n displayBrightness: DisplayBrightness\n) {\n _console.assertEnumWithError(displayBrightness, DisplayBrightnesses);\n}\n\nexport function assertValidColorValue(name: string, value: number) {\n _console.assertRangeWithError(name, value, 0, 255);\n}\nexport function assertValidColor(color: DisplayColorRGB) {\n assertValidColorValue(\"red\", color.r);\n assertValidColorValue(\"green\", color.g);\n assertValidColorValue(\"blue\", color.b);\n}\n\nexport function assertValidOpacity(value: number) {\n _console.assertRangeWithError(\"opacity\", value, 0, 1);\n}\n\nexport const DisplayCropDirections = [\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n] as const;\nexport type DisplayCropDirection = (typeof DisplayCropDirections)[number];\n\nexport const DisplayContextCropStateKeys = [\n \"cropTop\",\n \"cropRight\",\n \"cropBottom\",\n \"cropLeft\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextCropStateKey =\n (typeof DisplayContextCropStateKeys)[number];\n\nexport const DisplayCropDirectionToStateKey: Record<\n DisplayCropDirection,\n DisplayContextCropStateKey\n> = {\n top: \"cropTop\",\n right: \"cropRight\",\n bottom: \"cropBottom\",\n left: \"cropLeft\",\n};\n\nexport const DisplayContextCropCommandTypes = [\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextCropCommandType =\n (typeof DisplayContextCropCommandTypes)[number];\n\nexport const DisplayCropDirectionToCommandType: Record<\n DisplayCropDirection,\n DisplayContextCropCommandType\n> = {\n top: \"setCropTop\",\n right: \"setCropRight\",\n bottom: \"setCropBottom\",\n left: \"setCropLeft\",\n};\n\nexport const DisplayContextRotationCropStateKeys = [\n \"rotationCropTop\",\n \"rotationCropRight\",\n \"rotationCropBottom\",\n \"rotationCropLeft\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextRotationCropStateKey =\n (typeof DisplayContextRotationCropStateKeys)[number];\n\nexport const DisplayRotationCropDirectionToStateKey: Record<\n DisplayCropDirection,\n DisplayContextRotationCropStateKey\n> = {\n top: \"rotationCropTop\",\n right: \"rotationCropRight\",\n bottom: \"rotationCropBottom\",\n left: \"rotationCropLeft\",\n};\n\nexport const DisplayContextRotationCropCommandTypes = [\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextRotationCropCommandType =\n (typeof DisplayContextRotationCropCommandTypes)[number];\n\nexport const DisplayRotationCropDirectionToCommandType: Record<\n DisplayCropDirection,\n DisplayContextRotationCropCommandType\n> = {\n top: \"setRotationCropTop\",\n right: \"setRotationCropRight\",\n bottom: \"setRotationCropBottom\",\n left: \"setRotationCropLeft\",\n};\n\nexport const DisplayContextAlignmentCommandTypes = [\n \"setVerticalAlignment\",\n \"setHorizontalAlignment\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplayContextAlignmentCommandType =\n (typeof DisplayContextAlignmentCommandTypes)[number];\nexport const DisplayAlignmentDirectionToCommandType: Record<\n DisplayAlignmentDirection,\n DisplayContextAlignmentCommandType\n> = {\n horizontal: \"setHorizontalAlignment\",\n vertical: \"setVerticalAlignment\",\n};\n\nexport const DisplayContextAlignmentStateKeys = [\n \"verticalAlignment\",\n \"horizontalAlignment\",\n] as const satisfies readonly DisplayContextStateKey[];\nexport type DisplayContextAlignmentStateKey =\n (typeof DisplayContextAlignmentStateKeys)[number];\n\nexport const DisplayAlignmentDirectionToStateKey: Record<\n DisplayAlignmentDirection,\n DisplayContextAlignmentStateKey\n> = {\n horizontal: \"horizontalAlignment\",\n vertical: \"verticalAlignment\",\n};\n\nexport function pixelDepthToNumberOfColors(pixelDepth: DisplayPixelDepth) {\n return 2 ** Number(pixelDepth);\n}\nexport function pixelDepthToPixelsPerByte(pixelDepth: DisplayPixelDepth) {\n return 8 / Number(pixelDepth);\n}\nexport function pixelDepthToPixelBitWidth(pixelDepth: DisplayPixelDepth) {\n return Number(pixelDepth);\n}\nexport function numberOfColorsToPixelDepth(numberOfColors: number) {\n return DisplayPixelDepths.find(\n (pixelDepth) => numberOfColors <= pixelDepthToNumberOfColors(pixelDepth)\n );\n}\n\nexport const DisplayScaleDirections = [\"x\", \"y\", \"all\"] as const;\nexport type DisplayScaleDirection = (typeof DisplayScaleDirections)[number];\n\nexport const DisplayBitmapScaleDirectionToCommandType: Record<\n DisplayScaleDirection,\n DisplayContextCommandType\n> = {\n x: \"setBitmapScaleX\",\n y: \"setBitmapScaleY\",\n all: \"setBitmapScale\",\n};\n\nexport const DisplaySpriteScaleDirectionToCommandType: Record<\n DisplayScaleDirection,\n DisplayContextCommandType\n> = {\n x: \"setSpriteScaleX\",\n y: \"setSpriteScaleY\",\n all: \"setSpriteScale\",\n};\n\nexport type DisplayColorRGB = {\n r: number;\n g: number;\n b: number;\n};\nexport type DisplayColorYCbCr = {\n y: number;\n cb: number;\n cr: number;\n};\n\nexport function assertValidAlignment(alignment: DisplayAlignment) {\n _console.assertEnumWithError(alignment, DisplayAlignments);\n}\n\nexport function assertValidDirection(direction: DisplayDirection) {\n _console.assertEnumWithError(direction, DisplayDirections);\n}\n\nexport function assertValidAlignmentDirection(\n direction: DisplayAlignmentDirection\n) {\n _console.assertEnumWithError(direction, DisplayAlignmentDirections);\n}\n\nexport const displayCurveTypeToNumberOfControlPoints: Record<\n DisplayBezierCurveType,\n number\n> = {\n segment: 2,\n quadratic: 3,\n cubic: 4,\n};\nexport const displayCurveTolerance = 2.0;\nexport const displayCurveToleranceSquared = displayCurveTolerance ** 2;\n\nexport const maxNumberOfDisplayCurvePoints = 150;\nexport function assertValidNumberOfControlPoints(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n isPath = false\n) {\n let numberOfControlPoints =\n displayCurveTypeToNumberOfControlPoints[curveType];\n if (isPath) {\n numberOfControlPoints -= 1;\n }\n _console.assertWithError(\n controlPoints.length == numberOfControlPoints,\n `invalid number of control points ${controlPoints.length}, expected ${numberOfControlPoints}`\n );\n}\nexport function assertValidPathNumberOfControlPoints(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[]\n) {\n const numberOfControlPoints =\n displayCurveTypeToNumberOfControlPoints[curveType];\n _console.assertWithError(\n (controlPoints.length - 1) % (numberOfControlPoints - 1) == 0,\n `invalid number of path control points ${controlPoints.length} for path \"${curveType}\"`\n );\n}\n\nexport function assertValidPath(curves: DisplayBezierCurve[]) {\n curves.forEach((curve, index) => {\n const { type, controlPoints } = curve;\n assertValidNumberOfControlPoints(type, controlPoints, index > 0);\n });\n}\n\nexport function assertValidWireframe({ points, edges }: DisplayWireframe) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n _console.assertRangeWithError(\"numberOfEdges\", edges.length, 1, 255);\n\n edges.forEach((edge, index) => {\n _console.assertRangeWithError(\n `edgeStartIndex.${index}`,\n edge.startIndex,\n 0,\n points.length\n );\n _console.assertRangeWithError(\n `edgeEndIndex.${index}`,\n edge.endIndex,\n 0,\n points.length\n );\n });\n}\nexport function isWireframePolygon({\n points,\n edges,\n}: DisplayWireframe): Vector2[] | undefined {\n _console.log(\"isWireframePolygon?\", points, edges);\n if (points.length != edges.length) {\n return;\n }\n const _edges = edges.slice();\n let pointIndices: number[] = [];\n for (let i = 0; i < points.length; i++) {\n if (i == 0) {\n const { startIndex, endIndex } = _edges.shift()!;\n pointIndices.push(startIndex);\n pointIndices.push(endIndex);\n } else {\n const startIndex = pointIndices.at(-1);\n const edge = _edges.find(\n (edge) => edge.startIndex == startIndex || edge.endIndex == startIndex\n );\n _console.log(i, \"edge\", edge);\n if (edge) {\n _edges.splice(_edges.indexOf(edge), 1);\n const endIndex =\n edge.startIndex == startIndex ? edge.endIndex : edge.startIndex;\n if (i == points.length - 1) {\n if (endIndex != pointIndices[0]) {\n return;\n }\n } else if (pointIndices.includes(endIndex)) {\n _console.log(\"duplicate endIndex\", endIndex);\n return;\n }\n pointIndices.push(endIndex);\n } else {\n _console.log(\"no edge found\");\n return;\n }\n }\n _console.log(\"remaining edges\", _edges);\n }\n _console.log(\"pointIndices\", pointIndices);\n const polygon = pointIndices\n .map((pointIndex) => points[pointIndex])\n .filter((point, index, polygon) => polygon.indexOf(point) == index);\n\n if (polygon.length == points.length) {\n polygon.push(polygon[0]);\n _console.log(\"polygon\", polygon);\n return polygon;\n }\n}\n\nexport function mergeWireframes(a: DisplayWireframe, b: DisplayWireframe) {\n const wireframe: DisplayWireframe = structuredClone(a);\n const pointIndexOffset = a.points.length;\n b.points.forEach((point) => {\n wireframe.points.push(point);\n });\n b.edges.forEach(({ startIndex, endIndex }) => {\n wireframe.edges.push({\n startIndex: startIndex + pointIndexOffset,\n endIndex: endIndex + pointIndexOffset,\n });\n });\n return trimWireframe(wireframe);\n}\n\nexport function intersectWireframes(\n a: DisplayWireframe,\n b: DisplayWireframe,\n ignoreDirection = true\n) {\n a = trimWireframe(a);\n b = trimWireframe(b);\n //_console.log(\"intersectWireframes\", a, b);\n const wireframe: DisplayWireframe = { points: [], edges: [] };\n const pointIndices: { a: number; b: number }[] = [];\n const aPointIndices: number[] = [];\n const bPointIndices: number[] = [];\n a.points.forEach((point, aPointIndex) => {\n const bPointIndex = b.points.findIndex((_point) => {\n const distance = getVector2Distance(point, _point);\n return distance == 0;\n });\n if (bPointIndex != -1) {\n pointIndices.push({ a: aPointIndex, b: bPointIndex });\n aPointIndices.push(aPointIndex);\n bPointIndices.push(bPointIndex);\n wireframe.points.push(structuredClone(point));\n }\n });\n a.edges.forEach((aEdge) => {\n if (\n !aPointIndices.includes(aEdge.startIndex) ||\n !aPointIndices.includes(aEdge.endIndex)\n ) {\n return;\n }\n const startIndex = aPointIndices.indexOf(aEdge.startIndex);\n const endIndex = aPointIndices.indexOf(aEdge.endIndex);\n\n const bEdge = b.edges.find((bEdge) => {\n if (\n !bPointIndices.includes(bEdge.startIndex) ||\n !bPointIndices.includes(bEdge.endIndex)\n ) {\n return false;\n }\n const bStartIndex = bPointIndices.indexOf(bEdge.startIndex);\n const bEndIndex = bPointIndices.indexOf(bEdge.endIndex);\n if (ignoreDirection) {\n return (\n (startIndex == bStartIndex && endIndex == bEndIndex) ||\n (startIndex == bEndIndex && endIndex == bStartIndex)\n );\n } else {\n return startIndex == bStartIndex && endIndex == bEndIndex;\n }\n });\n\n if (!bEdge) {\n return;\n }\n\n wireframe.edges.push({\n startIndex,\n endIndex,\n });\n });\n //_console.log(\"intersectedWireframe\", wireframe);\n return wireframe;\n}\n\nexport function trimWireframe(wireframe: DisplayWireframe): DisplayWireframe {\n _console.log(\"trimming wireframe\", wireframe);\n const { points, edges } = wireframe;\n const trimmedPoints: Vector2[] = [];\n const trimmedEdges: DisplayWireframeEdge[] = [];\n edges.forEach((edge) => {\n const { startIndex, endIndex } = edge;\n let startPoint = points[startIndex];\n let endPoint = points[endIndex];\n\n let trimmedStartIndex = trimmedPoints.findIndex(\n ({ x, y }) => startPoint.x == x && startPoint.y == y\n );\n if (trimmedStartIndex == -1) {\n //_console.log(\"adding startPoint\", startPoint);\n trimmedPoints.push(startPoint);\n trimmedStartIndex = trimmedPoints.length - 1;\n }\n\n let trimmedEndIndex = trimmedPoints.findIndex(\n ({ x, y }) => endPoint.x == x && endPoint.y == y\n );\n if (trimmedEndIndex == -1) {\n //_console.log(\"adding endPoint\", endPoint);\n trimmedPoints.push(endPoint);\n trimmedEndIndex = trimmedPoints.length - 1;\n }\n\n const trimmedEdge: DisplayWireframeEdge = {\n startIndex: trimmedStartIndex,\n endIndex: trimmedEndIndex,\n };\n let trimmedEdgeIndex = trimmedEdges.findIndex(\n ({ startIndex, endIndex }) =>\n startIndex == trimmedEdge.startIndex && endIndex == trimmedEdge.endIndex\n );\n if (trimmedEdgeIndex == -1) {\n //_console.log(\"adding edge\", trimmedEdge);\n trimmedEdges.push(trimmedEdge);\n trimmedEdgeIndex = trimmedEdges.length - 1;\n }\n });\n _console.log(\"trimmedWireframe\", trimmedPoints, trimmedEdges);\n return { points: trimmedPoints, edges: trimmedEdges };\n}\n\nexport function getPointDataType(points: Vector2[]): DisplayPointDataType {\n const range = new RangeHelper();\n points.forEach(({ x, y }) => {\n range.update(x);\n range.update(y);\n });\n const pointDataType = DisplayPointDataTypes.find((pointDataType) => {\n const { min, max } = displayPointDataTypeToRange[pointDataType];\n return range.min >= min && range.max <= max;\n })!;\n _console.log(\"pointDataType\", pointDataType, points);\n return pointDataType!;\n}\nexport function serializePoints(\n points: Vector2[],\n pointDataType?: DisplayPointDataType,\n isPath = false\n) {\n pointDataType = pointDataType || getPointDataType(points);\n _console.assertEnumWithError(pointDataType, DisplayPointDataTypes);\n const pointDataSize = displayPointDataTypeToSize[pointDataType];\n let dataViewLength = points.length * pointDataSize;\n if (!isPath) {\n dataViewLength += 2; // pointDataType + points.length\n }\n const dataView = new DataView(new ArrayBuffer(dataViewLength));\n _console.log(\n `serializing ${points.length} ${pointDataType} points (${dataView.byteLength} bytes)...`\n );\n let offset = 0;\n if (!isPath) {\n dataView.setUint8(offset++, DisplayPointDataTypes.indexOf(pointDataType));\n dataView.setUint8(offset++, points.length);\n }\n points.forEach(({ x, y }) => {\n switch (pointDataType) {\n case \"int8\":\n dataView.setInt8(offset, x);\n offset += 1;\n dataView.setInt8(offset, y);\n offset += 1;\n break;\n case \"int16\":\n dataView.setInt16(offset, x, true);\n offset += 2;\n dataView.setInt16(offset, y, true);\n offset += 2;\n break;\n case \"float\":\n dataView.setFloat32(offset, x, true);\n offset += 4;\n dataView.setFloat32(offset, y, true);\n offset += 4;\n break;\n }\n });\n return dataView;\n}\n","import {\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayBezierCurveTypes,\n DisplayBitmap,\n DisplayBitmapColorPair,\n displayCurveTypeBitWidth,\n DisplayPointDataTypes,\n displayCurveTypesPerByte,\n DisplaySpriteColorPair,\n DisplayWireframe,\n} from \"../DisplayManager.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./ArrayBufferUtils.ts\";\nimport { rgbToHex, stringToRGB } from \"./ColorUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { drawBitmapHeaderLength, getBitmapData } from \"./DisplayBitmapUtils.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignments,\n DisplayDirection,\n DisplayDirections,\n DisplaySegmentCap,\n DisplaySegmentCaps,\n} from \"./DisplayContextState.ts\";\nimport { DisplayManagerInterface } from \"./DisplayManagerInterface.ts\";\nimport { DisplaySpriteSerializedLines } from \"./DisplaySpriteSheetUtils.ts\";\nimport {\n assertValidAlignment,\n assertValidColor,\n assertValidDirection,\n assertValidPathNumberOfControlPoints,\n assertValidNumberOfControlPoints,\n assertValidOpacity,\n assertValidPath,\n assertValidSegmentCap,\n assertValidWireframe,\n DisplayColorRGB,\n formatRotation,\n formatScale,\n maxDisplayScale,\n minDisplayScale,\n roundScale,\n serializePoints,\n getPointDataType,\n} from \"./DisplayUtils.ts\";\nimport {\n clamp,\n degToRad,\n Int16Max,\n Int16Min,\n normalizeRadians,\n twoPi,\n Vector2,\n} from \"./MathUtils.ts\";\n\nconst _console = createConsole(\"DisplayContextCommand\", { log: false });\n\nexport const DisplayContextCommandTypes = [\n \"show\",\n \"clear\",\n\n \"setColor\",\n \"setColorOpacity\",\n \"setOpacity\",\n\n \"saveContext\",\n \"restoreContext\",\n\n \"selectBackgroundColor\",\n \"selectFillColor\",\n \"selectLineColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setHorizontalAlignment\",\n \"setVerticalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"clearRect\",\n\n \"drawRect\",\n \"drawRoundRect\",\n\n \"drawCircle\",\n \"drawArc\",\n\n \"drawEllipse\",\n \"drawArcEllipse\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawBitmap\",\n\n \"selectSpriteSheet\",\n \"drawSprite\",\n \"drawSprites\",\n\n \"startSprite\",\n \"endSprite\",\n] as const;\nexport type DisplayContextCommandType =\n (typeof DisplayContextCommandTypes)[number];\n\nexport const DisplaySpriteContextCommandTypes = [\n \"selectFillColor\",\n \"selectLineColor\",\n // \"selectBackgroundColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n // \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setVerticalAlignment\",\n \"setHorizontalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"clearRect\",\n\n \"drawRect\",\n \"drawRoundRect\",\n \"drawCircle\",\n \"drawEllipse\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawArc\",\n \"drawArcEllipse\",\n\n \"drawBitmap\",\n \"drawSprite\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type DisplaySpriteContextCommandType =\n (typeof DisplaySpriteContextCommandTypes)[number];\n\nexport interface BaseDisplayContextCommand {\n type: DisplayContextCommandType | \"runDisplayContextCommands\";\n hide?: boolean;\n}\n\nexport interface SimpleDisplayCommand extends BaseDisplayContextCommand {\n type:\n | \"show\"\n | \"clear\"\n | \"saveContext\"\n | \"restoreContext\"\n | \"clearRotation\"\n | \"clearCrop\"\n | \"clearRotationCrop\"\n | \"resetBitmapScale\"\n | \"resetSpriteColors\"\n | \"resetSpriteScale\"\n | \"resetAlignment\"\n | \"endSprite\";\n}\n\nexport interface SetDisplayColorCommand extends BaseDisplayContextCommand {\n type: \"setColor\";\n colorIndex: number;\n color: DisplayColorRGB | string;\n}\nexport interface SetDisplayColorOpacityCommand\n extends BaseDisplayContextCommand {\n type: \"setColorOpacity\";\n colorIndex: number;\n opacity: number;\n}\nexport interface SetDisplayOpacityCommand extends BaseDisplayContextCommand {\n type: \"setOpacity\";\n opacity: number;\n}\n\nexport interface SetDisplayHorizontalAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setHorizontalAlignment\";\n horizontalAlignment: DisplayAlignment;\n}\nexport interface SetDisplayVerticalAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setVerticalAlignment\";\n verticalAlignment: DisplayAlignment;\n}\n\nexport interface SelectDisplayBackgroundColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectBackgroundColor\";\n backgroundColorIndex: number;\n}\nexport interface SelectDisplayFillColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectFillColor\";\n fillColorIndex: number;\n}\nexport interface SelectDisplayLineColorCommand\n extends BaseDisplayContextCommand {\n type: \"selectLineColor\";\n lineColorIndex: number;\n}\nexport interface SelectDisplayIgnoreFillCommand\n extends BaseDisplayContextCommand {\n type: \"setIgnoreFill\";\n ignoreFill: boolean;\n}\nexport interface SelectDisplayIgnoreLineCommand\n extends BaseDisplayContextCommand {\n type: \"setIgnoreLine\";\n ignoreLine: boolean;\n}\nexport interface SelectDisplayFillBackgroundCommand\n extends BaseDisplayContextCommand {\n type: \"setFillBackground\";\n fillBackground: boolean;\n}\nexport interface SetDisplayLineWidthCommand extends BaseDisplayContextCommand {\n type: \"setLineWidth\";\n lineWidth: number;\n}\nexport interface SetDisplayRotationCommand extends BaseDisplayContextCommand {\n type: \"setRotation\";\n rotation: number;\n isRadians?: boolean;\n}\n\nexport interface SetDisplaySegmentStartCapCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentStartCap\";\n segmentStartCap: DisplaySegmentCap;\n}\nexport interface SetDisplaySegmentEndCapCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentEndCap\";\n segmentEndCap: DisplaySegmentCap;\n}\nexport interface SetDisplaySegmentCapCommand extends BaseDisplayContextCommand {\n type: \"setSegmentCap\";\n segmentCap: DisplaySegmentCap;\n}\n\nexport interface SetDisplaySegmentStartRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentStartRadius\";\n segmentStartRadius: number;\n}\nexport interface SetDisplaySegmentEndRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentEndRadius\";\n segmentEndRadius: number;\n}\nexport interface SetDisplaySegmentRadiusCommand\n extends BaseDisplayContextCommand {\n type: \"setSegmentRadius\";\n segmentRadius: number;\n}\n\nexport interface SetDisplayCropTopCommand extends BaseDisplayContextCommand {\n type: \"setCropTop\";\n cropTop: number;\n}\nexport interface SetDisplayCropRightCommand extends BaseDisplayContextCommand {\n type: \"setCropRight\";\n cropRight: number;\n}\nexport interface SetDisplayCropBottomCommand extends BaseDisplayContextCommand {\n type: \"setCropBottom\";\n cropBottom: number;\n}\nexport interface SetDisplayCropLeftCommand extends BaseDisplayContextCommand {\n type: \"setCropLeft\";\n cropLeft: number;\n}\n\nexport interface SetDisplayRotationCropTopCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropTop\";\n rotationCropTop: number;\n}\nexport interface SetDisplayRotationCropRightCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropRight\";\n rotationCropRight: number;\n}\nexport interface SetDisplayRotationCropBottomCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropBottom\";\n rotationCropBottom: number;\n}\nexport interface SetDisplayRotationCropLeftCommand\n extends BaseDisplayContextCommand {\n type: \"setRotationCropLeft\";\n rotationCropLeft: number;\n}\n\nexport interface SelectDisplayBitmapColorIndexCommand\n extends BaseDisplayContextCommand {\n type: \"selectBitmapColor\";\n bitmapColorIndex: number;\n colorIndex: number;\n}\nexport interface SelectDisplayBitmapColorIndicesCommand\n extends BaseDisplayContextCommand {\n type: \"selectBitmapColors\";\n bitmapColorPairs: DisplayBitmapColorPair[];\n}\n\nexport interface SetDisplayBitmapScaleXCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScaleX\";\n bitmapScaleX: number;\n}\nexport interface SetDisplayBitmapScaleYCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScaleY\";\n bitmapScaleY: number;\n}\nexport interface SetDisplayBitmapScaleCommand\n extends BaseDisplayContextCommand {\n type: \"setBitmapScale\";\n bitmapScale: number;\n}\n\nexport interface SelectDisplaySpriteColorIndexCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteColor\";\n spriteColorIndex: number;\n colorIndex: number;\n}\nexport interface SelectDisplaySpriteColorIndicesCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteColors\";\n spriteColorPairs: DisplaySpriteColorPair[];\n}\n\nexport interface SetDisplaySpriteScaleXCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScaleX\";\n spriteScaleX: number;\n}\nexport interface SetDisplaySpriteScaleYCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScaleY\";\n spriteScaleY: number;\n}\nexport interface SetDisplaySpriteScaleCommand\n extends BaseDisplayContextCommand {\n type: \"setSpriteScale\";\n spriteScale: number;\n}\n\nexport interface SetDisplaySpritesLineHeightCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineHeight\";\n spritesLineHeight: number;\n}\n\nexport interface SetDisplaySpritesDirectionCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesDirection\";\n spritesDirection: DisplayDirection;\n}\nexport interface SetDisplaySpritesLineDirectionCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineDirection\";\n spritesLineDirection: DisplayDirection;\n}\n\nexport interface SetDisplaySpritesSpacingCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesSpacing\";\n spritesSpacing: number;\n}\nexport interface SetDisplaySpritesLineSpacingCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineSpacing\";\n spritesLineSpacing: number;\n}\n\nexport interface SetDisplaySpritesAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesAlignment\";\n spritesAlignment: DisplayAlignment;\n}\nexport interface SetDisplaySpritesLineAlignmentCommand\n extends BaseDisplayContextCommand {\n type: \"setSpritesLineAlignment\";\n spritesLineAlignment: DisplayAlignment;\n}\n\nexport interface BasePositionDisplayContextCommand\n extends BaseDisplayContextCommand {\n x: number;\n y: number;\n}\nexport interface BaseOffsetPositionDisplayContextCommand\n extends BaseDisplayContextCommand {\n offsetX: number;\n offsetY: number;\n}\nexport interface BaseSizeDisplayContextCommand\n extends BaseDisplayContextCommand {\n width: number;\n height: number;\n}\n\nexport interface BaseDisplayRectCommand\n extends BasePositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {}\nexport interface BaseDisplayCenterRectCommand\n extends BaseOffsetPositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {}\n\nexport interface ClearDisplayRectCommand extends BaseDisplayRectCommand {\n type: \"clearRect\";\n}\nexport interface DrawDisplayRectCommand extends BaseDisplayCenterRectCommand {\n type: \"drawRect\";\n}\n\nexport interface DrawDisplayRoundedRectCommand\n extends BaseOffsetPositionDisplayContextCommand,\n BaseSizeDisplayContextCommand {\n type: \"drawRoundRect\";\n borderRadius: number;\n}\n\nexport interface DrawDisplayCircleCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawCircle\";\n radius: number;\n}\nexport interface DrawDisplayEllipseCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawEllipse\";\n radiusX: number;\n radiusY: number;\n}\n\nexport interface DrawDisplayRegularPolygonCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawRegularPolygon\";\n radius: number;\n numberOfSides: number;\n}\nexport interface DrawDisplayPolygonCommand extends BaseDisplayContextCommand {\n type: \"drawPolygon\";\n points: Vector2[];\n}\nexport interface DrawDisplaySegmentCommand extends BaseDisplayContextCommand {\n type: \"drawSegment\";\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}\nexport interface DrawDisplaySegmentsCommand extends BaseDisplayContextCommand {\n type: \"drawSegments\";\n points: Vector2[];\n}\n\nexport interface DrawDisplayBezierCurveCommand\n extends BaseDisplayContextCommand {\n type:\n | \"drawQuadraticBezierCurve\"\n | \"drawQuadraticBezierCurves\"\n | \"drawCubicBezierCurve\"\n | \"drawCubicBezierCurves\";\n controlPoints: Vector2[];\n}\n\nexport interface DrawDisplayPathCommand extends BaseDisplayContextCommand {\n type: \"drawPath\" | \"drawClosedPath\";\n curves: DisplayBezierCurve[];\n}\n\nexport interface DrawDisplayWireframeCommand extends BaseDisplayContextCommand {\n type: \"drawWireframe\";\n wireframe: DisplayWireframe;\n}\n\nexport interface DrawDisplayArcCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawArc\";\n radius: number;\n startAngle: number;\n angleOffset: number;\n isRadians?: boolean;\n}\nexport interface DrawDisplayArcEllipseCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawArcEllipse\";\n radiusX: number;\n radiusY: number;\n startAngle: number;\n angleOffset: number;\n isRadians?: boolean;\n}\n\nexport interface DrawDisplayBitmapCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawBitmap\";\n bitmap: DisplayBitmap;\n}\n\nexport interface SelectDisplaySpriteSheetCommand\n extends BaseDisplayContextCommand {\n type: \"selectSpriteSheet\";\n spriteSheetIndex: number;\n}\n\nexport interface DrawDisplaySpriteCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawSprite\";\n spriteIndex: number;\n use2Bytes: boolean;\n}\n\nexport interface DrawDisplaySpritesCommand\n extends BaseOffsetPositionDisplayContextCommand {\n type: \"drawSprites\";\n spriteSerializedLines: DisplaySpriteSerializedLines;\n}\n\nexport interface StartDisplaySpriteCommand\n extends BaseDisplayCenterRectCommand {\n type: \"startSprite\";\n}\n\nexport type DisplayContextCommand =\n | SimpleDisplayCommand\n | SetDisplayColorCommand\n | SetDisplayColorOpacityCommand\n | SetDisplayOpacityCommand\n | SelectDisplayBackgroundColorCommand\n | SelectDisplayFillColorCommand\n | SelectDisplayLineColorCommand\n | SetDisplayLineWidthCommand\n | SetDisplayRotationCommand\n | SetDisplaySegmentStartCapCommand\n | SetDisplaySegmentEndCapCommand\n | SetDisplaySegmentCapCommand\n | SetDisplaySegmentStartRadiusCommand\n | SetDisplaySegmentEndRadiusCommand\n | SetDisplaySegmentRadiusCommand\n | SetDisplayCropTopCommand\n | SetDisplayCropRightCommand\n | SetDisplayCropBottomCommand\n | SetDisplayCropLeftCommand\n | SetDisplayRotationCropTopCommand\n | SetDisplayRotationCropRightCommand\n | SetDisplayRotationCropBottomCommand\n | SetDisplayRotationCropLeftCommand\n | SelectDisplayBitmapColorIndexCommand\n | SelectDisplayBitmapColorIndicesCommand\n | SetDisplayBitmapScaleXCommand\n | SetDisplayBitmapScaleYCommand\n | SetDisplayBitmapScaleCommand\n | SelectDisplaySpriteColorIndexCommand\n | SelectDisplaySpriteColorIndicesCommand\n | SetDisplaySpriteScaleXCommand\n | SetDisplaySpriteScaleYCommand\n | SetDisplaySpriteScaleCommand\n | ClearDisplayRectCommand\n | DrawDisplayRectCommand\n | DrawDisplayRoundedRectCommand\n | DrawDisplayCircleCommand\n | DrawDisplayEllipseCommand\n | DrawDisplayRegularPolygonCommand\n | DrawDisplayPolygonCommand\n | DrawDisplaySegmentCommand\n | DrawDisplaySegmentsCommand\n | DrawDisplayArcCommand\n | DrawDisplayArcEllipseCommand\n | DrawDisplayBitmapCommand\n | DrawDisplaySpriteCommand\n | DrawDisplaySpritesCommand\n | SelectDisplaySpriteSheetCommand\n | SetDisplayHorizontalAlignmentCommand\n | SetDisplayVerticalAlignmentCommand\n | SetDisplaySpritesDirectionCommand\n | SetDisplaySpritesLineDirectionCommand\n | SetDisplaySpritesSpacingCommand\n | SetDisplaySpritesLineSpacingCommand\n | SetDisplaySpritesAlignmentCommand\n | SetDisplaySpritesLineAlignmentCommand\n | SetDisplaySpritesLineHeightCommand\n | DrawDisplayWireframeCommand\n | DrawDisplayBezierCurveCommand\n | DrawDisplayPathCommand\n | SelectDisplayIgnoreFillCommand\n | SelectDisplayIgnoreLineCommand\n | SelectDisplayFillBackgroundCommand\n | StartDisplaySpriteCommand;\n\nexport function serializeContextCommand(\n displayManager: DisplayManagerInterface,\n command: DisplayContextCommand\n) {\n let dataView: DataView | undefined;\n\n switch (command.type) {\n case \"show\":\n case \"clear\":\n case \"saveContext\":\n case \"restoreContext\":\n case \"clearRotation\":\n case \"clearCrop\":\n case \"clearRotationCrop\":\n case \"resetBitmapScale\":\n case \"resetSpriteColors\":\n case \"resetSpriteScale\":\n case \"resetAlignment\":\n case \"endSprite\":\n break;\n case \"setColor\":\n {\n const { color, colorIndex } = command;\n\n let colorRGB: DisplayColorRGB;\n if (typeof color == \"string\") {\n colorRGB = stringToRGB(color);\n } else {\n colorRGB = color;\n }\n const colorHex = rgbToHex(colorRGB);\n if (displayManager.colors[colorIndex] == colorHex) {\n _console.log(`redundant color #${colorIndex} ${colorHex}`);\n return;\n }\n\n //_console.log(`setting color #${colorIndex}`, colorRGB);\n displayManager.assertValidColorIndex(colorIndex);\n assertValidColor(colorRGB);\n dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, colorRGB.r);\n dataView.setUint8(2, colorRGB.g);\n dataView.setUint8(3, colorRGB.b);\n }\n break;\n case \"setColorOpacity\":\n {\n const { colorIndex, opacity } = command;\n displayManager.assertValidColorIndex(colorIndex);\n assertValidOpacity(opacity);\n if (\n Math.floor(255 * displayManager.opacities[colorIndex]) ==\n Math.floor(255 * opacity)\n ) {\n _console.log(`redundant opacity #${colorIndex} ${opacity}`);\n return;\n }\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, opacity * 255);\n }\n break;\n case \"setOpacity\":\n {\n const { opacity } = command;\n assertValidOpacity(opacity);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, Math.round(opacity * 255));\n }\n break;\n case \"selectFillColor\":\n {\n const { fillColorIndex } = command;\n displayManager.assertValidColorIndex(fillColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, fillColorIndex);\n }\n break;\n case \"selectBackgroundColor\":\n {\n const { backgroundColorIndex } = command;\n displayManager.assertValidColorIndex(backgroundColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, backgroundColorIndex);\n }\n break;\n case \"selectLineColor\":\n {\n const { lineColorIndex } = command;\n displayManager.assertValidColorIndex(lineColorIndex);\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, lineColorIndex);\n }\n break;\n case \"setIgnoreFill\":\n {\n const { ignoreFill } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, ignoreFill ? 1 : 0);\n }\n break;\n case \"setIgnoreLine\":\n {\n const { ignoreLine } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, ignoreLine ? 1 : 0);\n }\n break;\n case \"setFillBackground\":\n {\n const { fillBackground } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, fillBackground ? 1 : 0);\n }\n break;\n case \"setLineWidth\":\n {\n const { lineWidth } = command;\n displayManager.assertValidLineWidth(lineWidth);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, lineWidth, true);\n }\n break;\n case \"setHorizontalAlignment\":\n {\n const { horizontalAlignment } = command;\n assertValidAlignment(horizontalAlignment);\n _console.log({ horizontalAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(horizontalAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setVerticalAlignment\":\n {\n const { verticalAlignment } = command;\n assertValidAlignment(verticalAlignment);\n _console.log({ verticalAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(verticalAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setRotation\":\n {\n let { rotation, isRadians } = command;\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation = normalizeRadians(rotation);\n isRadians = true;\n // _console.log({ rotation, isRadians });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, formatRotation(rotation, isRadians), true);\n }\n break;\n case \"setSegmentStartCap\":\n {\n const { segmentStartCap } = command;\n assertValidSegmentCap(segmentStartCap);\n _console.log({ segmentStartCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentStartCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentEndCap\":\n {\n const { segmentEndCap } = command;\n assertValidSegmentCap(segmentEndCap);\n _console.log({ segmentEndCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentEndCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentCap\":\n {\n const { segmentCap } = command;\n assertValidSegmentCap(segmentCap);\n _console.log({ segmentCap });\n dataView = new DataView(new ArrayBuffer(1));\n const segmentCapEnum = DisplaySegmentCaps.indexOf(segmentCap);\n dataView.setUint8(0, segmentCapEnum);\n }\n break;\n case \"setSegmentStartRadius\":\n {\n const { segmentStartRadius } = command;\n _console.log({ segmentStartRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentStartRadius, true);\n }\n break;\n case \"setSegmentEndRadius\":\n {\n const { segmentEndRadius } = command;\n _console.log({ segmentEndRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentEndRadius, true);\n }\n break;\n case \"setSegmentRadius\":\n {\n const { segmentRadius } = command;\n _console.log({ segmentRadius });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, segmentRadius, true);\n }\n break;\n case \"setCropTop\":\n {\n const { cropTop } = command;\n _console.log({ cropTop });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropTop, true);\n }\n break;\n case \"setCropRight\":\n {\n const { cropRight } = command;\n _console.log({ cropRight });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropRight, true);\n }\n break;\n case \"setCropBottom\":\n {\n const { cropBottom } = command;\n _console.log({ cropBottom });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropBottom, true);\n }\n break;\n case \"setCropLeft\":\n {\n const { cropLeft } = command;\n _console.log({ cropLeft });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, cropLeft, true);\n }\n break;\n case \"setRotationCropTop\":\n {\n const { rotationCropTop } = command;\n _console.log({ rotationCropTop });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropTop, true);\n }\n break;\n case \"setRotationCropRight\":\n {\n const { rotationCropRight } = command;\n _console.log({ rotationCropRight });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropRight, true);\n }\n break;\n case \"setRotationCropBottom\":\n {\n const { rotationCropBottom } = command;\n _console.log({ rotationCropBottom });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropBottom, true);\n }\n break;\n case \"setRotationCropLeft\":\n {\n const { rotationCropLeft } = command;\n _console.log({ rotationCropLeft });\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, rotationCropLeft, true);\n }\n break;\n case \"selectBitmapColor\":\n {\n const { bitmapColorIndex, colorIndex } = command;\n displayManager.assertValidColorIndex(bitmapColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, bitmapColorIndex);\n dataView.setUint8(1, colorIndex);\n }\n break;\n case \"selectBitmapColors\":\n {\n const { bitmapColorPairs } = command;\n\n _console.assertRangeWithError(\n \"bitmapColors\",\n bitmapColorPairs.length,\n 1,\n displayManager.numberOfColors\n );\n const bitmapColorIndices =\n displayManager.contextState.bitmapColorIndices.slice();\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n displayManager.assertValidColorIndex(bitmapColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n });\n\n dataView = new DataView(\n new ArrayBuffer(bitmapColorPairs.length * 2 + 1)\n );\n let offset = 0;\n dataView.setUint8(offset++, bitmapColorPairs.length);\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n dataView!.setUint8(offset, bitmapColorIndex);\n dataView!.setUint8(offset + 1, colorIndex);\n offset += 2;\n });\n }\n break;\n case \"setBitmapScaleX\":\n {\n let { bitmapScaleX } = command;\n bitmapScaleX = clamp(bitmapScaleX, minDisplayScale, maxDisplayScale);\n bitmapScaleX = roundScale(bitmapScaleX);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScaleX), true);\n }\n break;\n case \"setBitmapScaleY\":\n {\n let { bitmapScaleY } = command;\n bitmapScaleY = clamp(bitmapScaleY, minDisplayScale, maxDisplayScale);\n bitmapScaleY = roundScale(bitmapScaleY);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScaleY), true);\n }\n break;\n case \"setBitmapScale\":\n {\n let { bitmapScale } = command;\n bitmapScale = clamp(bitmapScale, minDisplayScale, maxDisplayScale);\n bitmapScale = roundScale(bitmapScale);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(bitmapScale), true);\n }\n break;\n case \"selectSpriteColor\":\n {\n const { spriteColorIndex, colorIndex } = command;\n displayManager.assertValidColorIndex(spriteColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint8(0, spriteColorIndex);\n dataView.setUint8(1, colorIndex);\n }\n break;\n case \"selectSpriteColors\":\n {\n const { spriteColorPairs } = command;\n _console.assertRangeWithError(\n \"spriteColors\",\n spriteColorPairs.length,\n 1,\n displayManager.numberOfColors\n );\n const spriteColorIndices =\n displayManager.contextState.spriteColorIndices.slice();\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n displayManager.assertValidColorIndex(spriteColorIndex);\n displayManager.assertValidColorIndex(colorIndex);\n spriteColorIndices[spriteColorIndex] = colorIndex;\n });\n\n dataView = new DataView(\n new ArrayBuffer(spriteColorPairs.length * 2 + 1)\n );\n let offset = 0;\n dataView.setUint8(offset++, spriteColorPairs.length);\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n dataView!.setUint8(offset, spriteColorIndex);\n dataView!.setUint8(offset + 1, colorIndex);\n offset += 2;\n });\n }\n break;\n case \"setSpriteScaleX\":\n {\n let { spriteScaleX } = command;\n spriteScaleX = clamp(spriteScaleX, minDisplayScale, maxDisplayScale);\n spriteScaleX = roundScale(spriteScaleX);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScaleX), true);\n }\n break;\n case \"setSpriteScaleY\":\n {\n let { spriteScaleY } = command;\n spriteScaleY = clamp(spriteScaleY, minDisplayScale, maxDisplayScale);\n spriteScaleY = roundScale(spriteScaleY);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScaleY), true);\n }\n break;\n case \"setSpriteScale\":\n {\n let { spriteScale } = command;\n spriteScale = clamp(spriteScale, minDisplayScale, maxDisplayScale);\n spriteScale = roundScale(spriteScale);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, formatScale(spriteScale), true);\n }\n break;\n case \"setSpritesLineHeight\":\n {\n const { spritesLineHeight } = command;\n displayManager.assertValidLineWidth(spritesLineHeight);\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setUint16(0, spritesLineHeight, true);\n }\n break;\n case \"setSpritesDirection\":\n {\n const { spritesDirection } = command;\n assertValidDirection(spritesDirection);\n _console.log({ spritesDirection });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayDirections.indexOf(spritesDirection);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesLineDirection\":\n {\n const { spritesLineDirection } = command;\n assertValidDirection(spritesLineDirection);\n _console.log({ spritesLineDirection });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayDirections.indexOf(spritesLineDirection);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesSpacing\":\n {\n const { spritesSpacing } = command;\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, spritesSpacing, true);\n }\n break;\n case \"setSpritesLineSpacing\":\n {\n const { spritesLineSpacing } = command;\n dataView = new DataView(new ArrayBuffer(2));\n dataView.setInt16(0, spritesLineSpacing, true);\n }\n break;\n case \"setSpritesAlignment\":\n {\n const { spritesAlignment } = command;\n assertValidAlignment(spritesAlignment);\n _console.log({ spritesAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(spritesAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"setSpritesLineAlignment\":\n {\n const { spritesLineAlignment } = command;\n assertValidAlignment(spritesLineAlignment);\n _console.log({ spritesLineAlignment });\n dataView = new DataView(new ArrayBuffer(1));\n const alignmentEnum = DisplayAlignments.indexOf(spritesLineAlignment);\n dataView.setUint8(0, alignmentEnum);\n }\n break;\n case \"clearRect\":\n {\n const { x, y, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, x, true);\n dataView.setInt16(2, y, true);\n dataView.setInt16(4, width, true);\n dataView.setInt16(6, height, true);\n }\n break;\n case \"drawRect\":\n {\n const { offsetX, offsetY, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n }\n break;\n case \"drawRoundRect\":\n {\n const { offsetX, offsetY, width, height, borderRadius } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4 + 1));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n dataView.setUint8(8, borderRadius);\n }\n break;\n case \"drawCircle\":\n {\n const { offsetX, offsetY, radius } = command;\n dataView = new DataView(new ArrayBuffer(2 * 3));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n }\n break;\n case \"drawEllipse\":\n {\n const { offsetX, offsetY, radiusX, radiusY } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radiusX, true);\n dataView.setUint16(6, radiusY, true);\n }\n break;\n case \"drawRegularPolygon\":\n {\n const { offsetX, offsetY, radius, numberOfSides } = command;\n dataView = new DataView(new ArrayBuffer(2 * 3 + 1));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n dataView.setUint8(6, numberOfSides);\n }\n break;\n case \"drawPolygon\":\n {\n const { points } = command;\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n dataView = serializePoints(points);\n }\n break;\n case \"drawWireframe\":\n {\n const { wireframe } = command;\n const { points, edges } = wireframe;\n if (wireframe.points.length == 0) {\n return;\n }\n assertValidWireframe(wireframe);\n // [pointDataType, numberOfPoints, ...points, numberOfEdges, ...edges]\n const pointsDataView = serializePoints(points);\n\n const edgesDataView = new DataView(\n new ArrayBuffer(1 + 2 * edges.length)\n );\n let edgesDataOffset = 0;\n edgesDataView.setUint8(edgesDataOffset++, edges.length);\n edges.forEach((edge) => {\n edgesDataView.setUint8(edgesDataOffset++, edge.startIndex);\n edgesDataView.setUint8(edgesDataOffset++, edge.endIndex);\n });\n\n dataView = new DataView(\n concatenateArrayBuffers(pointsDataView, edgesDataView)\n );\n }\n break;\n case \"drawQuadraticBezierCurve\":\n case \"drawCubicBezierCurve\":\n {\n const { controlPoints } = command;\n const curveType: DisplayBezierCurveType =\n command.type == \"drawCubicBezierCurve\" ? \"cubic\" : \"quadratic\";\n assertValidNumberOfControlPoints(curveType, controlPoints);\n dataView = new DataView(new ArrayBuffer(4 * controlPoints.length));\n let offset = 0;\n controlPoints.forEach((controlPoint) => {\n dataView!.setInt16(offset, controlPoint.x, true);\n offset += 2;\n dataView!.setInt16(offset, controlPoint.y, true);\n offset += 2;\n });\n }\n break;\n case \"drawQuadraticBezierCurves\":\n case \"drawCubicBezierCurves\":\n {\n const { controlPoints } = command;\n const curveType: DisplayBezierCurveType =\n command.type == \"drawCubicBezierCurves\" ? \"cubic\" : \"quadratic\";\n assertValidPathNumberOfControlPoints(curveType, controlPoints);\n dataView = serializePoints(controlPoints);\n }\n break;\n case \"drawPath\":\n case \"drawClosedPath\":\n {\n const { curves } = command;\n // _console.log(\"curves\", curves);\n assertValidPath(curves);\n const typesDataView = new DataView(\n new ArrayBuffer(Math.ceil(curves.length / displayCurveTypesPerByte))\n );\n // _console.log({ \"curves.length\": curves.length, typesDataView });\n const controlPointsDataViews: DataView[] = [];\n\n // [pointDataType, numberOfCurves, numberOfPoints, ...curveTypes, ...points]\n\n const allControlPoints: Vector2[] = [];\n curves.forEach((curve) => {\n allControlPoints.push(...curve.controlPoints);\n });\n const pointDataType = getPointDataType(allControlPoints);\n const numberOfControlPoints = allControlPoints.length;\n _console.log({ numberOfControlPoints });\n\n curves.forEach((curve, index) => {\n const { type, controlPoints } = curve;\n const typeByteIndex = Math.floor(index / displayCurveTypesPerByte);\n const typeBitShift =\n (index % displayCurveTypesPerByte) * displayCurveTypeBitWidth;\n // _console.log({ type, typeByteIndex, typeBitShift });\n let typeValue = typesDataView.getUint8(typeByteIndex) || 0;\n typeValue |= DisplayBezierCurveTypes.indexOf(type) << typeBitShift;\n typesDataView.setUint8(typeByteIndex, typeValue);\n\n const controlPointsDataView = serializePoints(\n controlPoints,\n pointDataType,\n true\n );\n controlPointsDataViews.push(controlPointsDataView);\n });\n\n const controlPointsBuffer = concatenateArrayBuffers(\n ...controlPointsDataViews\n );\n const headerDataView = new DataView(new ArrayBuffer(3));\n headerDataView.setUint8(\n 0,\n DisplayPointDataTypes.indexOf(pointDataType)\n );\n headerDataView.setUint8(1, curves.length);\n headerDataView.setUint8(2, numberOfControlPoints);\n dataView = new DataView(\n concatenateArrayBuffers(\n headerDataView,\n typesDataView,\n controlPointsBuffer\n )\n );\n }\n break;\n case \"drawSegment\":\n {\n const { startX, startY, endX, endY } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, startX, true);\n dataView.setInt16(2, startY, true);\n dataView.setInt16(4, endX, true);\n dataView.setInt16(6, endY, true);\n }\n break;\n case \"drawSegments\":\n {\n const { points } = command;\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n dataView = serializePoints(points);\n }\n break;\n case \"drawArc\":\n {\n let { offsetX, offsetY, radius, isRadians, startAngle, angleOffset } =\n command;\n\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n startAngle = normalizeRadians(startAngle);\n\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n angleOffset = clamp(angleOffset, -twoPi, twoPi);\n\n angleOffset /= twoPi;\n angleOffset *= (angleOffset > 0 ? Int16Max - 1 : -Int16Min) - 1;\n\n isRadians = true;\n\n dataView = new DataView(new ArrayBuffer(2 * 5));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radius, true);\n dataView.setUint16(6, formatRotation(startAngle, isRadians), true);\n dataView.setInt16(8, angleOffset, true);\n }\n break;\n case \"drawArcEllipse\":\n {\n let {\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n isRadians,\n startAngle,\n angleOffset,\n } = command;\n\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n startAngle = normalizeRadians(startAngle);\n\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n angleOffset = clamp(angleOffset, -twoPi, twoPi);\n\n angleOffset /= twoPi;\n angleOffset *= (angleOffset > 0 ? Int16Max : -Int16Min) - 1;\n\n isRadians = true;\n\n dataView = new DataView(new ArrayBuffer(2 * 6));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, radiusX, true);\n dataView.setUint16(6, radiusY, true);\n dataView.setUint16(8, formatRotation(startAngle, isRadians), true);\n dataView.setUint16(10, angleOffset, true);\n }\n break;\n case \"drawBitmap\":\n {\n const { bitmap, offsetX, offsetY } = command;\n displayManager.assertValidBitmap(bitmap, false);\n dataView = new DataView(new ArrayBuffer(drawBitmapHeaderLength));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, bitmap.width, true);\n dataView.setUint32(6, bitmap.pixels.length, true);\n dataView.setUint8(10, bitmap.numberOfColors);\n\n const bitmapData = getBitmapData(bitmap);\n dataView.setUint16(11, bitmapData.byteLength, true);\n const buffer = concatenateArrayBuffers(dataView, bitmapData);\n dataView = new DataView(buffer);\n }\n break;\n case \"selectSpriteSheet\":\n {\n const { spriteSheetIndex } = command;\n dataView = new DataView(new ArrayBuffer(1));\n dataView.setUint8(0, spriteSheetIndex);\n }\n break;\n case \"drawSprite\":\n {\n const { offsetX, offsetY, spriteIndex, use2Bytes } = command;\n dataView = new DataView(new ArrayBuffer(2 * 2 + (use2Bytes ? 2 : 1)));\n let offset = 0;\n dataView.setInt16(offset, offsetX, true);\n offset += 2;\n dataView.setInt16(offset, offsetY, true);\n offset += 2;\n if (use2Bytes) {\n dataView.setUint16(offset, spriteIndex, true);\n offset += 2;\n } else {\n dataView.setUint8(offset++, spriteIndex!);\n }\n }\n break;\n case \"drawSprites\":\n {\n const { offsetX, offsetY, spriteSerializedLines } = command;\n const lineArrayBuffers: ArrayBuffer[] = [];\n spriteSerializedLines.forEach((spriteLines) => {\n const subLineArrayBuffers: ArrayBuffer[] = [];\n spriteLines.forEach((subSpriteLine) => {\n const { spriteSheetIndex, spriteIndices, use2Bytes } =\n subSpriteLine;\n const subLineSpriteIndicesDataView = new DataView(\n new ArrayBuffer(spriteIndices.length * (use2Bytes ? 2 : 1))\n );\n spriteIndices.forEach((spriteIndex, i) => {\n if (use2Bytes) {\n subLineSpriteIndicesDataView.setUint16(\n i * 2,\n spriteIndex,\n true\n );\n } else {\n subLineSpriteIndicesDataView.setUint8(i, spriteIndex);\n }\n });\n const subLineHeaderDataView = new DataView(new ArrayBuffer(2));\n subLineHeaderDataView.setUint8(0, spriteSheetIndex);\n subLineHeaderDataView.setUint8(1, spriteIndices.length);\n subLineArrayBuffers.push(\n concatenateArrayBuffers(\n subLineHeaderDataView,\n subLineSpriteIndicesDataView\n )\n );\n });\n const lineArrayHeaderDataView = new DataView(new ArrayBuffer(2));\n const concatenatedSubLineArrayBuffers = concatenateArrayBuffers(\n ...subLineArrayBuffers\n );\n lineArrayHeaderDataView.setUint16(\n 0,\n concatenatedSubLineArrayBuffers.byteLength,\n true\n );\n lineArrayBuffers.push(\n concatenateArrayBuffers(\n lineArrayHeaderDataView,\n concatenatedSubLineArrayBuffers\n )\n );\n });\n\n const concatenatedLineArrayBuffers = concatenateArrayBuffers(\n ...lineArrayBuffers\n );\n\n dataView = new DataView(new ArrayBuffer(2 * 3));\n let offset = 0;\n dataView.setInt16(offset, offsetX, true);\n offset += 2;\n dataView.setInt16(offset, offsetY, true);\n offset += 2;\n dataView.setUint16(\n offset,\n concatenatedLineArrayBuffers.byteLength,\n true\n );\n offset += 2;\n\n const buffer = concatenateArrayBuffers(\n dataView,\n concatenatedLineArrayBuffers\n );\n dataView = new DataView(buffer);\n }\n break;\n case \"startSprite\":\n {\n const { offsetX, offsetY, width, height } = command;\n dataView = new DataView(new ArrayBuffer(2 * 4));\n dataView.setInt16(0, offsetX, true);\n dataView.setInt16(2, offsetY, true);\n dataView.setUint16(4, width, true);\n dataView.setUint16(6, height, true);\n }\n break;\n }\n\n return dataView;\n}\nexport function serializeContextCommands(\n displayManager: DisplayManagerInterface,\n commands: DisplayContextCommand[]\n) {\n const serializedContextCommandArray = commands\n .filter((command) => !command.hide)\n .map((command) => {\n const displayContextCommandEnum = DisplayContextCommandTypes.indexOf(\n command.type\n );\n const serializedContextCommand = serializeContextCommand(\n displayManager,\n command\n );\n return concatenateArrayBuffers(\n UInt8ByteBuffer(displayContextCommandEnum),\n serializedContextCommand\n );\n });\n const serializedContextCommands = concatenateArrayBuffers(\n serializedContextCommandArray\n );\n _console.log(\n \"serializedContextCommands\",\n commands,\n serializedContextCommandArray,\n serializedContextCommands\n );\n return serializedContextCommands;\n}\n\nconst DrawDisplayContextCommandTypes = [\n \"drawRect\",\n \"drawRoundRect\",\n\n \"drawCircle\",\n \"drawArc\",\n\n \"drawEllipse\",\n \"drawArcEllipse\",\n\n \"drawSegment\",\n \"drawSegments\",\n\n \"drawRegularPolygon\",\n \"drawPolygon\",\n\n \"drawWireframe\",\n\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n\n \"drawPath\",\n \"drawClosedPath\",\n\n \"drawBitmap\",\n\n \"drawSprite\",\n \"drawSprites\",\n] as const satisfies readonly DisplayContextCommandType[];\ntype DrawDisplayContextCommandType =\n (typeof DrawDisplayContextCommandTypes)[number];\n\nconst StateDisplayContextCommandTypes = [\n \"setColor\",\n \"setColorOpacity\",\n \"setOpacity\",\n\n \"saveContext\",\n \"restoreContext\",\n\n \"selectBackgroundColor\",\n \"selectFillColor\",\n \"selectLineColor\",\n\n \"setIgnoreFill\",\n \"setIgnoreLine\",\n \"setFillBackground\",\n\n \"setLineWidth\",\n \"setRotation\",\n \"clearRotation\",\n\n \"setHorizontalAlignment\",\n \"setVerticalAlignment\",\n \"resetAlignment\",\n\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n \"setSegmentCap\",\n\n \"setSegmentStartRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentRadius\",\n\n \"setCropTop\",\n \"setCropRight\",\n \"setCropBottom\",\n \"setCropLeft\",\n \"clearCrop\",\n\n \"setRotationCropTop\",\n \"setRotationCropRight\",\n \"setRotationCropBottom\",\n \"setRotationCropLeft\",\n \"clearRotationCrop\",\n\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"selectSpriteSheet\",\n] as const satisfies readonly DisplayContextCommandType[];\ntype StateDisplayContextCommandType =\n (typeof StateDisplayContextCommandTypes)[number];\n\nconst SpritesDisplayContextCommandTypes = [\n \"selectSpriteColor\",\n \"selectSpriteColors\",\n \"resetSpriteColors\",\n \"setSpriteScaleX\",\n \"setSpriteScaleY\",\n \"setSpriteScale\",\n \"resetSpriteScale\",\n\n \"setSpritesLineHeight\",\n \"setSpritesDirection\",\n \"setSpritesLineDirection\",\n \"setSpritesSpacing\",\n \"setSpritesLineSpacing\",\n \"setSpritesAlignment\",\n \"setSpritesLineAlignment\",\n\n \"selectSpriteSheet\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type SpritesDisplayContextCommandType =\n (typeof SpritesDisplayContextCommandTypes)[number];\n\nconst PathDrawDisplayContextCommandTypes = [\n \"drawSegment\",\n \"drawSegments\",\n \"drawQuadraticBezierCurve\",\n \"drawQuadraticBezierCurves\",\n \"drawCubicBezierCurve\",\n \"drawCubicBezierCurves\",\n \"drawPath\",\n \"drawWireframe\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type PathDrawDisplayContextCommandType =\n (typeof PathDrawDisplayContextCommandTypes)[number];\n\nconst PathStateDisplayContextCommandTypes = [\n \"setSegmentRadius\",\n \"setSegmentEndRadius\",\n \"setSegmentStartRadius\",\n \"setSegmentCap\",\n \"setSegmentStartCap\",\n \"setSegmentEndCap\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type PathStateDisplayContextCommandType =\n (typeof PathStateDisplayContextCommandTypes)[number];\n\nconst BitmapDisplayContextCommandTypes = [\n \"selectBitmapColor\",\n \"selectBitmapColors\",\n \"setBitmapScaleX\",\n \"setBitmapScaleY\",\n \"setBitmapScale\",\n \"resetBitmapScale\",\n] as const satisfies readonly DisplayContextCommandType[];\nexport type BitmapDisplayContextCommandType =\n (typeof BitmapDisplayContextCommandTypes)[number];\n\nconst contextCommandDependencies: Map<\n Set<DisplayContextCommandType>,\n Set<DisplayContextCommandType>\n> = new Map();\nfunction appendContextCommandDependencyPair(\n key: DisplayContextCommandType[],\n value: DisplayContextCommandType[]\n) {\n contextCommandDependencies.set(new Set(key), new Set(value));\n}\nappendContextCommandDependencyPair(\n [...PathStateDisplayContextCommandTypes],\n [...PathDrawDisplayContextCommandTypes]\n);\nappendContextCommandDependencyPair(\n [...StateDisplayContextCommandTypes],\n [...DrawDisplayContextCommandTypes]\n);\nappendContextCommandDependencyPair(\n [...SpritesDisplayContextCommandTypes],\n [\"drawSprite\", \"drawSprites\"]\n);\nappendContextCommandDependencyPair(\n [...BitmapDisplayContextCommandTypes],\n [\"drawBitmap\"]\n);\n\n// TODO - can refine more (e.g. if ignoreLine, then skip setLineWidth, or skip if a set value is already default, etc)\n\nexport function trimContextCommands(commands: DisplayContextCommand[]) {\n _console.log(\"trimming commands\", commands);\n const trimmedCommands: DisplayContextCommand[] = [];\n\n commands\n .slice()\n .reverse()\n .forEach((command) => {\n let include = true;\n\n let dependencies: Set<DisplayContextCommandType> | undefined;\n for (const [keys, values] of contextCommandDependencies) {\n if (keys.has(command.type)) {\n dependencies = values;\n break;\n }\n }\n\n //_console.log(\"command\", command, \"dependencies\", dependencies);\n\n if (dependencies) {\n const similarCommandIndex = trimmedCommands.findIndex(\n (trimmedCommand) => {\n return trimmedCommand.type == command.type;\n }\n );\n const dependentCommandIndex = trimmedCommands.findIndex(\n (trimmedCommand) => dependencies.has(trimmedCommand.type)\n );\n\n //_console.log({ similarCommandIndex, dependentCommandIndex });\n\n if (dependentCommandIndex == -1) {\n include = false;\n } else if (similarCommandIndex != -1) {\n include = similarCommandIndex > dependentCommandIndex;\n }\n }\n if (include) {\n trimmedCommands.unshift(command);\n } else {\n //_console.log(\"skipping command\", command);\n }\n });\n\n _console.log(\"trimmedCommands\", trimmedCommands);\n return trimmedCommands;\n}\n","import { createConsole } from \"./Console.ts\";\nimport { Vector2 } from \"./MathUtils.ts\";\nimport { DisplayBezierCurve } from \"../DisplayManager.ts\";\nimport simplify from \"simplify-js\";\nimport fitCurve from \"fit-curve\";\n\nconst _console = createConsole(\"PathUtils\", { log: false });\n\nfunction perpendicularDistance(p: Vector2, p1: Vector2, p2: Vector2): number {\n const dx = p2.x - p1.x;\n const dy = p2.y - p1.y;\n if (dx === 0 && dy === 0) return Math.hypot(p.x - p1.x, p.y - p1.y);\n const t = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);\n const projX = p1.x + t * dx;\n const projY = p1.y + t * dy;\n return Math.hypot(p.x - projX, p.y - projY);\n}\n\nfunction rdp(points: Vector2[], epsilon: number): Vector2[] {\n if (points.length < 3) return points;\n let maxDist = 0;\n let index = 0;\n for (let i = 1; i < points.length - 1; i++) {\n const d = perpendicularDistance(\n points[i],\n points[0],\n points[points.length - 1]\n );\n if (d > maxDist) {\n maxDist = d;\n index = i;\n }\n }\n if (maxDist > epsilon) {\n const left = rdp(points.slice(0, index + 1), epsilon);\n const right = rdp(points.slice(index), epsilon);\n return left.slice(0, -1).concat(right);\n }\n return [points[0], points[points.length - 1]];\n}\n\n// Linear interpolation\nfunction lerp(a: number, b: number, t: number) {\n return a + (b - a) * t;\n}\n\n// Sample quadratic Bezier\nfunction sampleQuadratic(\n p0: Vector2,\n p1: Vector2,\n p2: Vector2,\n steps: number = 5\n): Vector2[] {\n const points: Vector2[] = [];\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n const x = (1 - t) ** 2 * p0.x + 2 * (1 - t) * t * p1.x + t ** 2 * p2.x;\n const y = (1 - t) ** 2 * p0.y + 2 * (1 - t) * t * p1.y + t ** 2 * p2.y;\n points.push({ x, y });\n }\n return points;\n}\n\n// Sample cubic Bezier\nfunction sampleCubic(\n p0: Vector2,\n p1: Vector2,\n p2: Vector2,\n p3: Vector2,\n steps: number = 5\n): Vector2[] {\n const points: Vector2[] = [];\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n const mt = 1 - t;\n const x =\n mt ** 3 * p0.x +\n 3 * mt ** 2 * t * p1.x +\n 3 * mt * t ** 2 * p2.x +\n t ** 3 * p3.x;\n const y =\n mt ** 3 * p0.y +\n 3 * mt ** 2 * t * p1.y +\n 3 * mt * t ** 2 * p2.y +\n t ** 3 * p3.y;\n points.push({ x, y });\n }\n return points;\n}\n\nfunction areCollinear(\n p1: Vector2,\n p2: Vector2,\n p3: Vector2,\n epsilon = 1e-6\n): boolean {\n // Vector p1->p2\n const dx1 = p2.x - p1.x;\n const dy1 = p2.y - p1.y;\n\n // Vector p2->p3\n const dx2 = p3.x - p2.x;\n const dy2 = p3.y - p2.y;\n\n // Cross product\n const cross = dx1 * dy2 - dy1 * dx2;\n return Math.abs(cross) < epsilon;\n}\n\nexport function simplifyCurves(curves: DisplayBezierCurve[], epsilon = 1) {\n const simplified: DisplayBezierCurve[] = [];\n //_console.log(\"simplifying\", curves, { epsilon });\n let cursor: Vector2;\n curves.forEach((curve, index) => {\n const { controlPoints } = curve;\n const isFirst = index == 0;\n if (isFirst) {\n cursor = controlPoints[0];\n }\n\n switch (curve.type) {\n case \"segment\":\n {\n // Merge collinear lines\n const lastPoint = controlPoints.at(-1)!;\n const lastCommand = simplified.at(-1);\n if (lastCommand?.type == \"segment\" && simplified.length >= 2) {\n const [c1, c2] = [simplified.at(-1)!, simplified.at(-2)!];\n if (\n areCollinear(\n c2.controlPoints.at(-1)!,\n c1.controlPoints.at(-1)!,\n lastPoint\n )\n ) {\n // Remove middle collinear point\n simplified.pop();\n }\n }\n simplified.push({ ...curve });\n cursor = lastPoint;\n }\n break;\n case \"quadratic\":\n {\n const p0 = cursor;\n const p1 = controlPoints.at(-2)!;\n const p2 = controlPoints.at(-1)!;\n\n // Sample points along the curve\n const sampled = sampleQuadratic(p0, p1, p2, 5);\n const simplifiedPoints = rdp(sampled, epsilon);\n\n // If curve is almost straight, convert to a line\n if (simplifiedPoints.length === 2) {\n simplified.push({\n type: \"segment\",\n controlPoints: [{ x: p2.x, y: p2.y }],\n });\n if (isFirst) {\n simplified.at(-1)!.controlPoints.unshift({ ...p0 });\n }\n } else {\n simplified.push({ ...curve }); // Keep the curve\n }\n cursor = p2;\n }\n break;\n case \"cubic\":\n {\n const p0 = cursor;\n const p1 = controlPoints.at(-3)!;\n const p2 = controlPoints.at(-2)!;\n const p3 = controlPoints.at(-1)!;\n\n const sampled = sampleCubic(p0, p1, p2, p3, 5);\n const simplifiedPoints = rdp(sampled, epsilon);\n\n if (simplifiedPoints.length === 2) {\n simplified.push({\n type: \"segment\",\n controlPoints: [{ x: p3.x, y: p3.y }],\n });\n if (isFirst) {\n simplified.at(-1)!.controlPoints.unshift({ ...p0 });\n }\n } else {\n simplified.push({ ...curve }); // Keep the curve\n }\n cursor = p3;\n }\n break;\n }\n cursor = curve.controlPoints[curve.controlPoints.length - 1];\n });\n //_console.log(\"simplified\", simplified);\n return simplified;\n}\n\nexport function simplifyPoints(points: Vector2[], tolerance?: number) {\n points = simplify(points, tolerance, false);\n return points;\n}\nexport function simplifyPointsAsCubicCurveControlPoints(\n points: Vector2[],\n error?: number\n) {\n const flatPoints = points.map(({ x, y }) => [x, y]);\n const curves = fitCurve(flatPoints, error ?? 50);\n const controlPoints: Vector2[] = [];\n curves.forEach((curve, index) => {\n const points = curve.map(([x, y]) => ({ x, y }));\n if (index != 0) {\n points.shift();\n }\n controlPoints.push(...points);\n });\n return controlPoints;\n}\n","import { createConsole } from \"./Console.ts\";\nimport {\n DisplayContextCommand,\n trimContextCommands,\n} from \"./DisplayContextCommand.ts\";\nimport { INode, parseSync } from \"svgson\";\nimport { SVGPathData } from \"svg-pathdata\";\nimport { DisplayBezierCurve, DisplaySize } from \"../DisplayManager.ts\";\nimport { pointInPolygon, Vector2 } from \"./MathUtils.ts\";\nimport {\n contourArea,\n DisplaySprite,\n DisplaySpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\nimport { simplifyCurves } from \"./PathUtils.ts\";\nimport { DisplayBoundingBox } from \"./DisplayCanvasHelper.ts\";\nimport RangeHelper from \"./RangeHelper.ts\";\nimport { kMeansColors, mapToClosestPaletteIndex } from \"./ColorUtils.ts\";\n\nconst _console = createConsole(\"SvgUtils\", { log: false });\n\ntype FillRule = \"nonzero\" | \"evenodd\";\ntype CanvasCommand =\n | { type: \"lineWidth\"; lineWidth: number }\n | { type: \"fillStyle\"; fillStyle: string }\n | { type: \"strokeStyle\"; strokeStyle: string }\n | { type: \"fillRule\"; fillRule: FillRule }\n | { type: \"pathStart\" | \"pathEnd\" }\n | { type: \"moveTo\" | \"lineTo\"; x: number; y: number }\n | { type: \"line\"; x1: number; y1: number; x2: number; y2: number }\n | { type: \"quadraticCurveTo\"; cpx: number; cpy: number; x: number; y: number }\n | {\n type: \"bezierCurveTo\";\n cp1x: number;\n cp1y: number;\n cp2x: number;\n cp2y: number;\n x: number;\n y: number;\n }\n | { type: \"closePath\"; checkIfHole?: boolean }\n | {\n type: \"rect\";\n x: number;\n y: number;\n width: number;\n height: number;\n rotation: number;\n }\n | {\n type: \"roundRect\";\n x: number;\n y: number;\n width: number;\n height: number;\n r: number;\n rotation: number;\n }\n | { type: \"circle\"; x: number; y: number; r: number }\n | {\n type: \"ellipse\";\n x: number;\n y: number;\n rx: number;\n ry: number;\n rotation: number;\n };\n\ninterface Transform {\n a: number;\n b: number;\n c: number;\n d: number;\n e: number;\n f: number;\n}\n\ninterface DecomposedTransform {\n translation: { x: number; y: number };\n rotation: number; // in radians\n scale: { x: number; y: number };\n skew: { x: number; y: number }; // skewX/Y in radians\n isScaleUniform: boolean; // true if scaleX ≈ scaleY\n}\n\n/** Fully decompose a 2D affine transform */\nfunction decomposeTransform(\n t: Transform,\n tolerance = 1e-6\n): DecomposedTransform {\n // Translation\n const tx = t.e;\n const ty = t.f;\n\n // Compute scale\n const scaleX = Math.sqrt(t.a * t.a + t.b * t.b);\n const scaleY = Math.sqrt(t.c * t.c + t.d * t.d);\n\n // Compute rotation (from X-axis)\n let rotation = 0;\n if (scaleX !== 0) {\n rotation = Math.atan2(t.b / scaleX, t.a / scaleX);\n }\n\n // Compute skew (skewX = angle between x and y axes)\n let skewX = 0;\n let skewY = 0;\n if (scaleX !== 0 && scaleY !== 0) {\n skewX = Math.atan2(t.a * t.c + t.b * t.d, scaleX * scaleX);\n skewY = 0; // rarely needed, can be calculated similarly if desired\n }\n\n // Uniform scale check\n const isScaleUniform = Math.abs(scaleX - scaleY) < tolerance;\n\n return {\n translation: { x: tx, y: ty },\n rotation,\n scale: { x: scaleX, y: scaleY },\n skew: { x: skewX, y: skewY },\n isScaleUniform,\n };\n}\n\nconst identity: Transform = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };\n\nfunction multiply(t1: Transform, t2: Transform): Transform {\n //_console.log(\"multiplying matrices\", t1, t2);\n return {\n a: t1.a * t2.a + t1.c * t2.b,\n b: t1.b * t2.a + t1.d * t2.b,\n c: t1.a * t2.c + t1.c * t2.d,\n d: t1.b * t2.c + t1.d * t2.d,\n e: t1.a * t2.e + t1.c * t2.f + t1.e,\n f: t1.b * t2.e + t1.d * t2.f + t1.f,\n };\n}\n\nfunction parseTransform(transformStr: string): Transform {\n // Very basic parser, handles translate, scale, rotate, matrix\n if (!transformStr) return identity;\n\n const t = transformStr.match(/(\\w+)\\(([^)]+)\\)/g);\n if (!t) return identity;\n\n let matrix = structuredClone(identity);\n\n for (const part of t) {\n const [, fn, argsStr] = /(\\w+)\\(([^)]+)\\)/.exec(part)!;\n const args = argsStr.split(/[\\s,]+/).map(Number);\n let m: Transform = structuredClone(identity);\n\n switch (fn) {\n case \"translate\":\n //_console.log(\"translate\", { x: args[0], y: args[1] });\n m.e = args[0];\n m.f = args[1] || 0;\n break;\n case \"scale\":\n //_console.log(\"scale\", { x: args[0], y: args[1] });\n m.a = args[0];\n m.d = args[1] !== undefined ? args[1] : args[0];\n break;\n case \"rotate\":\n const angle = (args[0] * Math.PI) / 180;\n //_console.log(\"rotate\", { angle });\n const cos = Math.cos(angle),\n sin = Math.sin(angle);\n if (args[1] !== undefined && args[2] !== undefined) {\n const [cx, cy] = [args[1], args[2]];\n m = {\n a: cos,\n b: sin,\n c: -sin,\n d: cos,\n e: cx - cos * cx + sin * cy,\n f: cy - sin * cx - cos * cy,\n };\n } else {\n m.a = cos;\n m.b = sin;\n m.c = -sin;\n m.d = cos;\n }\n break;\n case \"matrix\":\n //_console.log(\"matrix\", args);\n [m.a, m.b, m.c, m.d, m.e, m.f] = args;\n break;\n }\n\n matrix = multiply(matrix, m);\n }\n\n //_console.log(\"parsedTransform\", matrix);\n return matrix;\n}\n\nfunction applyTransform(x: number, y: number, t: Transform) {\n //_console.log(\"applying transform\", { x, y, t });\n const value: Vector2 = {\n x: t.a * x + t.c * y + t.e,\n y: t.b * x + t.d * y + t.f,\n };\n //_console.log(\"transformed value\", value);\n return value;\n}\nfunction parseStyle(styleStr: string | undefined): Record<string, string> {\n const style: Record<string, string> = {};\n if (!styleStr) return style;\n\n styleStr.split(\";\").forEach((item) => {\n const [key, value] = item.split(\":\").map((s) => s.trim());\n if (key && value) style[key] = value;\n });\n return style;\n}\n\nconst circleBezierConstant = 0.5522847498307936;\nfunction svgJsonToCanvasCommands(svgJson: INode): CanvasCommand[] {\n const commands: CanvasCommand[] = [];\n\n function traverse(node: any, parentTransform: Transform) {\n //_console.log(\"traversing node\", node, parentTransform);\n const transform = parseTransform(node.attributes.transform);\n //_console.log(\"transform\", transform);\n const nodeTransform = multiply(parentTransform, transform);\n //_console.log(\"nodeTransform\", nodeTransform);\n\n const { scale, translation, rotation, isScaleUniform } =\n decomposeTransform(nodeTransform);\n //_console.log({ scale, translation, rotation, isScaleUniform });\n const uniformScale = scale.x;\n\n // Handle styles\n const style = parseStyle(node.attributes.style);\n // Fill\n if (style.fill) commands.push({ type: \"fillStyle\", fillStyle: style.fill });\n if (node.attributes.fill)\n commands.push({ type: \"fillStyle\", fillStyle: node.attributes.fill });\n\n // Stroke\n if (style.stroke)\n commands.push({ type: \"strokeStyle\", strokeStyle: style.stroke });\n if (node.attributes.stroke)\n commands.push({\n type: \"strokeStyle\",\n strokeStyle: node.attributes.stroke,\n });\n\n // Stroke width\n let strokeWidth = 0;\n if (style[\"stroke-width\"])\n strokeWidth = parseLength(style[\"stroke-width\"]) ?? 0;\n if (node.attributes[\"stroke-width\"])\n strokeWidth = parseLength(node.attributes[\"stroke-width\"]) ?? strokeWidth;\n if (strokeWidth)\n commands.push({\n type: \"lineWidth\",\n lineWidth: strokeWidth * nodeTransform.a, // scale to pixels\n });\n\n // Fill rule\n let fillRule = style[\"fill-rule\"];\n if (node.attributes[\"fill-rule\"]) fillRule = node.attributes[\"fill-rule\"];\n if (fillRule)\n commands.push({ type: \"fillRule\", fillRule: fillRule as FillRule });\n\n switch (node.name) {\n case \"path\":\n const d = node.attributes.d;\n if (!d) break;\n const pathData = new SVGPathData(d)\n .toAbs()\n .aToC()\n .normalizeHVZ(false)\n .normalizeST()\n .removeCollinear()\n .sanitize();\n //_console.log(\"pathData\", d, pathData);\n commands.push({ type: \"pathStart\" });\n for (const cmd of pathData.commands) {\n switch (cmd.type) {\n case SVGPathData.MOVE_TO:\n commands.push({ type: \"closePath\" });\n const m = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({ type: \"moveTo\", x: m.x, y: m.y });\n break;\n\n case SVGPathData.LINE_TO:\n const l = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({ type: \"lineTo\", x: l.x, y: l.y });\n break;\n case SVGPathData.CURVE_TO:\n const c1 = applyTransform(cmd.x1!, cmd.y1!, nodeTransform);\n const c2 = applyTransform(cmd.x2!, cmd.y2!, nodeTransform);\n const ce = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: c1.x,\n cp1y: c1.y,\n cp2x: c2.x,\n cp2y: c2.y,\n x: ce.x,\n y: ce.y,\n });\n break;\n case SVGPathData.QUAD_TO:\n const qcp = applyTransform(cmd.x1!, cmd.y1!, nodeTransform);\n const qe = applyTransform(cmd.x!, cmd.y!, nodeTransform);\n commands.push({\n type: \"quadraticCurveTo\",\n cpx: qcp.x,\n cpy: qcp.y,\n x: qe.x,\n y: qe.y,\n });\n break;\n case SVGPathData.CLOSE_PATH:\n commands.push({ type: \"closePath\" });\n break;\n default:\n _console.warn(\"uncaught command\", cmd);\n break;\n }\n }\n if (commands.at(-1)?.type != \"closePath\") {\n commands.push({ type: \"closePath\" });\n }\n commands.push({ type: \"pathEnd\" });\n\n break;\n\n case \"rect\": {\n const x = parseFloat(node.attributes.x || \"0\");\n const y = parseFloat(node.attributes.y || \"0\");\n const width = parseFloat(node.attributes.width || \"0\");\n const height = parseFloat(node.attributes.height || \"0\");\n\n let rx = parseFloat(node.attributes.rx || \"0\");\n let ry = parseFloat(node.attributes.ry || \"0\");\n if (!node.attributes.ry && rx) ry = rx;\n\n rx = Math.min(rx, width / 2);\n ry = Math.min(ry, height / 2);\n\n if (rx === 0 && ry === 0) {\n // sharp rect\n if (isScaleUniform) {\n const center = applyTransform(\n x + width / 2,\n y + height / 2,\n nodeTransform\n );\n commands.push({\n type: \"rect\",\n x: center.x,\n y: center.y,\n width: width * uniformScale,\n height: height * uniformScale,\n rotation,\n });\n } else {\n const tl = applyTransform(x, y, nodeTransform);\n const tr = applyTransform(x + width, y, nodeTransform);\n const br = applyTransform(x + width, y + height, nodeTransform);\n const bl = applyTransform(x, y + height, nodeTransform);\n\n commands.push({ type: \"moveTo\", x: tl.x, y: tl.y });\n commands.push({ type: \"lineTo\", x: tr.x, y: tr.y });\n commands.push({ type: \"lineTo\", x: br.x, y: br.y });\n commands.push({ type: \"lineTo\", x: bl.x, y: bl.y });\n commands.push({ type: \"closePath\" });\n }\n } else {\n // rounded rect\n if (rx == ry && isScaleUniform) {\n const center = applyTransform(\n x + width / 2,\n y + height / 2,\n nodeTransform\n );\n commands.push({\n type: \"roundRect\",\n x: center.x,\n y: center.y,\n width: width * uniformScale,\n height: height * uniformScale,\n rotation,\n r: rx * uniformScale,\n });\n } else {\n const ox = rx * circleBezierConstant; // x offset for control points\n const oy = ry * circleBezierConstant; // y offset for control points\n\n // Corners before transform\n const p1 = { x: x + rx, y: y };\n const p2 = { x: x + width - rx, y: y };\n const p3 = { x: x + width, y: y + ry };\n const p4 = { x: x + width, y: y + height - ry };\n const p5 = { x: x + width - rx, y: y + height };\n const p6 = { x: x + rx, y: y + height };\n const p7 = { x: x, y: y + height - ry };\n const p8 = { x: x, y: y + ry };\n\n // Move to start\n const start = applyTransform(p1.x, p1.y, nodeTransform);\n commands.push({ type: \"moveTo\", x: start.x, y: start.y });\n\n // Top edge + top-right corner\n let cp1 = applyTransform(p2.x + ox, p2.y, nodeTransform);\n let cp2 = applyTransform(p3.x, p3.y - oy, nodeTransform);\n let end = applyTransform(p3.x, p3.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p2.x, p2.y, nodeTransform).x,\n y: applyTransform(p2.x, p2.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Right edge + bottom-right corner\n cp1 = applyTransform(p4.x, p4.y + oy, nodeTransform);\n cp2 = applyTransform(p5.x + ox, p5.y, nodeTransform);\n end = applyTransform(p5.x, p5.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p4.x, p4.y, nodeTransform).x,\n y: applyTransform(p4.x, p4.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Bottom edge + bottom-left corner\n cp1 = applyTransform(p6.x - ox, p6.y, nodeTransform);\n cp2 = applyTransform(p7.x, p7.y + oy, nodeTransform);\n end = applyTransform(p7.x, p7.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p6.x, p6.y, nodeTransform).x,\n y: applyTransform(p6.x, p6.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n // Left edge + top-left corner\n cp1 = applyTransform(p8.x, p8.y - oy, nodeTransform);\n cp2 = applyTransform(p1.x - ox, p1.y, nodeTransform);\n end = applyTransform(p1.x, p1.y, nodeTransform);\n commands.push({\n type: \"lineTo\",\n x: applyTransform(p8.x, p8.y, nodeTransform).x,\n y: applyTransform(p8.x, p8.y, nodeTransform).y,\n });\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cp1.x,\n cp1y: cp1.y,\n cp2x: cp2.x,\n cp2y: cp2.y,\n x: end.x,\n y: end.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n }\n break;\n }\n\n case \"circle\": {\n const cx = parseFloat(node.attributes.cx || \"0\");\n const cy = parseFloat(node.attributes.cy || \"0\");\n const r = parseFloat(node.attributes.r || \"0\");\n\n if (r === 0) break;\n\n if (isScaleUniform) {\n //_console.log({ cx, cy, r, uniformScale });\n const center = applyTransform(cx, cy, nodeTransform);\n commands.push({\n type: \"circle\",\n x: center.x,\n y: center.y,\n r: r * uniformScale,\n });\n } else {\n const ox = r * circleBezierConstant; // control point offset\n\n // Points around the circle\n const pTop = applyTransform(cx, cy - r, nodeTransform);\n const pRight = applyTransform(cx + r, cy, nodeTransform);\n const pBottom = applyTransform(cx, cy + r, nodeTransform);\n const pLeft = applyTransform(cx - r, cy, nodeTransform);\n //_console.log({ pTop, pRight, pBottom, pLeft });\n\n const cpTopRight = applyTransform(cx + ox, cy - r, nodeTransform);\n const cpRightTop = applyTransform(cx + r, cy - ox, nodeTransform);\n\n const cpRightBottom = applyTransform(cx + r, cy + ox, nodeTransform);\n const cpBottomRight = applyTransform(cx + ox, cy + r, nodeTransform);\n\n const cpBottomLeft = applyTransform(cx - ox, cy + r, nodeTransform);\n const cpLeftBottom = applyTransform(cx - r, cy + ox, nodeTransform);\n\n const cpLeftTop = applyTransform(cx - r, cy - ox, nodeTransform);\n const cpTopLeft = applyTransform(cx - ox, cy - r, nodeTransform);\n\n commands.push({ type: \"moveTo\", x: pTop.x, y: pTop.y });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpTopRight.x,\n cp1y: cpTopRight.y,\n cp2x: cpRightTop.x,\n cp2y: cpRightTop.y,\n x: pRight.x,\n y: pRight.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpRightBottom.x,\n cp1y: cpRightBottom.y,\n cp2x: cpBottomRight.x,\n cp2y: cpBottomRight.y,\n x: pBottom.x,\n y: pBottom.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpBottomLeft.x,\n cp1y: cpBottomLeft.y,\n cp2x: cpLeftBottom.x,\n cp2y: cpLeftBottom.y,\n x: pLeft.x,\n y: pLeft.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpLeftTop.x,\n cp1y: cpLeftTop.y,\n cp2x: cpTopLeft.x,\n cp2y: cpTopLeft.y,\n x: pTop.x,\n y: pTop.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n break;\n }\n\n case \"ellipse\": {\n const cx = parseFloat(node.attributes.cx || \"0\");\n const cy = parseFloat(node.attributes.cy || \"0\");\n const rx = parseFloat(node.attributes.rx || \"0\");\n const ry = parseFloat(node.attributes.ry || \"0\");\n\n if (rx === 0 || ry === 0) break;\n\n if (isScaleUniform) {\n const center = applyTransform(cx, cy, nodeTransform);\n if (rx == ry) {\n commands.push({\n type: \"circle\",\n x: center.x,\n y: center.y,\n r: rx * uniformScale,\n });\n } else {\n commands.push({\n type: \"ellipse\",\n x: center.x,\n y: center.y,\n rx: rx * uniformScale,\n ry: ry * uniformScale,\n rotation,\n });\n }\n } else {\n const ox = rx * circleBezierConstant;\n const oy = ry * circleBezierConstant;\n\n // Key points\n const pTop = applyTransform(cx, cy - ry, nodeTransform);\n const pRight = applyTransform(cx + rx, cy, nodeTransform);\n const pBottom = applyTransform(cx, cy + ry, nodeTransform);\n const pLeft = applyTransform(cx - rx, cy, nodeTransform);\n\n // Control points\n const cpTopRight = applyTransform(cx + ox, cy - ry, nodeTransform);\n const cpRightTop = applyTransform(cx + rx, cy - oy, nodeTransform);\n\n const cpRightBottom = applyTransform(cx + rx, cy + oy, nodeTransform);\n const cpBottomRight = applyTransform(cx + ox, cy + ry, nodeTransform);\n\n const cpBottomLeft = applyTransform(cx - ox, cy + ry, nodeTransform);\n const cpLeftBottom = applyTransform(cx - rx, cy + oy, nodeTransform);\n\n const cpLeftTop = applyTransform(cx - rx, cy - oy, nodeTransform);\n const cpTopLeft = applyTransform(cx - ox, cy - ry, nodeTransform);\n\n // Draw ellipse using cubic Beziers\n commands.push({ type: \"moveTo\", x: pTop.x, y: pTop.y });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpTopRight.x,\n cp1y: cpTopRight.y,\n cp2x: cpRightTop.x,\n cp2y: cpRightTop.y,\n x: pRight.x,\n y: pRight.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpRightBottom.x,\n cp1y: cpRightBottom.y,\n cp2x: cpBottomRight.x,\n cp2y: cpBottomRight.y,\n x: pBottom.x,\n y: pBottom.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpBottomLeft.x,\n cp1y: cpBottomLeft.y,\n cp2x: cpLeftBottom.x,\n cp2y: cpLeftBottom.y,\n x: pLeft.x,\n y: pLeft.y,\n });\n\n commands.push({\n type: \"bezierCurveTo\",\n cp1x: cpLeftTop.x,\n cp1y: cpLeftTop.y,\n cp2x: cpTopLeft.x,\n cp2y: cpTopLeft.y,\n x: pTop.x,\n y: pTop.y,\n });\n\n commands.push({ type: \"closePath\" });\n }\n break;\n }\n\n case \"polyline\":\n case \"polygon\": {\n const pointsStr: string = node.attributes.points || \"\";\n const points: { x: number; y: number }[] = pointsStr\n .trim()\n .split(/[\\s,]+/)\n .map(Number)\n .reduce<{ x?: number; y?: number }[]>((acc, val, idx) => {\n if (idx % 2 === 0) acc.push({ x: val, y: 0 });\n else acc[acc.length - 1].y = val;\n return acc;\n }, [])\n .map((p) => ({ x: p.x!, y: p.y! }));\n\n if (points.length === 0) break;\n\n // Move to first point\n const start = applyTransform(points[0].x, points[0].y, nodeTransform);\n commands.push({ type: \"moveTo\", x: start.x, y: start.y });\n\n // Draw lines to remaining points\n for (let i = 1; i < points.length; i++) {\n const p = applyTransform(points[i].x, points[i].y, nodeTransform);\n commands.push({ type: \"lineTo\", x: p.x, y: p.y });\n }\n\n // close path, even if polyline\n commands.push({ type: \"closePath\" });\n break;\n }\n\n case \"line\": {\n const x1 = parseFloat(node.attributes.x1 || \"0\");\n const y1 = parseFloat(node.attributes.y1 || \"0\");\n const x2 = parseFloat(node.attributes.x2 || \"0\");\n const y2 = parseFloat(node.attributes.y2 || \"0\");\n\n const p1 = applyTransform(x1, y1, nodeTransform);\n const p2 = applyTransform(x2, y2, nodeTransform);\n\n commands.push({ type: \"line\", x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y });\n\n break;\n }\n case \"svg\":\n break;\n default:\n _console.log(\"uncaught node\", node);\n break;\n }\n\n if (node.children) {\n for (const child of node.children) traverse(child, nodeTransform);\n }\n }\n\n traverse(svgJson, getSvgTransformToPixels(svgJson));\n return commands;\n}\n\nfunction parseLength(\n str: string | undefined,\n relativeTo?: number\n): number | undefined {\n if (!str) return undefined;\n const match = /^([0-9.]+)([a-z%]*)$/.exec(str.trim());\n if (!match) return undefined;\n\n const value = parseFloat(match[1]);\n const unit = match[2] || \"px\";\n\n switch (unit) {\n case \"px\":\n return value;\n case \"pt\":\n return value * (96 / 72); // 1pt = 1/72in, 96dpi\n case \"in\":\n return value * 96; // 1in = 96px\n case \"cm\":\n return value * (96 / 2.54); // 1cm = 96/2.54 px\n case \"mm\":\n return value * (96 / 25.4); // 1mm = 96/25.4 px\n case \"%\":\n if (relativeTo === undefined) return undefined;\n return (value / 100) * relativeTo;\n case \"\":\n return value; // unitless → px\n default:\n return value; // unknown unit → assume px\n }\n}\n\nfunction getSvgJsonSize(svgJson: INode) {\n const attrs = svgJson.attributes || {};\n let width = parseLength(attrs.width);\n let height = parseLength(attrs.height);\n\n // Fallback to viewBox dimensions\n if ((width == null || height == null) && attrs.viewBox) {\n const [, , vbWidth, vbHeight] = attrs.viewBox\n .split(/[\\s,]+/)\n .map(parseFloat);\n width ??= vbWidth;\n height ??= vbHeight;\n }\n\n const size: DisplaySize = {\n width: width ?? 300,\n height: height ?? 150,\n };\n //_console.log(\"size\", size);\n return size;\n}\n\nfunction getSvgJsonViewBox(svgJson: INode): DisplayBoundingBox {\n const attrs = svgJson.attributes || {};\n let x = 0,\n y = 0,\n width: number | undefined,\n height: number | undefined;\n\n if (attrs.viewBox) {\n [x, y, width, height] = attrs.viewBox.split(/[\\s,]+/).map(parseFloat);\n }\n\n // Fallback to size if no viewBox\n if (width == null || height == null) {\n const size = getSvgJsonSize(svgJson);\n width ??= size.width;\n height ??= size.height;\n }\n\n const viewBox: DisplayBoundingBox = {\n x,\n y,\n width: width!,\n height: height!,\n };\n //_console.log(\"viewBox\", viewBox);\n return viewBox;\n}\n\nfunction getSvgJsonBoundingBox(svgJson: INode): DisplayBoundingBox {\n const { width, height } = getSvgJsonSize(svgJson);\n const viewBox = getSvgJsonViewBox(svgJson);\n\n if (width !== undefined && height !== undefined) {\n return { x: 0, y: 0, width, height };\n } else if (viewBox.width !== undefined && viewBox.height !== undefined) {\n return viewBox;\n } else {\n return { x: 0, y: 0, width: 300, height: 150 };\n }\n}\n\nfunction getSvgTransformToPixels(svgJson: INode): Transform {\n const attrs = svgJson.attributes || {};\n const { width, height } = getSvgJsonSize(svgJson); // in px\n const viewBox = getSvgJsonViewBox(svgJson); // { x, y, width, height }\n\n //_console.log({ width, height, viewBox });\n\n // Base scales\n let scaleX = width / viewBox.width;\n let scaleY = height / viewBox.height;\n let offsetX = 0;\n let offsetY = 0;\n\n // Handle preserveAspectRatio=\"xMidYMid meet\"\n if (attrs.preserveAspectRatio?.includes(\"meet\")) {\n const s = Math.min(scaleX, scaleY);\n offsetX = (width - viewBox.width * s) / 2;\n offsetY = (height - viewBox.height * s) / 2;\n scaleX = scaleY = s;\n }\n\n // Return the affine transform matrix\n return {\n a: scaleX,\n b: 0,\n c: 0,\n d: scaleY,\n e: -viewBox.x * scaleX + offsetX,\n f: -viewBox.y * scaleY + offsetY,\n };\n}\n\nexport type ParseSvgOptions = {\n fit?: boolean; // removes extra empty space around the shapes\n width?: number; // scale output to this width\n height?: number; // scale output to this height\n aspectRatio?: number; // width / height, used if only one of width/height is provided\n offsetX?: number;\n offsetY?: number;\n centered?: boolean;\n};\nconst defaultParseSvgOptions: ParseSvgOptions = {\n fit: false,\n centered: true,\n};\n\nfunction transformCanvasCommands(\n canvasCommands: CanvasCommand[],\n xCallback: (x: number) => number,\n yCallback: (y: number) => number,\n type: \"offset\" | \"scale\"\n): CanvasCommand[] {\n return canvasCommands.map((command) => {\n switch (command.type) {\n case \"moveTo\":\n case \"lineTo\": {\n let { x, y } = command;\n x = xCallback(x);\n y = yCallback(y);\n return { type: command.type, x, y };\n break;\n }\n case \"quadraticCurveTo\": {\n let { x, y, cpx, cpy } = command;\n x = xCallback(x);\n y = yCallback(y);\n cpx = xCallback(cpx);\n cpy = yCallback(cpy);\n return { type: command.type, x, y, cpx, cpy };\n break;\n }\n case \"bezierCurveTo\": {\n let { x, y, cp1x, cp1y, cp2x, cp2y } = command;\n x = xCallback(x);\n y = yCallback(y);\n cp1x = xCallback(cp1x);\n cp1y = yCallback(cp1y);\n cp2x = xCallback(cp2x);\n cp2y = yCallback(cp2y);\n return { type: command.type, x, y, cp1x, cp1y, cp2x, cp2y };\n break;\n }\n case \"lineWidth\": {\n if (type == \"scale\") {\n let { lineWidth } = command;\n lineWidth = xCallback(lineWidth);\n return { type: command.type, lineWidth };\n }\n break;\n }\n case \"rect\":\n case \"roundRect\": {\n let { x, y, width, height, rotation } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n width = xCallback(width);\n height = yCallback(height);\n }\n if (command.type == \"roundRect\") {\n let { r } = command;\n if (type == \"scale\") {\n r = xCallback(r);\n }\n return { type: command.type, x, y, width, height, rotation, r };\n }\n return { type: command.type, x, y, width, height, rotation };\n break;\n }\n case \"circle\":\n {\n let { x, y, r } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n r = xCallback(r);\n }\n return { type: command.type, x, y, r };\n }\n break;\n case \"ellipse\":\n {\n let { x, y, rx, ry, rotation } = command;\n x = xCallback(x);\n y = yCallback(y);\n if (type == \"scale\") {\n rx = xCallback(rx);\n ry = xCallback(ry);\n }\n return { type: command.type, x, y, rx, ry, rotation };\n }\n break;\n default:\n return command;\n }\n return command;\n });\n}\nfunction forEachCanvasCommandVector2(\n canvasCommands: CanvasCommand[],\n vectorCallback: (x: number, y: number) => void\n) {\n canvasCommands.forEach((command) => {\n switch (command.type) {\n case \"moveTo\":\n case \"lineTo\":\n {\n let { x, y } = command;\n vectorCallback(x, y);\n }\n break;\n case \"quadraticCurveTo\":\n {\n let { x, y, cpx, cpy } = command;\n vectorCallback(x, y);\n vectorCallback(cpx, cpy);\n }\n break;\n case \"bezierCurveTo\": {\n let { x, y, cp1x, cp1y, cp2x, cp2y } = command;\n vectorCallback(x, y);\n vectorCallback(cp1x, cp1y);\n vectorCallback(cp2x, cp2y);\n }\n default:\n break;\n }\n });\n}\nfunction offsetCanvasCommands(\n canvasCommands: CanvasCommand[],\n offsetX = 0,\n offsetY = 0\n) {\n return transformCanvasCommands(\n canvasCommands,\n (x) => x + offsetX,\n (y) => y + offsetY,\n \"offset\"\n );\n}\nfunction scaleCanvasCommands(\n canvasCommands: CanvasCommand[],\n scaleX: number,\n scaleY: number\n) {\n return transformCanvasCommands(\n canvasCommands,\n (x) => x * scaleX,\n (y) => y * scaleY,\n \"scale\"\n );\n}\n\nfunction getBoundingBox(path: Vector2[]) {\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const p of path) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n return { minX, minY, maxX, maxY };\n}\n\nfunction bboxContains(\n a: ReturnType<typeof getBoundingBox>,\n b: ReturnType<typeof getBoundingBox>\n) {\n return (\n a.minX <= b.minX && a.minY <= b.minY && a.maxX >= b.maxX && a.maxY >= b.maxY\n );\n}\n\nexport function classifySubpath(\n subpath: Vector2[],\n previous: { path: Vector2[]; isHole: boolean }[],\n fillRule: FillRule\n): boolean {\n const centroid = subpath.reduce(\n (acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),\n { x: 0, y: 0 }\n );\n centroid.x /= subpath.length;\n centroid.y /= subpath.length;\n\n const subBBox = getBoundingBox(subpath);\n\n let insideCount = 0;\n\n for (const other of previous) {\n const otherBBox = getBoundingBox(other.path);\n\n // must be fully inside bbox\n if (!bboxContains(otherBBox, subBBox)) continue;\n\n // require *most* points to be inside\n const insidePoints = subpath.filter((p) =>\n pointInPolygon(p, other.path)\n ).length;\n const allInside = insidePoints > subpath.length * 0.8;\n if (!allInside) continue;\n\n insideCount++;\n }\n\n if (fillRule === \"evenodd\") {\n return insideCount % 2 === 1; // odd count = hole\n } else {\n // non-zero winding rule\n let winding = 0;\n for (const other of previous) {\n const otherBBox = getBoundingBox(other.path);\n if (!bboxContains(otherBBox, subBBox)) continue;\n if (pointInPolygon(centroid, other.path)) {\n winding += contourArea(other.path) > 0 ? 1 : -1;\n }\n }\n return winding !== 0; // nonzero = inside → hole\n }\n}\n\nexport function svgToDisplayContextCommands(\n svgString: string,\n numberOfColors: number,\n paletteOffset: number,\n colors?: string[],\n options?: ParseSvgOptions\n) {\n _console.assertWithError(\n numberOfColors > 1,\n \"numberOfColors must be greater than 1\"\n );\n options = { ...defaultParseSvgOptions, ...options };\n _console.log(\"options\", options);\n\n const svgJson = parseSync(svgString);\n\n let canvasCommands = svgJsonToCanvasCommands(svgJson);\n _console.log(\"canvasCommands\", canvasCommands);\n\n const boundingBox = getSvgJsonBoundingBox(svgJson);\n //_console.log(\"boundingBox\", boundingBox);\n\n let intrinsicWidth = boundingBox.width;\n let intrinsicHeight = boundingBox.height;\n\n _console.log({ intrinsicWidth, intrinsicHeight });\n\n let scaleX = 1,\n scaleY = 1;\n if (options.width && options.height) {\n scaleX = options.width / intrinsicWidth;\n scaleY = options.height / intrinsicHeight;\n } else if (options.width) {\n scaleX = scaleY = options.width / intrinsicWidth;\n if (options.aspectRatio) scaleY = scaleX / options.aspectRatio;\n } else if (options.height) {\n scaleX = scaleY = options.height / intrinsicHeight;\n if (options.aspectRatio) scaleX = scaleY * options.aspectRatio;\n }\n\n _console.log({ scaleX, scaleY });\n\n let width = intrinsicWidth * scaleX;\n let height = intrinsicWidth * scaleX;\n\n _console.log({ width, height });\n\n if (scaleX !== 1 || scaleY !== 1) {\n canvasCommands = scaleCanvasCommands(canvasCommands, scaleX, scaleY);\n }\n\n if (options.fit) {\n const rangeHelper = {\n x: new RangeHelper(),\n y: new RangeHelper(),\n };\n forEachCanvasCommandVector2(canvasCommands, (x, y) => {\n rangeHelper.x.update(x);\n rangeHelper.y.update(y);\n });\n\n // _console.log(\"xRange\", rangeHelper.x.min, rangeHelper.x.max);\n // _console.log(\"yRange\", rangeHelper.y.min, rangeHelper.y.max);\n\n width = rangeHelper.x.span;\n height = rangeHelper.y.span;\n\n const offsetX = -rangeHelper.x.min;\n const offsetY = -rangeHelper.y.min;\n\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n if (options.offsetX || options.offsetY) {\n const offsetX = options.offsetX || 0;\n const offsetY = options.offsetY || 0;\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n if (options.centered) {\n const offsetX = -width / 2;\n const offsetY = -height / 2;\n canvasCommands = offsetCanvasCommands(canvasCommands, offsetX, offsetY);\n }\n\n let svgColors: string[] = [];\n canvasCommands.forEach((canvasCommand) => {\n let color: string | undefined;\n switch (canvasCommand.type) {\n case \"fillStyle\":\n color = canvasCommand.fillStyle;\n break;\n case \"strokeStyle\":\n color = canvasCommand.strokeStyle;\n break;\n default:\n return;\n }\n if (color && color != \"none\" && !svgColors.includes(color)) {\n svgColors.push(color);\n }\n });\n if (svgColors.length == 0) {\n svgColors.push(\"black\");\n }\n if (svgColors.length == 1) {\n svgColors.push(\"white\");\n }\n _console.log(\"colors\", svgColors);\n\n const colorToIndex: Record<string, number> = {};\n if (colors) {\n colors = colors.slice(0, numberOfColors);\n const mapping = mapToClosestPaletteIndex(svgColors, colors.slice(1));\n _console.log(\"mapping\", mapping, colors);\n svgColors.forEach((color) => {\n colorToIndex[color] = mapping[color] + 1;\n });\n } else {\n // FIX - annoying when an svg has a black fill\n const { palette, mapping } = kMeansColors(svgColors, numberOfColors);\n _console.log(\"mapping\", mapping);\n _console.log(\"palette\", palette);\n\n svgColors.forEach((color) => {\n colorToIndex[color] = mapping[color];\n });\n colors = palette;\n }\n _console.log(\"colorToIndex\", colorToIndex);\n\n _console.log(\"transformed canvasCommands\", canvasCommands);\n\n let curves: DisplayBezierCurve[] = [];\n let startPoint: Vector2 = { x: 0, y: 0 };\n let fillRule: FillRule = \"nonzero\";\n let fillStyle: string | undefined;\n let strokeStyle = \"none\";\n let lineWidth = 1;\n let segmentRadius = 1;\n let wasHole = false;\n let ignoreFill = false;\n let ignoreLine = true;\n let fillColorIndex = 1;\n let lineColorIndex = 1;\n const getFillColorIndex = () => fillColorIndex + paletteOffset;\n const getLineColorIndex = () => lineColorIndex + paletteOffset;\n let isDrawingPath = false;\n const parsedPaths: { path: Vector2[]; isHole: boolean }[] = [];\n\n let displayCommands: DisplayContextCommand[] = [];\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"selectLineColor\",\n lineColorIndex: getLineColorIndex(),\n });\n displayCommands.push({ type: \"setIgnoreLine\", ignoreLine: true });\n displayCommands.push({ type: \"setLineWidth\", lineWidth });\n displayCommands.push({\n type: \"setSegmentRadius\",\n segmentRadius,\n });\n\n canvasCommands.forEach((canvasCommand) => {\n switch (canvasCommand.type) {\n case \"moveTo\":\n {\n const { x, y } = canvasCommand;\n startPoint.x = x;\n startPoint.y = y;\n }\n break;\n case \"lineTo\":\n {\n const { x, y } = canvasCommand;\n const controlPoints: Vector2[] = [{ x, y }];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"segment\", controlPoints });\n }\n break;\n case \"quadraticCurveTo\":\n {\n const { x, y, cpx, cpy } = canvasCommand;\n const controlPoints: Vector2[] = [\n { x: cpx, y: cpy },\n { x, y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"quadratic\", controlPoints });\n }\n break;\n case \"bezierCurveTo\":\n {\n const { x, y, cp1x, cp1y, cp2x, cp2y } = canvasCommand;\n const controlPoints: Vector2[] = [\n { x: cp1x, y: cp1y },\n { x: cp2x, y: cp2y },\n { x, y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"cubic\", controlPoints });\n }\n break;\n case \"closePath\":\n if (curves.length === 0) break;\n\n curves = simplifyCurves(curves);\n\n // Flatten all control points\n const controlPoints = curves.flatMap((c) => c.controlPoints);\n\n if (isDrawingPath) {\n const isHole = classifySubpath(controlPoints, parsedPaths, fillRule);\n parsedPaths.push({ path: controlPoints, isHole });\n\n // _console.log({\n // pathIndex: parsedPaths.length - 1,\n // isHole,\n // fillStyle,\n // strokeStyle,\n // fillRule,\n // lineWidth,\n // });\n\n if (isHole != wasHole) {\n wasHole = isHole;\n if (isHole) {\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: 0,\n });\n } else {\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n }\n }\n }\n\n if (ignoreFill) {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth: 0,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getLineColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill: false,\n });\n }\n\n const isSegments = curves.every((c) => c.type === \"segment\");\n if (isSegments) {\n if (ignoreFill) {\n displayCommands.push({\n type: \"drawSegments\",\n points: controlPoints,\n });\n } else {\n displayCommands.push({\n type: \"drawPolygon\",\n points: controlPoints,\n });\n }\n } else {\n if (ignoreFill) {\n displayCommands.push({ type: \"drawPath\", curves });\n } else {\n displayCommands.push({ type: \"drawClosedPath\", curves });\n }\n }\n\n if (ignoreFill) {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill,\n });\n }\n\n // Reset curves\n curves = [];\n break;\n case \"pathStart\":\n parsedPaths.length = 0;\n if (wasHole) {\n displayCommands.push({ type: \"selectFillColor\", fillColorIndex });\n }\n wasHole = false;\n isDrawingPath = true;\n break;\n case \"pathEnd\":\n isDrawingPath = false;\n break;\n case \"line\":\n if (strokeStyle != \"none\") {\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth: 0,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getLineColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill: false,\n });\n\n const { x1, y1, x2, y2 } = canvasCommand;\n displayCommands.push({\n type: \"drawSegment\",\n startX: x1,\n startY: y1,\n endX: x2,\n endY: y2,\n });\n\n displayCommands.push({\n type: \"setLineWidth\",\n lineWidth,\n });\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n displayCommands.push({\n type: \"setIgnoreFill\",\n ignoreFill,\n });\n }\n\n break;\n case \"fillStyle\":\n _console.log(\"fillStyle\", canvasCommand.fillStyle);\n if (fillStyle != canvasCommand.fillStyle) {\n const newIgnoreFill = canvasCommand.fillStyle == \"none\";\n if (ignoreFill != newIgnoreFill) {\n ignoreFill = newIgnoreFill;\n _console.log({ ignoreFill });\n displayCommands.push({ type: \"setIgnoreFill\", ignoreFill });\n }\n if (!ignoreFill) {\n if (fillStyle != canvasCommand.fillStyle) {\n fillStyle = canvasCommand.fillStyle;\n if (fillColorIndex != colorToIndex[fillStyle]) {\n _console.log({ fillColorIndex });\n fillColorIndex = colorToIndex[fillStyle];\n displayCommands.push({\n type: \"selectFillColor\",\n fillColorIndex: getFillColorIndex(),\n });\n }\n }\n }\n }\n break;\n case \"strokeStyle\":\n _console.log(\"strokeStyle\", canvasCommand.strokeStyle);\n if (strokeStyle != canvasCommand.strokeStyle) {\n const newIgnoreLine = canvasCommand.strokeStyle == \"none\";\n if (ignoreLine != newIgnoreLine) {\n ignoreLine = newIgnoreLine;\n _console.log({ ignoreLine });\n displayCommands.push({ type: \"setIgnoreLine\", ignoreLine });\n }\n if (!ignoreLine) {\n if (strokeStyle != canvasCommand.strokeStyle) {\n strokeStyle = canvasCommand.strokeStyle;\n if (lineColorIndex != colorToIndex[strokeStyle]) {\n _console.log({ lineColorIndex });\n lineColorIndex = colorToIndex[strokeStyle];\n displayCommands.push({\n type: \"selectLineColor\",\n lineColorIndex: getLineColorIndex(),\n });\n }\n }\n }\n }\n break;\n case \"lineWidth\":\n if (lineWidth != canvasCommand.lineWidth) {\n lineWidth = canvasCommand.lineWidth;\n displayCommands.push({ type: \"setLineWidth\", lineWidth });\n segmentRadius = lineWidth / 2;\n displayCommands.push({\n type: \"setSegmentRadius\",\n segmentRadius,\n });\n }\n break;\n case \"fillRule\":\n fillRule = canvasCommand.fillRule;\n break;\n case \"rect\":\n {\n const { x, y, width, height, rotation } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawRect\",\n offsetX: x,\n offsetY: y,\n width: width,\n height: height,\n });\n }\n break;\n case \"roundRect\":\n {\n const { x, y, width, height, rotation, r } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawRoundRect\",\n offsetX: x,\n offsetY: y,\n width: width,\n height: height,\n borderRadius: r,\n });\n }\n break;\n case \"circle\":\n {\n const { x, y, r } = canvasCommand;\n displayCommands.push({\n type: \"drawCircle\",\n offsetX: x,\n offsetY: y,\n radius: r,\n });\n }\n break;\n case \"ellipse\":\n {\n const { x, y, rx, ry, rotation } = canvasCommand;\n displayCommands.push({\n type: \"setRotation\",\n rotation,\n isRadians: true,\n });\n displayCommands.push({\n type: \"drawEllipse\",\n offsetX: x,\n offsetY: y,\n radiusX: rx,\n radiusY: ry,\n });\n }\n break;\n default:\n _console.warn(\"uncaught canvasCommand\", canvasCommand);\n break;\n }\n });\n\n displayCommands = trimContextCommands(displayCommands);\n\n _console.log(\"displayCommands\", displayCommands);\n _console.log(\"colors\", colors);\n return { commands: displayCommands, colors, width, height };\n}\n\nexport function svgToSprite(\n svgString: string,\n spriteName: string,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0,\n options?: ParseSvgOptions\n) {\n options = { ...defaultParseSvgOptions, ...options };\n _console.log(\"options\", options, { overridePalette });\n\n let palette = spriteSheet.palettes?.find(\n (palette) => palette.name == paletteName\n );\n if (!palette) {\n palette = {\n name: paletteName,\n numberOfColors,\n colors: new Array(numberOfColors).fill(\"#000000\"),\n };\n spriteSheet.palettes = spriteSheet.palettes || [];\n spriteSheet.palettes?.push(palette);\n }\n _console.log(\"pallete\", palette);\n\n const { commands, colors, width, height } = svgToDisplayContextCommands(\n svgString,\n numberOfColors,\n paletteOffset,\n !overridePalette ? palette.colors : undefined,\n options\n );\n\n const sprite: DisplaySprite = {\n name: spriteName,\n width,\n height,\n paletteSwaps: [],\n commands,\n };\n if (overridePalette) {\n _console.log(\"overriding palette\", colors);\n colors.forEach((color, index) => {\n palette.colors[index + paletteOffset] = color;\n });\n }\n\n const spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n if (spriteIndex == -1) {\n spriteSheet.sprites.push(sprite);\n } else {\n _console.log(`overwriting spriteInde ${spriteIndex}`);\n spriteSheet.sprites[spriteIndex] = sprite;\n }\n\n return sprite;\n}\n\nexport function svgToSpriteSheet(\n svgString: string,\n spriteSheetName: string,\n numberOfColors: number,\n paletteName: string,\n options?: ParseSvgOptions\n) {\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n palettes: [],\n paletteSwaps: [],\n sprites: [],\n };\n\n svgToSprite(\n svgString,\n \"svg\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet,\n 0,\n options\n );\n\n return spriteSheet;\n}\n\nexport function getSvgStringFromDataUrl(string: string) {\n if (!string.startsWith(\"data:image/svg+xml\"))\n throw new Error(\"Not a data URL\");\n\n // Data URL might be base64 or URI encoded\n const data = string.split(\",\")[1];\n if (string.includes(\"base64\")) {\n return atob(data);\n } else {\n return decodeURIComponent(data);\n }\n}\n\nexport function isValidSVG(svgString: string) {\n if (typeof svgString !== \"string\") return false;\n const parser = new DOMParser();\n const doc = parser.parseFromString(svgString, \"image/svg+xml\");\n\n // Different browsers may put parser errors in different places; check several ways:\n if (\n doc.querySelector(\"parsererror\") ||\n doc.getElementsByTagName(\"parsererror\").length > 0\n ) {\n return false;\n }\n\n const root = doc.documentElement;\n return (\n !!root &&\n root.nodeName.toLowerCase() === \"svg\" &&\n root.namespaceURI === \"http://www.w3.org/2000/svg\"\n );\n}\n","import { removeRedundancies } from \"./ObjectUtils.ts\";\n\nexport function spacesToPascalCase(string: string) {\n return string\n .replace(/(?:^\\w|\\b\\w)/g, function (match) {\n return match.toUpperCase();\n })\n .replace(/\\s+/g, \"\");\n}\n\nexport function capitalizeFirstCharacter(string: string) {\n return string[0].toUpperCase() + string.slice(1);\n}\n\nexport function removeRedundantCharacters(string: string) {\n return removeRedundancies(Array.from(string)).join(\"\");\n}\n\nexport function removeSubstrings(string: string, substrings: string[]): string {\n let result = string;\n for (const sub of substrings) {\n result = result.split(sub).join(\"\");\n }\n return result;\n}\n","import {\n DisplayBezierCurve,\n DisplayBitmap,\n DisplaySize,\n} from \"../DisplayManager.ts\";\nimport { concatenateArrayBuffers } from \"./ArrayBufferUtils.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { quantizeCanvas } from \"./DisplayBitmapUtils.ts\";\nimport {\n DisplayContextCommand,\n serializeContextCommands,\n} from \"./DisplayContextCommand.ts\";\nimport { DisplayManagerInterface } from \"./DisplayManagerInterface.ts\";\nimport opentype, { Glyph, Font } from \"opentype.js\";\nimport { decompress } from \"woff2-encoder\";\nimport RangeHelper from \"./RangeHelper.ts\";\nimport { Vector2 } from \"./MathUtils.ts\";\nimport { simplifyCurves } from \"./PathUtils.ts\";\nimport {\n DisplayContextState,\n isDirectionHorizontal,\n} from \"./DisplayContextState.ts\";\nimport { classifySubpath } from \"./SvgUtils.ts\";\nimport { removeRedundantCharacters, removeSubstrings } from \"./stringUtils.ts\";\n\nconst _console = createConsole(\"DisplaySpriteSheetUtils\", { log: false });\n\nexport type DisplaySpriteSubLine = {\n spriteSheetName: string;\n spriteNames: string[];\n};\nexport type DisplaySpriteLine = DisplaySpriteSubLine[];\nexport type DisplaySpriteLines = DisplaySpriteLine[];\n\nexport type DisplaySpriteSerializedSubLine = {\n spriteSheetIndex: number;\n spriteIndices: number[];\n use2Bytes: boolean;\n};\nexport type DisplaySpriteSerializedLine = DisplaySpriteSerializedSubLine[];\nexport type DisplaySpriteSerializedLines = DisplaySpriteSerializedLine[];\n\nexport type DisplaySpritePaletteSwap = {\n name: string;\n numberOfColors: number;\n spriteColorIndices: number[];\n};\nexport type DisplaySprite = {\n name: string;\n width: number;\n height: number;\n paletteSwaps?: DisplaySpritePaletteSwap[];\n commands: DisplayContextCommand[];\n};\nexport type DisplaySpriteSheetPaletteSwap = {\n name: string;\n numberOfColors: number;\n spriteColorIndices: number[];\n};\nexport type DisplaySpriteSheetPalette = {\n name: string;\n numberOfColors: number;\n colors: string[];\n opacities?: number[];\n};\nexport type DisplaySpriteSheet = {\n name: string;\n palettes?: DisplaySpriteSheetPalette[];\n paletteSwaps?: DisplaySpriteSheetPaletteSwap[];\n sprites: DisplaySprite[];\n};\n\nexport const spriteHeaderLength = 3 * 2; // width, height, commandsLength\nexport function calculateSpriteSheetHeaderLength(numberOfSprites: number) {\n // numberOfSprites, spriteOffsets, spriteHeader\n return 2 + numberOfSprites * 2 + numberOfSprites * spriteHeaderLength;\n}\nexport function getCurvesPoints(curves: DisplayBezierCurve[]) {\n const curvePoints: Vector2[] = [];\n curves.forEach((curve, index) => {\n if (index == 0) {\n curvePoints.push(curve.controlPoints[0]);\n }\n curvePoints.push(curve.controlPoints.at(-1)!);\n });\n return curvePoints;\n}\nexport function serializeSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheet: DisplaySpriteSheet\n) {\n const { name, sprites } = spriteSheet;\n _console.log(`serializing ${name} spriteSheet`, spriteSheet);\n\n const numberOfSprites = sprites.length;\n const numberOfSpritesDataView = new DataView(new ArrayBuffer(2));\n numberOfSpritesDataView.setUint16(0, numberOfSprites, true);\n\n const spritePayloads = sprites.map((sprite, index) => {\n const commandsData = serializeContextCommands(\n displayManager,\n sprite.commands\n );\n const dataView = new DataView(new ArrayBuffer(spriteHeaderLength));\n dataView.setUint16(0, sprite.width, true);\n dataView.setUint16(2, sprite.height, true);\n dataView.setUint16(4, commandsData.byteLength, true);\n const serializedSprite = concatenateArrayBuffers(dataView, commandsData);\n _console.log(\"serializedSprite\", sprite, serializedSprite);\n return serializedSprite;\n });\n const spriteOffsetsDataView = new DataView(\n new ArrayBuffer(sprites.length * 2)\n );\n let offset =\n numberOfSpritesDataView.byteLength + spriteOffsetsDataView.byteLength;\n spritePayloads.forEach((spritePayload, index) => {\n //_console.log(\"spritePayloads\", index, offset, spritePayload);\n spriteOffsetsDataView.setUint16(index * 2, offset, true);\n offset += spritePayload.byteLength;\n });\n\n // [numberOfSprites, ...spriteOffsets, ...[width, height, commands]]\n const serializedSpriteSheet = concatenateArrayBuffers(\n numberOfSpritesDataView,\n spriteOffsetsDataView,\n spritePayloads\n );\n _console.log(\"serializedSpriteSheet\", serializedSpriteSheet);\n\n return serializedSpriteSheet;\n}\n\nexport function parseSpriteSheet(dataView: DataView) {\n // FILL\n}\n\nexport type FontToSpriteSheetOptions = {\n stroke?: boolean;\n strokeWidth?: number;\n unicodeOnly?: boolean;\n englishOnly?: boolean;\n usePath?: boolean;\n script?: string;\n string?: string;\n minSpriteY?: number;\n maxSpriteY?: number;\n maxSpriteheight?: number;\n};\nexport const defaultFontToSpriteSheetOptions: FontToSpriteSheetOptions = {\n stroke: false,\n strokeWidth: 1,\n unicodeOnly: true,\n englishOnly: true,\n usePath: false,\n};\n\nfunction isWoff2(arrayBuffer: ArrayBuffer) {\n if (arrayBuffer.byteLength < 4) return false;\n\n const header = new Uint8Array(arrayBuffer, 0, 4);\n return (\n header[0] === 0x77 && // 'w'\n header[1] === 0x4f && // 'O'\n header[2] === 0x46 && // 'F'\n header[3] === 0x32 // '2'\n );\n}\nexport async function parseFont(arrayBuffer: ArrayBuffer) {\n if (isWoff2(arrayBuffer)) {\n const result = await decompress(arrayBuffer);\n // @ts-expect-error\n arrayBuffer = result.buffer;\n }\n const font = opentype.parse(arrayBuffer);\n //_console.log(\"font\", font);\n return font;\n}\n\nexport function getFontUnicodeRange(font: Font) {\n const rangeHelper = new RangeHelper();\n\n for (let i = 0; i < font.glyphs.length; i++) {\n const glyph = font.glyphs.get(i);\n if (!glyph.unicodes || glyph.unicodes.length === 0) continue;\n\n glyph.unicodes\n .filter((unicode) => {\n const char = String.fromCodePoint(unicode);\n // Keep only letters (any language)\n return /\\p{Letter}/u.test(char);\n })\n .forEach((unicode) => rangeHelper.update(unicode));\n }\n\n //_console.log(\"range\", rangeHelper.range);\n return rangeHelper.span > 0 ? rangeHelper.range : undefined;\n}\n\nexport const englishRegex = /^[A-Za-z0-9 !\"#$%&'()*+,\\-./:;?@[\\]^_`{|}~\\\\]+$/;\n\nexport function contourArea(points: Vector2[]) {\n let area = 0;\n for (let i = 0, j = points.length - 1; i < points.length; j = i++) {\n area += (points[j].x - points[i].x) * (points[j].y + points[i].y);\n }\n return area;\n}\n\nexport function getFontMetrics(\n font: Font | Font[],\n fontSize: number,\n options?: FontToSpriteSheetOptions\n) {\n _console.assertTypeWithError(fontSize, \"number\");\n\n options = options\n ? { ...defaultFontToSpriteSheetOptions, ...options }\n : defaultFontToSpriteSheetOptions;\n\n const fonts = Array.isArray(font) ? font : [font];\n\n let minSpriteY = Infinity;\n let maxSpriteY = -Infinity;\n\n const strokeWidth = options.stroke ? options.strokeWidth || 1 : 0;\n\n let string = options.string;\n if (string) {\n string = removeRedundantCharacters(string);\n console.log(\"filtered string\", string);\n }\n\n for (let font of fonts) {\n const fontScale = (1 / font.unitsPerEm) * fontSize;\n\n const glyphs: Glyph[] = [];\n let filteredGlyphs: Glyph[] | undefined;\n if (string != undefined) {\n filteredGlyphs = font\n .stringToGlyphs(string)\n .filter((glyph) => glyph.unicode != undefined);\n string = removeSubstrings(\n string,\n filteredGlyphs.map((glyph) => String.fromCharCode(glyph.unicode!))\n );\n }\n\n for (let index = 0; index < font.glyphs.length; index++) {\n const glyph = font.glyphs.get(index);\n const hasUnicode = glyph.unicode != undefined;\n if (hasUnicode) {\n //_console.log(String.fromCharCode(glyph.unicode!), glyph);\n } else {\n //_console.log(\"no unicode\", glyph);\n }\n\n if (filteredGlyphs) {\n if (!filteredGlyphs.includes(glyph)) {\n continue;\n }\n }\n\n if (options.unicodeOnly || options.englishOnly) {\n if (!hasUnicode) {\n continue;\n }\n }\n if (options.script && hasUnicode) {\n const regex = new RegExp(`\\\\p{Script=${options.script}}`, \"u\");\n if (!regex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n if (options.englishOnly) {\n if (!englishRegex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n\n const bbox = glyph.getBoundingBox();\n minSpriteY = Math.min(minSpriteY, bbox.y1 * fontScale);\n maxSpriteY = Math.max(maxSpriteY, bbox.y2 * fontScale);\n\n glyphs.push(glyph);\n }\n\n // _console.log({\n // fontName: font.getEnglishName(\"fullName\"),\n // minSpriteY,\n // maxSpriteY,\n // });\n }\n\n minSpriteY = options.minSpriteY ?? minSpriteY;\n maxSpriteY = options.maxSpriteY ?? maxSpriteY;\n\n const maxSpriteHeight =\n options.maxSpriteheight ?? maxSpriteY - minSpriteY + strokeWidth;\n return { maxSpriteHeight, maxSpriteY, minSpriteY };\n}\n\nexport async function fontToSpriteSheet(\n font: Font | Font[],\n fontSize: number,\n spriteSheetName?: string,\n options?: FontToSpriteSheetOptions\n) {\n _console.assertTypeWithError(fontSize, \"number\");\n\n options = options\n ? { ...defaultFontToSpriteSheetOptions, ...options }\n : defaultFontToSpriteSheetOptions;\n\n const fonts = Array.isArray(font) ? font : [font];\n font = fonts[0];\n spriteSheetName = spriteSheetName || font.getEnglishName(\"fullName\");\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n sprites: [],\n };\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\")!;\n\n const { maxSpriteHeight, maxSpriteY, minSpriteY } = getFontMetrics(\n fonts,\n fontSize,\n options\n );\n const strokeWidth = options.stroke ? options.strokeWidth || 1 : 0;\n\n let string = options.string;\n if (string) {\n string = removeRedundantCharacters(string);\n _console.log(\"filtered string\", string);\n }\n\n for (let font of fonts) {\n const fontScale = (1 / font.unitsPerEm) * fontSize;\n\n const glyphs: Glyph[] = [];\n let filteredGlyphs: Glyph[] | undefined;\n if (string != undefined) {\n filteredGlyphs = font\n .stringToGlyphs(string)\n .filter((glyph) => glyph.unicode != undefined);\n string = removeSubstrings(\n string,\n filteredGlyphs.map((glyph) => String.fromCharCode(glyph.unicode!))\n );\n //_console.log(\"filteredString\", string);\n //_console.log(\"filteredGlyphs\", filteredGlyphs);\n }\n\n for (let index = 0; index < font.glyphs.length; index++) {\n const glyph = font.glyphs.get(index);\n const hasUnicode = glyph.unicode != undefined;\n if (hasUnicode) {\n //_console.log(String.fromCharCode(glyph.unicode!), glyph);\n } else {\n //_console.log(\"no unicode\", glyph);\n }\n\n if (filteredGlyphs) {\n if (!filteredGlyphs.includes(glyph)) {\n continue;\n }\n }\n\n if (options.unicodeOnly || options.englishOnly) {\n if (!hasUnicode) {\n continue;\n }\n }\n if (options.script && hasUnicode) {\n const regex = new RegExp(`\\\\p{Script=${options.script}}`, \"u\");\n if (!regex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n if (options.englishOnly) {\n if (!englishRegex.test(String.fromCharCode(glyph.unicode!))) {\n continue;\n }\n }\n\n glyphs.push(glyph);\n }\n\n for (let i = 0; i < glyphs.length; i++) {\n const glyph = glyphs[i];\n\n let name = glyph.name;\n if (glyph.unicode != undefined) {\n name = String.fromCharCode(glyph.unicode);\n }\n //_console.log(name, glyph);\n if (typeof name != \"string\") {\n continue;\n }\n\n const bbox = glyph.getBoundingBox();\n\n const spriteWidth =\n Math.max(\n Math.max(bbox.x2, bbox.x2 - bbox.x1),\n glyph.advanceWidth || 0\n ) *\n fontScale +\n strokeWidth;\n const spriteHeight = maxSpriteHeight;\n\n const commands: DisplayContextCommand[] = [];\n\n const path = glyph.getPath(\n -bbox.x1 * fontScale,\n bbox.y2 * fontScale,\n fontSize\n );\n if (options.stroke) {\n path.stroke = \"white\";\n path.strokeWidth = strokeWidth;\n commands.push({ type: \"setLineWidth\", lineWidth: strokeWidth });\n commands.push({ type: \"setIgnoreFill\", ignoreFill: true });\n } else {\n path.fill = \"white\";\n }\n\n const bitmapWidth = (bbox.x2 - bbox.x1) * fontScale + strokeWidth;\n const bitmapHeight = (bbox.y2 - bbox.y1) * fontScale + strokeWidth;\n\n const bitmapX = (spriteWidth - bitmapWidth) / 2;\n const bitmapY =\n (spriteHeight - bitmapHeight) / 2 - (bbox.y1 * fontScale - minSpriteY);\n if (options.usePath) {\n const pathOffset: Vector2 = {\n x: -bitmapWidth / 2 + bitmapX,\n y: -bitmapHeight / 2 + bitmapY,\n };\n //_console.log(`${name} path.commands`, path.commands);\n let curves: DisplayBezierCurve[] = [];\n let startPoint: Vector2 = { x: 0, y: 0 };\n\n const allCurves: DisplayBezierCurve[][] = [];\n const parsedPaths: { path: Vector2[]; isHole: boolean }[] = [];\n let wasHole = false;\n\n let pathCommands = path.commands;\n pathCommands.forEach((cmd) => {\n switch (cmd.type) {\n case \"M\": // moveTo\n {\n startPoint.x = cmd.x;\n startPoint.y = cmd.y;\n }\n break;\n\n case \"L\": // lineTo\n {\n const controlPoints: Vector2[] = [{ x: cmd.x, y: cmd.y }];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"segment\", controlPoints });\n }\n break;\n\n case \"Q\": // quadratic Bezier\n {\n const controlPoints: Vector2[] = [\n { x: cmd.x1, y: cmd.y1 },\n { x: cmd.x, y: cmd.y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"quadratic\", controlPoints });\n }\n break;\n\n case \"C\": // cubic Bezier\n {\n const controlPoints: Vector2[] = [\n { x: cmd.x1, y: cmd.y1 },\n { x: cmd.x2, y: cmd.y2 },\n { x: cmd.x, y: cmd.y },\n ];\n if (curves.length === 0) {\n controlPoints.unshift({ ...startPoint });\n }\n curves.push({ type: \"cubic\", controlPoints });\n }\n break;\n\n case \"Z\": // closePath\n {\n if (curves.length === 0) {\n break;\n }\n\n curves = simplifyCurves(curves);\n\n // Flatten all control points\n const controlPoints = curves.flatMap((c) => c.controlPoints);\n\n // Apply path offset\n controlPoints.forEach((pt) => {\n pt.x = pt.x + pathOffset.x;\n pt.y = pt.y + pathOffset.y;\n });\n\n allCurves.push(curves);\n\n // Reset curves\n curves = [];\n }\n break;\n }\n });\n\n allCurves.sort((a, b) => {\n const aPoints = getCurvesPoints(a);\n const bPoints = getCurvesPoints(b);\n return contourArea(bPoints) - contourArea(aPoints);\n });\n\n allCurves.forEach((curve) => {\n const controlPoints = curve.flatMap((c) => c.controlPoints);\n const isHole = classifySubpath(controlPoints, parsedPaths, \"nonzero\");\n parsedPaths.push({ path: controlPoints, isHole });\n if (isHole != wasHole) {\n wasHole = isHole;\n if (isHole) {\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 0,\n });\n } else {\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 1,\n });\n }\n }\n\n const isSegments = curves.every((c) => c.type === \"segment\");\n if (isSegments) {\n commands.push({\n type: \"drawPolygon\",\n points: controlPoints,\n });\n } else {\n commands.push({ type: \"drawClosedPath\", curves });\n }\n });\n } else {\n if (bitmapWidth > 0 && bitmapHeight > 0) {\n canvas.width = bitmapWidth;\n canvas.height = bitmapHeight;\n ctx.imageSmoothingEnabled = false;\n\n ctx.fillStyle = \"black\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n path.draw(ctx);\n const { colorIndices } = await quantizeCanvas(canvas, 2, [\n \"#000000\",\n \"#ffffff\",\n ]);\n const bitmap: DisplayBitmap = {\n width: bitmapWidth,\n height: bitmapHeight,\n numberOfColors: 2,\n pixels: colorIndices,\n };\n\n commands.push({\n type: \"selectBitmapColor\",\n bitmapColorIndex: 1,\n colorIndex: 1,\n });\n if (false) {\n // debugging\n commands.push({\n type: \"selectFillColor\",\n fillColorIndex: 2,\n });\n commands.push({\n type: \"drawRect\",\n offsetX: 0,\n offsetY: 0,\n width: spriteWidth,\n height: spriteHeight,\n });\n }\n\n commands.push({\n type: \"drawBitmap\",\n offsetX: bitmapX,\n offsetY: bitmapY,\n bitmap,\n });\n }\n }\n\n const sprite: DisplaySprite = {\n name,\n commands,\n width: spriteWidth,\n height: spriteHeight,\n };\n\n spriteSheet.sprites.push(sprite);\n }\n\n if (string != undefined && string.length == 0) {\n break;\n }\n }\n\n return spriteSheet;\n}\n\nexport function stringToSprites(\n string: string,\n spriteSheet: DisplaySpriteSheet,\n requireAll = false\n) {\n const sprites: DisplaySprite[] = [];\n let substring = string;\n while (substring.length > 0) {\n let longestSprite: DisplaySprite | undefined;\n\n spriteSheet.sprites.forEach((sprite) => {\n if (substring.startsWith(sprite.name)) {\n if (!longestSprite || sprite.name.length > longestSprite.name.length) {\n longestSprite = sprite;\n }\n }\n });\n\n // _console.log(\"longestSprite\", longestSprite);\n if (requireAll) {\n _console.assertWithError(\n longestSprite,\n `couldn't find sprite with name prefixing \"${substring}\"`\n );\n }\n\n if (longestSprite) {\n sprites.push(longestSprite);\n substring = substring.substring(longestSprite!.name.length);\n } else {\n substring = substring.substring(1);\n }\n //_console.log(\"new substring\", substring);\n }\n\n //_console.log(`string \"${string}\" to sprites`, sprites);\n return sprites;\n}\n\nexport function getReferencedSprites(\n sprite: DisplaySprite,\n spriteSheet: DisplaySpriteSheet\n) {\n const sprites: DisplaySprite[] = [];\n sprite.commands\n .filter((command) => command.type == \"drawSprite\")\n .map((command) => command.spriteIndex)\n .map((spriteIndex) => spriteSheet.sprites[spriteIndex])\n .forEach((_sprite) => {\n if (!sprites.includes(_sprite)) {\n sprites.push(_sprite);\n sprites.push(...getReferencedSprites(_sprite, spriteSheet));\n }\n });\n _console.log(\"referencedSprites\", sprite, sprites);\n return sprites;\n}\nexport function reduceSpriteSheet(\n spriteSheet: DisplaySpriteSheet,\n spriteNames: string | string[],\n requireAll = false\n) {\n const reducedSpriteSheet = Object.assign({}, spriteSheet);\n if (!(spriteNames instanceof Array)) {\n spriteNames = stringToSprites(spriteNames, spriteSheet, requireAll).map(\n (sprite) => sprite.name\n );\n }\n _console.log(\"reducingSpriteSheet\", spriteSheet, spriteNames);\n reducedSpriteSheet.sprites = [];\n spriteSheet.sprites.forEach((sprite) => {\n if (spriteNames.includes(sprite.name)) {\n reducedSpriteSheet.sprites.push(sprite);\n reducedSpriteSheet.sprites.push(\n ...getReferencedSprites(sprite, spriteSheet)\n );\n }\n });\n _console.log(\"reducedSpriteSheet\", reducedSpriteSheet);\n return reducedSpriteSheet;\n}\n\nexport function stringToSpriteLines(\n string: string,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState,\n requireAll = false,\n maxLineBreadth = Infinity,\n separators = [\" \"]\n): DisplaySpriteLines {\n _console.log(\"stringToSpriteLines\", string);\n const isSpritesDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesDirection\n );\n const isSpritesLineDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesLineDirection\n );\n const areSpritesDirectionsOrthogonal =\n isSpritesDirectionHorizontal != isSpritesLineDirectionHorizontal;\n\n const lineStrings = string.split(\"\\n\");\n let lineBreadth = 0;\n\n if (isSpritesLineDirectionHorizontal) {\n maxLineBreadth /= contextState.spriteScaleX;\n } else {\n maxLineBreadth /= contextState.spriteScaleY;\n }\n\n const sprites: {\n sprite: DisplaySprite;\n spriteSheet: DisplaySpriteSheet;\n }[][] = [];\n let latestSeparatorIndex = -1;\n let latestSeparator: string | undefined;\n let latestSeparatorLineBreadth: number | undefined;\n let latestSeparatorBreadth: number | undefined;\n const spritesLineIndices: number[][] = [];\n\n lineStrings.forEach((lineString) => {\n sprites.push([]);\n spritesLineIndices.push([]);\n const i = sprites.length - 1;\n if (areSpritesDirectionsOrthogonal) {\n lineBreadth = 0;\n } else {\n lineBreadth += contextState.spritesLineSpacing;\n }\n\n let lineSubstring = lineString;\n while (lineSubstring.length > 0) {\n let longestSprite: DisplaySprite | undefined;\n let longestSpriteSheet: DisplaySpriteSheet | undefined;\n for (let spriteSheetName in spriteSheets) {\n const spriteSheet = spriteSheets[spriteSheetName];\n spriteSheet.sprites.forEach((sprite) => {\n if (lineSubstring.startsWith(sprite.name)) {\n if (\n !longestSprite ||\n sprite.name.length > longestSprite.name.length\n ) {\n longestSprite = sprite;\n longestSpriteSheet = spriteSheet;\n }\n }\n });\n }\n //_console.log(\"longestSprite\", longestSprite);\n if (requireAll) {\n _console.assertWithError(\n longestSprite,\n `couldn't find sprite with name prefixing \"${lineSubstring}\"`\n );\n }\n\n if (longestSprite && longestSpriteSheet) {\n const isSeparator =\n separators.length > 0\n ? separators.includes(longestSprite.name)\n : true;\n\n sprites[i].push({\n sprite: longestSprite,\n spriteSheet: longestSpriteSheet,\n });\n\n // _console.log({\n // name: longestSprite!.name,\n // isSeparator,\n // lineBreadth,\n // latestSeparatorIndex,\n // latestSeparatorLineBreadth,\n // latestSeparator,\n // index: sprites[i].length - 1,\n // });\n\n let newLineBreadth = lineBreadth;\n const longestSpriteBreadth = isSpritesDirectionHorizontal\n ? longestSprite.width\n : longestSprite.height;\n newLineBreadth += longestSpriteBreadth;\n newLineBreadth += contextState.spritesSpacing;\n if (newLineBreadth >= maxLineBreadth) {\n if (isSeparator) {\n if (longestSprite.name.trim().length == 0) {\n sprites[i].pop();\n }\n spritesLineIndices[i].push(sprites[i].length);\n lineBreadth = 0;\n } else {\n if (latestSeparatorIndex != -1) {\n if (latestSeparator!.trim().length == 0) {\n sprites[i].splice(latestSeparatorIndex, 1);\n lineBreadth -= latestSeparatorBreadth!;\n latestSeparatorIndex;\n }\n spritesLineIndices[i].push(latestSeparatorIndex);\n lineBreadth = newLineBreadth - latestSeparatorLineBreadth!;\n } else {\n spritesLineIndices[i].push(sprites[i].length - 1);\n lineBreadth = 0;\n }\n }\n latestSeparatorIndex = -1;\n latestSeparator = undefined;\n } else {\n lineBreadth = newLineBreadth;\n\n if (isSeparator) {\n latestSeparator = longestSprite.name;\n latestSeparatorIndex = sprites[i].length - 1;\n //_console.log({ latestSeparatorIndex });\n latestSeparatorLineBreadth = lineBreadth;\n latestSeparatorBreadth = longestSpriteBreadth;\n }\n }\n\n lineSubstring = lineSubstring.substring(longestSprite!.name.length);\n } else {\n lineSubstring = lineSubstring.substring(1);\n }\n }\n });\n\n const spriteLines: DisplaySpriteLine[] = [];\n sprites.forEach((_sprites, i) => {\n let spriteLine: DisplaySpriteLine = [];\n spriteLines.push(spriteLine);\n\n let spriteSubLine: DisplaySpriteSubLine | undefined;\n\n _sprites.forEach(({ sprite, spriteSheet }, index) => {\n if (spritesLineIndices[i].includes(index)) {\n spriteLine = [];\n spriteLines.push(spriteLine);\n spriteSubLine = undefined;\n }\n\n if (!spriteSubLine || spriteSubLine.spriteSheetName != spriteSheet.name) {\n spriteSubLine = {\n spriteSheetName: spriteSheet.name,\n spriteNames: [],\n };\n spriteLine.push(spriteSubLine);\n }\n spriteSubLine.spriteNames.push(sprite.name);\n });\n });\n _console.log(`spriteLines for \"${string}\"`, spriteLines);\n return spriteLines;\n}\n\nexport function getFontMaxHeight(font: Font, fontSize: number) {\n const scale = (1 / font.unitsPerEm) * fontSize;\n const maxHeight = (font.ascender - font.descender) * scale;\n return maxHeight;\n}\nexport function getMaxSpriteSheetSize(spriteSheet: DisplaySpriteSheet) {\n const size: DisplaySize = { width: 0, height: 0 };\n spriteSheet.sprites.forEach((sprite) => {\n size.width = Math.max(size.width, sprite.width);\n size.height = Math.max(size.height, sprite.height);\n });\n return size;\n}\n\nexport function assertValidSpriteLines(\n displayManager: DisplayManagerInterface,\n spriteLines: DisplaySpriteLines\n) {\n spriteLines.forEach((spriteLine) => {\n spriteLine.forEach((spriteSubLine) => {\n const { spriteSheetName, spriteNames } = spriteSubLine;\n displayManager.assertLoadedSpriteSheet(spriteSheetName);\n const spriteSheet = displayManager.spriteSheets[spriteSheetName];\n spriteNames.forEach((spriteName) => {\n const sprite = spriteSheet.sprites.find(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n sprite,\n `no sprite with name \"${spriteName}\" found in spriteSheet \"${spriteSheetName}\"`\n );\n });\n });\n });\n}\n\nexport function getExpandedSpriteLines(\n spriteLines: DisplaySpriteLines,\n spriteSheets: Record<string, DisplaySpriteSheet>\n) {\n const expandedSpritesLines: DisplaySprite[][] = [];\n\n spriteLines.forEach((spriteLine) => {\n const _spritesLine: DisplaySprite[] = [];\n\n spriteLine.forEach(({ spriteSheetName, spriteNames }) => {\n const spriteSheet = spriteSheets[spriteSheetName];\n _console.assertWithError(\n spriteSheet,\n `no spriteSheet found with name \"${spriteSheetName}\"`\n );\n\n spriteNames.forEach((spriteName) => {\n const sprite = spriteSheet.sprites.find(\n (sprite) => sprite.name == spriteName\n )!;\n _console.assertWithError(\n sprite,\n `no sprite found with name \"${spriteName} in \"${spriteSheetName}\" spriteSheet`\n );\n _spritesLine.push(sprite);\n });\n });\n expandedSpritesLines.push(_spritesLine);\n });\n return expandedSpritesLines;\n}\n\nexport function getExpandedSpriteLinesSize(\n expandedSpritesLines: DisplaySprite[][],\n contextState: DisplayContextState\n) {\n const localSize = { width: 0, height: 0 };\n\n const isSpritesDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesDirection\n );\n const isSpritesLineDirectionHorizontal = isDirectionHorizontal(\n contextState.spritesLineDirection\n );\n\n const areSpritesDirectionsOrthogonal =\n isSpritesDirectionHorizontal != isSpritesLineDirectionHorizontal;\n\n const breadthSizeKey = isSpritesDirectionHorizontal ? \"width\" : \"height\";\n const depthSizeKey = isSpritesLineDirectionHorizontal ? \"width\" : \"height\";\n\n if (!areSpritesDirectionsOrthogonal) {\n if (isSpritesDirectionHorizontal) {\n localSize.height += contextState.spritesLineHeight;\n } else {\n localSize.width += contextState.spritesLineHeight;\n }\n }\n\n const lineBreadths: number[] = [];\n\n expandedSpritesLines.forEach((expandedSpriteLine, lineIndex) => {\n let spritesLineBreadth = 0;\n\n expandedSpriteLine.forEach((sprite) => {\n spritesLineBreadth += isSpritesDirectionHorizontal\n ? sprite.width\n : sprite.height;\n spritesLineBreadth += contextState.spritesSpacing;\n });\n spritesLineBreadth -= contextState.spritesSpacing;\n\n if (areSpritesDirectionsOrthogonal) {\n localSize[breadthSizeKey] = Math.max(\n localSize[breadthSizeKey],\n spritesLineBreadth\n );\n\n localSize[depthSizeKey] += contextState.spritesLineHeight;\n } else {\n localSize[breadthSizeKey] += spritesLineBreadth;\n }\n\n localSize[depthSizeKey] += contextState.spritesLineSpacing;\n\n // _console.log({\n // lineIndex,\n // spritesBreadth: spritesSize[breadthSizeKey],\n // spritesDepth: spritesSize[depthSizeKey],\n // });\n\n lineBreadths.push(spritesLineBreadth);\n });\n localSize[depthSizeKey] -= contextState.spritesLineSpacing;\n\n // _console.log({\n // spritesWidth: spritesSize.width,\n // spritesHeight: spritesSize.height,\n // });\n\n const spritesScaledWidth =\n localSize.width * Math.abs(contextState.spriteScaleX);\n const spritesScaledHeight =\n localSize.height * Math.abs(contextState.spriteScaleY);\n\n const size: DisplaySize = {\n width: spritesScaledWidth,\n height: spritesScaledHeight,\n };\n\n return { localSize, size, lineBreadths };\n}\n\nexport function getSpriteLinesMetrics(\n spriteLines: DisplaySpriteLines,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState\n) {\n const expandedSpritesLines = getExpandedSpriteLines(\n spriteLines,\n spriteSheets\n );\n return {\n expandedSpritesLines,\n numberOfLines: expandedSpritesLines.length,\n ...getExpandedSpriteLinesSize(expandedSpritesLines, contextState),\n };\n}\n\nexport type DisplaySpriteLinesMetrics = {\n localSize: {\n width: number;\n height: number;\n };\n size: DisplaySize;\n lineBreadths: number[];\n expandedSpritesLines: DisplaySprite[][];\n numberOfLines: number;\n};\nexport function stringToSpriteLinesMetrics(\n string: string,\n spriteSheets: Record<string, DisplaySpriteSheet>,\n contextState: DisplayContextState,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n): DisplaySpriteLinesMetrics {\n return getSpriteLinesMetrics(\n stringToSpriteLines(\n string,\n spriteSheets,\n contextState,\n requireAll,\n maxLineBreadth,\n separators\n ),\n spriteSheets,\n contextState\n );\n}\n","// @ts-expect-error\nimport RGBQuant from \"rgbquant\";\nimport { createConsole } from \"./Console.ts\";\nimport { hexToRGB, rgbToHex } from \"./ColorUtils.ts\";\nimport { getVector3Length, Vector3 } from \"./MathUtils.ts\";\nimport {\n DisplayColorRGB,\n numberOfColorsToPixelDepth,\n pixelDepthToNumberOfColors,\n pixelDepthToPixelBitWidth,\n pixelDepthToPixelsPerByte,\n} from \"./DisplayUtils.ts\";\nimport { DisplayBitmap, DisplayPixelDepths } from \"../DisplayManager.ts\";\nimport {\n calculateSpriteSheetHeaderLength,\n DisplaySprite,\n DisplaySpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\n\nconst _console = createConsole(\"DisplayBitmapUtils\", { log: false });\n\nexport const drawBitmapHeaderLength = 2 + 2 + 2 + 4 + 1 + 2; // x, y, width, numberOfPixels, numberOfColors, dataLength\n\nexport function getBitmapData(bitmap: DisplayBitmap) {\n const pixelDataLength = getBitmapNumberOfBytes(bitmap);\n const dataView = new DataView(new ArrayBuffer(pixelDataLength));\n const pixelDepth = numberOfColorsToPixelDepth(bitmap.numberOfColors)!;\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n bitmap.pixels.forEach((bitmapColorIndex, pixelIndex) => {\n const byteIndex = Math.floor(pixelIndex / pixelsPerByte);\n const byteSlot = pixelIndex % pixelsPerByte;\n const pixelBitWidth = pixelDepthToPixelBitWidth(pixelDepth);\n const bitOffset = pixelBitWidth * byteSlot;\n const shift = 8 - pixelBitWidth - bitOffset;\n let value = dataView.getUint8(byteIndex);\n value |= bitmapColorIndex << shift;\n dataView.setUint8(byteIndex, value);\n });\n _console.log(\"getBitmapData\", bitmap, dataView);\n return dataView;\n}\n\nexport async function quantizeCanvas(\n canvas: HTMLCanvasElement,\n numberOfColors: number,\n colors?: string[]\n) {\n _console.assertWithError(\n numberOfColors > 1,\n \"numberOfColors must be greater than 1\"\n );\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n removeAlphaFromCanvas(canvas);\n\n const isSmall = canvas.width * canvas.height < 4;\n\n const quantOptions = {\n method: isSmall ? 1 : 2,\n colors: numberOfColors,\n dithKern: null, // Disable dithering\n useCache: false, // Disable color caching to force exact matches\n reIndex: true, // Ensure strict re-indexing to the palette\n orDist: \"manhattan\",\n };\n\n if (colors) {\n // @ts-ignore\n quantOptions.palette = colors.map((color) => {\n const rgb = hexToRGB(color);\n if (rgb) {\n const { r, g, b } = rgb;\n return [r, g, b];\n } else {\n _console.error(`invalid rgb hex \"${color}\"`);\n }\n });\n }\n //_console.log(\"quantizeImage options\", quantOptions);\n const quantizer = new RGBQuant(quantOptions);\n const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n quantizer.sample(imageData);\n\n const quantizedPixels = quantizer.reduce(imageData.data);\n const quantizedImageData = new ImageData(\n new Uint8ClampedArray(quantizedPixels.buffer),\n canvas.width,\n canvas.height\n );\n ctx.putImageData(quantizedImageData, 0, 0);\n\n const pixels = quantizedImageData.data;\n\n const quantizedPaletteData: Uint8Array = quantizer.palette();\n const numberOfQuantizedPaletteColors = quantizedPaletteData.byteLength / 4;\n //_console.log(\"quantizedPaletteData\", quantizedPaletteData);\n const quantizedPaletteColors: DisplayColorRGB[] = [];\n let closestColorIndexToBlack = 0;\n let closestColorDistanceToBlack = Infinity;\n const vector3: Vector3 = { x: 0, y: 0, z: 0 };\n for (\n let colorIndex = 0;\n colorIndex < numberOfQuantizedPaletteColors;\n colorIndex++\n ) {\n const rgb: DisplayColorRGB = {\n r: quantizedPaletteData[colorIndex * 4],\n g: quantizedPaletteData[colorIndex * 4 + 1],\n b: quantizedPaletteData[colorIndex * 4 + 2],\n };\n quantizedPaletteColors.push(rgb);\n vector3.x = rgb.r;\n vector3.y = rgb.g;\n vector3.z = rgb.b;\n\n const distanceToBlack = getVector3Length(vector3);\n if (distanceToBlack < closestColorDistanceToBlack) {\n closestColorDistanceToBlack = distanceToBlack;\n closestColorIndexToBlack = colorIndex;\n }\n }\n //_console.log({ closestColorIndexToBlack, closestColorDistanceToBlack });\n if (closestColorIndexToBlack != 0) {\n const [currentBlack, newBlack] = [\n quantizedPaletteColors[0],\n quantizedPaletteColors[closestColorIndexToBlack],\n ];\n quantizedPaletteColors[0] = newBlack;\n quantizedPaletteColors[closestColorIndexToBlack] = currentBlack;\n }\n //_console.log(\"quantizedPaletteColors\", quantizedPaletteColors);\n const quantizedColors = quantizedPaletteColors.map((rgb, index) => {\n const hex = rgbToHex(rgb);\n return hex;\n });\n //_console.log(\"quantizedColors\", quantizedColors);\n\n const quantizedColorIndices: number[] = [];\n for (let i = 0; i < pixels.length; i += 4) {\n const r = pixels[i];\n const g = pixels[i + 1];\n const b = pixels[i + 2];\n const a = pixels[i + 3];\n\n const hex = rgbToHex({ r, g, b });\n quantizedColorIndices.push(quantizedColors.indexOf(hex));\n }\n //_console.log(\"quantizedColorIndices\", quantizedColorIndices);\n\n const promise = new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject();\n }\n }, \"image/png\");\n });\n\n const blob = await promise;\n return {\n blob,\n colors: quantizedColors,\n colorIndices: quantizedColorIndices,\n };\n}\n\nexport async function quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n let { naturalWidth: imageWidth, naturalHeight: imageHeight } = image;\n _console.log({ imageWidth, imageHeight });\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n\n ctx.drawImage(image, 0, 0, width, height);\n\n return quantizeCanvas(canvas, numberOfColors, colors);\n}\n\nexport function resizeImage(\n image: CanvasImageSource,\n width: number,\n height: number,\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n canvas.width = width;\n canvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n\n ctx.drawImage(image, 0, 0, width, height);\n\n return canvas;\n}\nexport function cropCanvas(\n canvas: HTMLCanvasElement,\n x: number,\n y: number,\n width: number,\n height: number,\n targetCanvas?: HTMLCanvasElement\n) {\n targetCanvas = targetCanvas || document.createElement(\"canvas\");\n const ctx = targetCanvas.getContext(\"2d\", { willReadFrequently: true })!;\n\n targetCanvas.width = width;\n targetCanvas.height = height;\n\n ctx.imageSmoothingEnabled = false;\n ctx.drawImage(canvas, x, y, width, height, 0, 0, width, height);\n\n return targetCanvas;\n}\nexport function removeAlphaFromCanvas(canvas: HTMLCanvasElement) {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })!;\n const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n const data = imageData.data;\n\n // turn any non-opaque pixel to black\n for (let i = 0; i < data.length; i += 4) {\n const alpha = data[i + 3];\n\n if (alpha < 255) {\n data[i] = 0;\n data[i + 1] = 0;\n data[i + 2] = 0;\n data[i + 3] = 255;\n }\n }\n\n ctx.putImageData(imageData, 0, 0);\n\n return canvas;\n}\n\nexport async function canvasToBlob(\n canvas: HTMLCanvasElement,\n type: \"image/png\" | \"image/jpeg\" = \"image/jpeg\",\n quality: number = 1\n) {\n const promise = new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject();\n }\n },\n type,\n quality\n );\n });\n const blob = await promise;\n return blob;\n}\n\nexport async function resizeAndQuantizeImage(\n image: CanvasImageSource,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n) {\n canvas = canvas || document.createElement(\"canvas\");\n resizeImage(image, width, height, canvas);\n removeAlphaFromCanvas(canvas);\n return quantizeCanvas(canvas, numberOfColors, colors);\n}\n\nexport async function imageToBitmap(\n image: CanvasImageSource,\n width: number,\n height: number,\n colors: string[],\n bitmapColorIndices: number[],\n numberOfColors?: number\n) {\n if (numberOfColors == undefined) {\n numberOfColors = colors.length;\n }\n const bitmapColors = bitmapColorIndices\n .map((bitmapColorIndex) => colors[bitmapColorIndex])\n .slice(0, numberOfColors);\n const { blob, colorIndices } = await resizeAndQuantizeImage(\n image,\n width,\n height,\n numberOfColors,\n bitmapColors\n );\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n return { blob, bitmap };\n}\n\nconst drawSpriteBitmapCommandHeaderLength = 1 + 2 + 2 + 2 + 2 + 1 + 2; // command, offetXY, width, numberOfPixels, numberOfColors, pixelDataLength\nexport async function canvasToBitmaps(\n canvas: HTMLCanvasElement,\n numberOfColors: number,\n mtu: number\n) {\n const { blob, colors, colorIndices } = await quantizeCanvas(\n canvas,\n numberOfColors\n );\n const bitmapRows: DisplayBitmap[][] = [];\n\n const { width, height } = canvas;\n\n const numberOfPixels = width * height;\n const pixelDepth = DisplayPixelDepths.find(\n (pixelDepth) => pixelDepthToNumberOfColors(pixelDepth) >= numberOfColors\n )!;\n _console.assertWithError(\n pixelDepth,\n `no pixelDepth found that covers ${numberOfColors} colors`\n );\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfBytes = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n width,\n height,\n numberOfPixels,\n pixelDepth,\n pixelsPerByte,\n numberOfBytes,\n mtu,\n });\n\n const maxPixelDataLength = mtu - (drawSpriteBitmapCommandHeaderLength + 5);\n const maxPixels = Math.floor(maxPixelDataLength / pixelsPerByte);\n const maxBitmapWidth = Math.min(maxPixels, width);\n let maxBitmapHeight = 1;\n if (maxBitmapWidth == width) {\n const bitmapRowPixelDataLength = Math.ceil(width / pixelsPerByte);\n maxBitmapHeight = Math.floor(maxPixelDataLength / bitmapRowPixelDataLength);\n }\n _console.log({\n maxPixelDataLength,\n maxPixels,\n maxBitmapHeight,\n maxBitmapWidth,\n });\n\n if (maxBitmapHeight >= height) {\n _console.log(\"image is small enough for a single bitmap\");\n\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n bitmapRows.push([bitmap]);\n } else {\n let offsetX = 0;\n let offsetY = 0;\n const bitmapCanvas: HTMLCanvasElement = document.createElement(\"canvas\");\n const bitmapColorIndices: number[] = new Array(numberOfColors)\n .fill(0)\n .map((_, i) => i);\n while (offsetY < height) {\n const bitmapHeight = Math.min(maxBitmapHeight, height - offsetY);\n offsetX = 0;\n const bitmapRow: DisplayBitmap[] = [];\n bitmapRows.push(bitmapRow);\n\n while (offsetX < width) {\n const bitmapWidth = Math.min(maxBitmapWidth, width - offsetX);\n cropCanvas(\n canvas,\n offsetX,\n offsetY,\n bitmapWidth,\n bitmapHeight,\n bitmapCanvas\n );\n // _console.log(`cropping bitmap`, {\n // bitmapWidth,\n // bitmapHeight,\n // offsetX,\n // offsetY,\n // });\n const { bitmap } = await imageToBitmap(\n bitmapCanvas,\n bitmapWidth,\n bitmapHeight,\n colors,\n bitmapColorIndices,\n numberOfColors\n );\n // _console.log(\"bitmap\", bitmap);\n bitmapRow.push(bitmap);\n offsetX += bitmapWidth;\n }\n offsetY += bitmapHeight;\n }\n }\n\n return { bitmapRows, colors };\n}\nexport async function imageToBitmaps(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n mtu: number\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToBitmaps(canvas, numberOfColors, mtu);\n}\n\nexport function getBitmapNumberOfBytes(bitmap: DisplayBitmap) {\n const pixelDepth = numberOfColorsToPixelDepth(bitmap.numberOfColors)!;\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfPixels = bitmap.pixels.length;\n const pixelDataLength = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n pixelDepth,\n pixelsPerByte,\n numberOfPixels,\n pixelDataLength,\n });\n return pixelDataLength;\n}\nexport function assertValidBitmapPixels(bitmap: DisplayBitmap) {\n _console.assertRangeWithError(\n \"bitmap.pixels.length\",\n bitmap.pixels.length,\n bitmap.width * (bitmap.height - 1) + 1,\n bitmap.width * bitmap.height\n );\n bitmap.pixels.forEach((pixel, index) => {\n _console.assertRangeWithError(\n `bitmap.pixels[${index}]`,\n pixel,\n 0,\n bitmap.numberOfColors - 1\n );\n });\n}\n\nexport async function canvasToSprite(\n canvas: HTMLCanvasElement,\n spriteName: string,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0\n) {\n const { width, height } = canvas;\n\n let palette = spriteSheet.palettes?.find(\n (palette) => palette.name == paletteName\n );\n if (!palette) {\n palette = {\n name: paletteName,\n numberOfColors,\n colors: new Array(numberOfColors).fill(\"#000000\"),\n };\n spriteSheet.palettes = spriteSheet.palettes || [];\n spriteSheet.palettes?.push(palette);\n }\n _console.log(\"pallete\", palette);\n\n // _console.assertWithError(\n // numberOfColors + paletteOffset <= palette.numberOfColors,\n // `invalid numberOfColors ${numberOfColors} + offset ${paletteOffset} (max ${palette.numberOfColors})`\n // );\n\n const sprite: DisplaySprite = {\n name: spriteName,\n width,\n height,\n paletteSwaps: [],\n commands: [],\n };\n\n const results = await quantizeCanvas(\n canvas,\n numberOfColors,\n !overridePalette ? palette.colors : undefined\n );\n const blob = results.blob;\n const colorIndices = results.colorIndices;\n if (overridePalette) {\n results.colors.forEach((color, index) => {\n palette.colors[index + paletteOffset] = color;\n });\n }\n\n sprite.commands.push({\n type: \"selectBitmapColors\",\n bitmapColorPairs: new Array(numberOfColors).fill(0).map((_, index) => ({\n bitmapColorIndex: index,\n colorIndex: index + paletteOffset,\n })),\n });\n const bitmap: DisplayBitmap = {\n numberOfColors,\n pixels: colorIndices,\n width,\n height,\n };\n sprite.commands.push({ type: \"drawBitmap\", offsetX: 0, offsetY: 0, bitmap });\n\n const spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n if (spriteIndex == -1) {\n spriteSheet.sprites.push(sprite);\n } else {\n _console.log(`overwriting spriteIndex ${spriteIndex}`);\n spriteSheet.sprites[spriteIndex] = sprite;\n }\n\n return { sprite, blob };\n}\nexport async function imageToSprite(\n image: HTMLImageElement,\n spriteName: string,\n width: number,\n height: number,\n numberOfColors: number,\n paletteName: string,\n overridePalette: boolean,\n spriteSheet: DisplaySpriteSheet,\n paletteOffset = 0\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToSprite(\n canvas,\n spriteName,\n numberOfColors,\n paletteName,\n overridePalette,\n spriteSheet,\n paletteOffset\n );\n}\n\nconst spriteSheetWithSingleBitmapCommandLength =\n calculateSpriteSheetHeaderLength(1) + drawSpriteBitmapCommandHeaderLength;\nfunction spriteSheetWithBitmapCommandAndSelectBitmapColorsLength(\n numberOfColors: number\n) {\n return (\n spriteSheetWithSingleBitmapCommandLength + (1 + 1 + numberOfColors * 2)\n ); // command, numberOfPairs, ...pairs\n}\n\nexport async function canvasToSpriteSheet(\n canvas: HTMLCanvasElement,\n spriteSheetName: string,\n numberOfColors: number,\n paletteName: string,\n maxFileLength?: number\n) {\n const spriteSheet: DisplaySpriteSheet = {\n name: spriteSheetName,\n palettes: [],\n paletteSwaps: [],\n sprites: [],\n };\n\n if (maxFileLength == undefined) {\n await canvasToSprite(\n canvas,\n \"image\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet\n );\n } else {\n const { width, height } = canvas;\n const numberOfPixels = width * height;\n const pixelDepth = DisplayPixelDepths.find(\n (pixelDepth) => pixelDepthToNumberOfColors(pixelDepth) >= numberOfColors\n )!;\n _console.assertWithError(\n pixelDepth,\n `no pixelDepth found that covers ${numberOfColors} colors`\n );\n const pixelsPerByte = pixelDepthToPixelsPerByte(pixelDepth);\n const numberOfBytes = Math.ceil(numberOfPixels / pixelsPerByte);\n _console.log({\n width,\n height,\n numberOfPixels,\n pixelDepth,\n pixelsPerByte,\n numberOfBytes,\n maxFileLength,\n });\n\n const maxPixelDataLength =\n maxFileLength -\n (spriteSheetWithBitmapCommandAndSelectBitmapColorsLength(numberOfColors) +\n 5);\n const imageRowPixelDataLength = Math.ceil(width / pixelsPerByte);\n const maxSpriteHeight = Math.floor(\n maxPixelDataLength / imageRowPixelDataLength\n );\n // _console.log({\n // maxPixelDataLength,\n // imageRowPixelDataLength,\n // maxSpriteHeight,\n // });\n\n if (maxSpriteHeight >= height) {\n _console.log(\"image is small enough for a single sprite\");\n await canvasToSprite(\n canvas,\n \"image\",\n numberOfColors,\n paletteName,\n true,\n spriteSheet\n );\n } else {\n const { colors } = await quantizeCanvas(canvas, numberOfColors);\n spriteSheet.palettes?.push({ name: paletteName, numberOfColors, colors });\n\n let offsetY = 0;\n let imageIndex = 0;\n const spriteCanvas: HTMLCanvasElement = document.createElement(\"canvas\");\n\n while (offsetY < height) {\n const spriteHeight = Math.min(maxSpriteHeight, height - offsetY);\n cropCanvas(canvas, 0, offsetY, width, spriteHeight, spriteCanvas);\n offsetY += spriteHeight;\n _console.log(`cropping sprite ${imageIndex}`, {\n offsetY,\n width,\n spriteHeight,\n });\n await canvasToSprite(\n spriteCanvas,\n `image${imageIndex}`,\n numberOfColors,\n paletteName,\n false,\n spriteSheet\n );\n imageIndex++;\n }\n }\n }\n\n return spriteSheet;\n}\n\nexport async function imageToSpriteSheet(\n image: HTMLImageElement,\n spriteSheetName: string,\n width: number,\n height: number,\n numberOfColors: number,\n paletteName: string,\n maxFileLength?: number\n) {\n const canvas = resizeImage(image, width, height);\n return canvasToSpriteSheet(\n canvas,\n spriteSheetName,\n numberOfColors,\n paletteName,\n maxFileLength\n );\n}\n","import {\n DisplayBitmapColorPair,\n DisplayBrightness,\n DisplaySpriteColorPair,\n DisplayBitmap,\n DisplayBezierCurve,\n DisplayBezierCurveType,\n DisplayWireframe,\n DisplaySize,\n} from \"../DisplayManager.ts\";\nimport { createConsole } from \"./Console.ts\";\nimport { DisplayContextCommand } from \"./DisplayContextCommand.ts\";\nimport {\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayContextState,\n DisplayDirection,\n DisplaySegmentCap,\n} from \"./DisplayContextState.ts\";\nimport {\n DisplaySprite,\n DisplaySpriteLines,\n DisplaySpriteLinesMetrics,\n DisplaySpritePaletteSwap,\n DisplaySpriteSheet,\n DisplaySpriteSheetPalette,\n DisplaySpriteSheetPaletteSwap,\n reduceSpriteSheet,\n} from \"./DisplaySpriteSheetUtils.ts\";\nimport {\n DisplayScaleDirection,\n DisplayColorRGB,\n DisplayCropDirection,\n} from \"./DisplayUtils.ts\";\nimport { degToRad, Vector2 } from \"./MathUtils.ts\";\n\nconst _console = createConsole(\"DisplayManagerInterface\", { log: false });\n\nexport interface DisplayManagerInterface {\n get isReady(): boolean;\n\n get contextState(): DisplayContextState;\n\n flushContextCommands(): Promise<void>;\n\n get brightness(): DisplayBrightness;\n setBrightness(\n newDisplayBrightness: DisplayBrightness,\n sendImmediately?: boolean\n ): Promise<void>;\n\n show(sendImmediately?: boolean): Promise<void>;\n clear(sendImmediately?: boolean): Promise<void>;\n\n get colors(): string[];\n get numberOfColors(): number;\n setColor(\n colorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n\n assertValidColorIndex(colorIndex: number): void;\n assertValidLineWidth(lineWidth: number): void;\n assertValidNumberOfColors(numberOfColors: number): void;\n assertValidBitmap(bitmap: DisplayBitmap, checkSize?: boolean): void;\n\n get opacities(): number[];\n setColorOpacity(\n colorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setOpacity(opacity: number, sendImmediately?: boolean): Promise<void>;\n\n saveContext(sendImmediately?: boolean): Promise<void>;\n restoreContext(sendImmediately?: boolean): Promise<void>;\n\n selectFillColor(\n fillColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectBackgroundColor(\n backgroundColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectLineColor(\n lineColorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setLineWidth(lineWidth: number, sendImmediately?: boolean): Promise<void>;\n\n setIgnoreFill(ignoreFill: boolean, sendImmediately?: boolean): Promise<void>;\n setIgnoreLine(ignoreLine: boolean, sendImmediately?: boolean): Promise<void>;\n setFillBackground(\n fillBackground: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setAlignment(\n alignmentDirection: DisplayAlignmentDirection,\n alignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setHorizontalAlignment(\n horizontalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setVerticalAlignment(\n verticalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n resetAlignment(sendImmediately?: boolean): Promise<void>;\n\n setRotation(\n rotation: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n clearRotation(sendImmediately?: boolean): Promise<void>;\n\n setSegmentStartCap(\n segmentStartCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentEndCap(\n segmentEndCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentCap(\n segmentCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSegmentStartRadius(\n segmentStartRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentEndRadius(\n segmentEndRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSegmentRadius(\n segmentRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setCropTop(cropTop: number, sendImmediately?: boolean): Promise<void>;\n setCropRight(cropRight: number, sendImmediately?: boolean): Promise<void>;\n setCropBottom(cropBottom: number, sendImmediately?: boolean): Promise<void>;\n setCropLeft(cropLeft: number, sendImmediately?: boolean): Promise<void>;\n clearCrop(sendImmediately?: boolean): Promise<void>;\n\n setRotationCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropTop(\n rotationCropTop: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropRight(\n rotationCropRight: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropBottom(\n rotationCropBottom: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setRotationCropLeft(\n rotationCropLeft: number,\n sendImmediately?: boolean\n ): Promise<void>;\n clearRotationCrop(sendImmediately?: boolean): Promise<void>;\n\n selectBitmapColor(\n bitmapColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get bitmapColorIndices(): number[];\n get bitmapColors(): string[];\n selectBitmapColors(\n bitmapColorPairs: DisplayBitmapColorPair[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n setBitmapColor(\n bitmapColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapColorOpacity(\n bitmapColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setBitmapScaleDirection(\n direction: DisplayScaleDirection,\n bitmapScale: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScaleX(\n bitmapScaleX: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScaleY(\n bitmapScaleY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setBitmapScale(bitmapScale: number, sendImmediately?: boolean): Promise<void>;\n resetBitmapScale(sendImmediately?: boolean): Promise<void>;\n\n selectSpriteColor(\n spriteColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get spriteColorIndices(): number[];\n get spriteColors(): string[];\n selectSpriteColors(\n spriteColorPairs: DisplaySpriteColorPair[],\n sendImmediately?: boolean\n ): Promise<void>;\n resetSpriteColors(sendImmediately?: boolean): Promise<void>;\n\n setSpriteColor(\n spriteColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteColorOpacity(\n spriteColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpriteScaleDirection(\n direction: DisplayScaleDirection,\n spriteScale: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScaleX(\n spriteScaleX: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScaleY(\n spriteScaleY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpriteScale(spriteScale: number, sendImmediately?: boolean): Promise<void>;\n resetSpriteScale(sendImmediately?: boolean): Promise<void>;\n\n setSpritesLineHeight(\n spritesLineHeight: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesDirectionGeneric(\n direction: DisplayDirection,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesDirection(\n spritesDirection: DisplayDirection,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineDirection(\n spritesLineDirection: DisplayDirection,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesSpacingGeneric(\n spacing: number,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n setSpritesAlignmentGeneric(\n alignment: DisplayAlignment,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesAlignment(\n spritesAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n setSpritesLineAlignment(\n spritesLineAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ): Promise<void>;\n\n clearRect(\n x: number,\n y: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRoundRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n borderRadius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawCircle(\n offsetX: number,\n offsetY: number,\n radius: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawRegularPolygon(\n offsetX: number,\n offsetY: number,\n radius: number,\n numberOfSides: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawPolygon(points: Vector2[], sendImmediately?: boolean): Promise<void>;\n\n drawWireframe(\n wireframe: DisplayWireframe,\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawCurve(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawCurves(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawQuadraticBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawQuadraticBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawCubicBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawCubicBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n _drawPath(\n isClosed: boolean,\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n drawClosedPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawSegment(\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSegments(points: Vector2[], sendImmediately?: boolean): Promise<void>;\n\n drawArc(\n offsetX: number,\n offsetY: number,\n radius: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n drawArcEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ): Promise<void>;\n\n drawBitmap(\n offsetX: number,\n offsetY: number,\n bitmap: DisplayBitmap,\n sendImmediately?: boolean\n ): Promise<void>;\n\n runContextCommand(\n command: DisplayContextCommand,\n sendImmediately?: boolean\n ): Promise<void>;\n\n runContextCommands(\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n ): Promise<void>;\n\n imageToBitmap(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors?: number\n ): Promise<{\n blob: Blob;\n bitmap: DisplayBitmap;\n }>;\n quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n ): Promise<{\n blob: Blob;\n colors: string[];\n colorIndices: number[];\n }>;\n resizeAndQuantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[],\n canvas?: HTMLCanvasElement\n ): Promise<{\n blob: Blob;\n colors: string[];\n colorIndices: number[];\n }>;\n\n uploadSpriteSheet(spriteSheet: DisplaySpriteSheet): Promise<void>;\n uploadSpriteSheets(spriteSheets: DisplaySpriteSheet[]): Promise<void>;\n selectSpriteSheet(\n spriteSheetName: string,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSprite(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n sendImmediately?: boolean\n ): Promise<void>;\n stringToSpriteLines(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLines;\n stringToSpriteLinesMetrics(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLinesMetrics;\n drawSprites(\n offsetX: number,\n offsetY: number,\n spriteLines: DisplaySpriteLines,\n sendImmediately?: boolean\n ): Promise<void>;\n drawSpritesString(\n offsetX: number,\n offsetY: number,\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[],\n sendImmediately?: boolean\n ): Promise<void>;\n assertLoadedSpriteSheet(spriteSheetName: string): void;\n assertSelectedSpriteSheet(spriteSheetName: string): void;\n assertAnySelectedSpriteSheet(): void;\n assertSprite(spriteName: string): void;\n getSprite(spriteName: string): DisplaySprite | undefined;\n getSpriteSheetPalette(\n paletteName: string\n ): DisplaySpriteSheetPalette | undefined;\n getSpriteSheetPaletteSwap(\n paletteSwapName: string\n ): DisplaySpriteSheetPaletteSwap | undefined;\n getSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string\n ): DisplaySpritePaletteSwap | undefined;\n\n drawSpriteFromSpriteSheet(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n ): Promise<void>;\n\n get selectedSpriteSheet(): DisplaySpriteSheet | undefined;\n get selectedSpriteSheetName(): string | undefined;\n\n spriteSheets: Record<string, DisplaySpriteSheet>;\n spriteSheetIndices: Record<string, number>;\n\n assertSpriteSheetPalette(paletteName: string): void;\n assertSpriteSheetPaletteSwap(paletteSwapName: string): void;\n assertSpritePaletteSwap(spriteName: string, paletteSwapName: string): void;\n selectSpriteSheetPalette(\n paletteName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectSpriteSheetPaletteSwap(\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n selectSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ): Promise<void>;\n\n serializeSpriteSheet(spriteSheet: DisplaySpriteSheet): ArrayBuffer;\n\n startSprite(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ): Promise<void>;\n endSprite(sendImmediately?: boolean): Promise<void>;\n}\n\nexport async function runDisplayContextCommand(\n displayManager: DisplayManagerInterface,\n command: DisplayContextCommand,\n sendImmediately?: boolean\n) {\n if (command.hide) {\n return;\n }\n switch (command.type) {\n case \"show\":\n await displayManager.show(sendImmediately);\n break;\n case \"clear\":\n await displayManager.clear(sendImmediately);\n break;\n case \"saveContext\":\n //await displayManager.saveContext(sendImmediately);\n break;\n case \"restoreContext\":\n //await displayManager.restoreContext(sendImmediately);\n break;\n case \"clearRotation\":\n await displayManager.clearRotation(sendImmediately);\n break;\n case \"clearCrop\":\n await displayManager.clearCrop(sendImmediately);\n break;\n case \"clearRotationCrop\":\n await displayManager.clearRotationCrop(sendImmediately);\n break;\n case \"resetBitmapScale\":\n await displayManager.resetBitmapScale(sendImmediately);\n break;\n case \"resetSpriteScale\":\n await displayManager.resetSpriteScale(sendImmediately);\n break;\n case \"setColor\":\n {\n const { colorIndex, color } = command;\n await displayManager.setColor(colorIndex, color, sendImmediately);\n }\n break;\n case \"setColorOpacity\":\n {\n const { colorIndex, opacity } = command;\n await displayManager.setColorOpacity(\n colorIndex,\n opacity,\n sendImmediately\n );\n }\n break;\n case \"setOpacity\":\n {\n const { opacity } = command;\n await displayManager.setOpacity(opacity, sendImmediately);\n }\n break;\n case \"selectBackgroundColor\":\n {\n const { backgroundColorIndex } = command;\n await displayManager.selectBackgroundColor(\n backgroundColorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectFillColor\":\n {\n const { fillColorIndex } = command;\n await displayManager.selectFillColor(fillColorIndex, sendImmediately);\n }\n break;\n case \"selectLineColor\":\n {\n const { lineColorIndex } = command;\n await displayManager.selectLineColor(lineColorIndex, sendImmediately);\n }\n break;\n case \"setIgnoreFill\":\n {\n const { ignoreFill } = command;\n await displayManager.setIgnoreFill(ignoreFill, sendImmediately);\n }\n break;\n case \"setIgnoreLine\":\n {\n const { ignoreLine } = command;\n await displayManager.setIgnoreLine(ignoreLine, sendImmediately);\n }\n break;\n case \"setFillBackground\":\n {\n const { fillBackground } = command;\n await displayManager.setFillBackground(fillBackground, sendImmediately);\n }\n break;\n case \"setLineWidth\":\n {\n const { lineWidth } = command;\n await displayManager.setLineWidth(lineWidth, sendImmediately);\n }\n break;\n case \"setRotation\":\n {\n let { rotation, isRadians } = command;\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation;\n await displayManager.setRotation(rotation, true, sendImmediately);\n }\n break;\n case \"setSegmentStartCap\":\n {\n const { segmentStartCap } = command;\n await displayManager.setSegmentStartCap(\n segmentStartCap,\n sendImmediately\n );\n }\n break;\n case \"setSegmentEndCap\":\n {\n const { segmentEndCap } = command;\n await displayManager.setSegmentEndCap(segmentEndCap, sendImmediately);\n }\n break;\n case \"setSegmentCap\":\n {\n const { segmentCap } = command;\n await displayManager.setSegmentCap(segmentCap, sendImmediately);\n }\n break;\n case \"setSegmentStartRadius\":\n {\n const { segmentStartRadius } = command;\n await displayManager.setSegmentStartRadius(\n segmentStartRadius,\n sendImmediately\n );\n }\n break;\n case \"setSegmentEndRadius\":\n {\n const { segmentEndRadius } = command;\n await displayManager.setSegmentEndRadius(\n segmentEndRadius,\n sendImmediately\n );\n }\n break;\n case \"setSegmentRadius\":\n {\n const { segmentRadius } = command;\n await displayManager.setSegmentRadius(segmentRadius, sendImmediately);\n }\n break;\n case \"setHorizontalAlignment\":\n {\n const { horizontalAlignment } = command;\n await displayManager.setHorizontalAlignment(\n horizontalAlignment,\n sendImmediately\n );\n }\n break;\n case \"setVerticalAlignment\":\n {\n const { verticalAlignment } = command;\n await displayManager.setVerticalAlignment(\n verticalAlignment,\n sendImmediately\n );\n }\n break;\n case \"resetAlignment\":\n {\n await displayManager.resetAlignment(sendImmediately);\n }\n break;\n case \"setCropTop\":\n {\n const { cropTop } = command;\n await displayManager.setCropTop(cropTop, sendImmediately);\n }\n break;\n case \"setCropRight\":\n {\n const { cropRight } = command;\n await displayManager.setCropRight(cropRight, sendImmediately);\n }\n break;\n case \"setCropBottom\":\n {\n const { cropBottom } = command;\n await displayManager.setCropBottom(cropBottom, sendImmediately);\n }\n break;\n case \"setCropLeft\":\n {\n const { cropLeft } = command;\n await displayManager.setCropLeft(cropLeft, sendImmediately);\n }\n break;\n case \"setRotationCropTop\":\n {\n const { rotationCropTop } = command;\n await displayManager.setRotationCropTop(\n rotationCropTop,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropRight\":\n {\n const { rotationCropRight } = command;\n await displayManager.setRotationCropRight(\n rotationCropRight,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropBottom\":\n {\n const { rotationCropBottom } = command;\n await displayManager.setRotationCropBottom(\n rotationCropBottom,\n sendImmediately\n );\n }\n break;\n case \"setRotationCropLeft\":\n {\n const { rotationCropLeft } = command;\n await displayManager.setRotationCropLeft(\n rotationCropLeft,\n sendImmediately\n );\n }\n break;\n case \"selectBitmapColor\":\n {\n const { bitmapColorIndex, colorIndex } = command;\n await displayManager.selectBitmapColor(\n bitmapColorIndex,\n colorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectBitmapColors\":\n {\n const { bitmapColorPairs } = command;\n await displayManager.selectBitmapColors(\n bitmapColorPairs,\n sendImmediately\n );\n }\n break;\n case \"setBitmapScaleX\":\n {\n const { bitmapScaleX } = command;\n await displayManager.setBitmapScaleX(bitmapScaleX, sendImmediately);\n }\n break;\n case \"setBitmapScaleY\":\n {\n const { bitmapScaleY } = command;\n await displayManager.setBitmapScaleY(bitmapScaleY, sendImmediately);\n }\n break;\n case \"setBitmapScale\":\n {\n const { bitmapScale } = command;\n await displayManager.setBitmapScale(bitmapScale, sendImmediately);\n }\n break;\n case \"selectSpriteColor\":\n {\n const { spriteColorIndex, colorIndex } = command;\n await displayManager.selectSpriteColor(\n spriteColorIndex,\n colorIndex,\n sendImmediately\n );\n }\n break;\n case \"selectSpriteColors\":\n {\n const { spriteColorPairs } = command;\n await displayManager.selectSpriteColors(\n spriteColorPairs,\n sendImmediately\n );\n }\n break;\n case \"setSpriteScaleX\":\n {\n const { spriteScaleX } = command;\n await displayManager.setSpriteScaleX(spriteScaleX, sendImmediately);\n }\n break;\n case \"setSpriteScaleY\":\n {\n const { spriteScaleY } = command;\n await displayManager.setSpriteScaleY(spriteScaleY, sendImmediately);\n }\n break;\n case \"setSpriteScale\":\n {\n const { spriteScale } = command;\n await displayManager.setSpriteScale(spriteScale, sendImmediately);\n }\n break;\n\n case \"clearRect\":\n {\n const { x, y, width, height } = command;\n await displayManager.clearRect(x, y, width, height, sendImmediately);\n }\n break;\n case \"drawRect\":\n {\n const { offsetX, offsetY, width, height } = command;\n await displayManager.drawRect(\n offsetX,\n offsetY,\n width,\n height,\n sendImmediately\n );\n }\n break;\n case \"drawRoundRect\":\n {\n const { offsetX, offsetY, width, height, borderRadius } = command;\n await displayManager.drawRoundRect(\n offsetX,\n offsetY,\n width,\n height,\n borderRadius,\n sendImmediately\n );\n }\n break;\n case \"drawCircle\":\n {\n const { offsetX, offsetY, radius } = command;\n await displayManager.drawCircle(\n offsetX,\n offsetY,\n radius,\n sendImmediately\n );\n }\n break;\n case \"drawEllipse\":\n {\n const { offsetX, offsetY, radiusX, radiusY } = command;\n await displayManager.drawEllipse(\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n sendImmediately\n );\n }\n break;\n case \"drawPolygon\":\n {\n const { points } = command;\n await displayManager.drawPolygon(points, sendImmediately);\n }\n break;\n case \"drawRegularPolygon\":\n {\n const { offsetX, offsetY, radius, numberOfSides } = command;\n await displayManager.drawRegularPolygon(\n offsetX,\n offsetY,\n radius,\n numberOfSides,\n sendImmediately\n );\n }\n break;\n case \"drawWireframe\":\n {\n const { wireframe } = command;\n await displayManager.drawWireframe(wireframe, sendImmediately);\n }\n break;\n case \"drawSegment\":\n {\n const { startX, startY, endX, endY } = command;\n await displayManager.drawSegment(\n startX,\n startY,\n endX,\n endY,\n sendImmediately\n );\n }\n break;\n case \"drawSegments\":\n {\n const { points } = command;\n await displayManager.drawSegments(\n points.map(({ x, y }) => ({ x: x, y: y })),\n sendImmediately\n );\n }\n break;\n case \"drawArc\":\n {\n let { offsetX, offsetY, radius, startAngle, angleOffset, isRadians } =\n command;\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n\n await displayManager.drawArc(\n offsetX,\n offsetY,\n radius,\n startAngle,\n angleOffset,\n true,\n sendImmediately\n );\n }\n break;\n case \"drawArcEllipse\":\n {\n let {\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n isRadians,\n } = command;\n startAngle = isRadians ? startAngle : degToRad(startAngle);\n angleOffset = isRadians ? angleOffset : degToRad(angleOffset);\n\n await displayManager.drawArcEllipse(\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n true,\n sendImmediately\n );\n }\n break;\n case \"drawBitmap\":\n {\n const { offsetX, offsetY, bitmap } = command;\n await displayManager.drawBitmap(\n offsetX,\n offsetY,\n bitmap,\n sendImmediately\n );\n }\n break;\n case \"drawSprite\":\n {\n const { offsetX, offsetY, spriteIndex } = command;\n const spriteName =\n displayManager.selectedSpriteSheet?.sprites[spriteIndex].name!;\n await displayManager.drawSprite(\n offsetX,\n offsetY,\n spriteName,\n sendImmediately\n );\n }\n break;\n case \"selectSpriteSheet\":\n {\n const { spriteSheetIndex } = command;\n const spriteSheetName = Object.entries(\n displayManager.spriteSheetIndices\n ).find((entry) => entry[1] == spriteSheetIndex)?.[0];\n await displayManager.selectSpriteSheet(\n spriteSheetName!,\n sendImmediately\n );\n }\n break;\n case \"resetSpriteColors\":\n await displayManager.resetSpriteColors(sendImmediately);\n break;\n\n case \"drawQuadraticBezierCurve\":\n {\n const { controlPoints } = command;\n await displayManager.drawQuadraticBezierCurve(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawQuadraticBezierCurves\":\n {\n const { controlPoints } = command;\n await displayManager.drawQuadraticBezierCurves(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawCubicBezierCurve\":\n {\n const { controlPoints } = command;\n await displayManager.drawCubicBezierCurve(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawCubicBezierCurves\":\n {\n const { controlPoints } = command;\n await displayManager.drawCubicBezierCurves(\n controlPoints,\n sendImmediately\n );\n }\n break;\n case \"drawClosedPath\":\n {\n const { curves } = command;\n await displayManager.drawClosedPath(curves, sendImmediately);\n }\n break;\n case \"drawPath\":\n {\n const { curves } = command;\n await displayManager.drawPath(curves, sendImmediately);\n }\n break;\n case \"startSprite\":\n {\n const { offsetX, offsetY, width, height } = command;\n await displayManager.startSprite(\n offsetX,\n offsetY,\n width,\n height,\n sendImmediately\n );\n }\n break;\n case \"endSprite\":\n await displayManager.endSprite(sendImmediately);\n break;\n }\n}\n\nexport async function runDisplayContextCommands(\n displayManager: DisplayManagerInterface,\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n) {\n _console.log(\"runDisplayContextCommands\", commands);\n commands\n .filter((command) => !command.hide)\n .forEach((command) => {\n runDisplayContextCommand(displayManager, command, false);\n });\n if (sendImmediately) {\n displayManager.flushContextCommands();\n }\n}\n\nexport function assertLoadedSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheetName: string\n) {\n _console.assertWithError(\n displayManager.spriteSheets[spriteSheetName],\n `spriteSheet \"${spriteSheetName}\" not loaded`\n );\n}\nexport function assertSelectedSpriteSheet(\n displayManager: DisplayManagerInterface,\n spriteSheetName: string\n) {\n displayManager.assertLoadedSpriteSheet(spriteSheetName);\n _console.assertWithError(\n displayManager.selectedSpriteSheetName == spriteSheetName,\n `spriteSheet \"${spriteSheetName}\" not selected`\n );\n}\nexport function assertAnySelectedSpriteSheet(\n displayManager: DisplayManagerInterface\n) {\n _console.assertWithError(\n displayManager.selectedSpriteSheet,\n \"no spriteSheet selected\"\n );\n}\nexport function getSprite(\n displayManager: DisplayManagerInterface,\n spriteName: string\n): DisplaySprite | undefined {\n displayManager.assertAnySelectedSpriteSheet();\n return displayManager.selectedSpriteSheet!.sprites.find(\n (sprite) => sprite.name == spriteName\n );\n}\nexport function assertSprite(\n displayManager: DisplayManagerInterface,\n spriteName: string\n) {\n displayManager.assertAnySelectedSpriteSheet();\n const sprite = displayManager.getSprite(spriteName);\n _console.assertWithError(sprite, `no sprite found with name \"${spriteName}\"`);\n}\nexport function getSpriteSheetPalette(\n displayManager: DisplayManagerInterface,\n paletteName: string\n): DisplaySpriteSheetPalette | undefined {\n return displayManager.selectedSpriteSheet?.palettes?.find(\n (palette) => palette.name == paletteName\n );\n}\nexport function getSpriteSheetPaletteSwap(\n displayManager: DisplayManagerInterface,\n paletteSwapName: string\n): DisplaySpriteSheetPaletteSwap | undefined {\n return displayManager.selectedSpriteSheet?.paletteSwaps?.find(\n (paletteSwap) => paletteSwap.name == paletteSwapName\n );\n}\nexport function getSpritePaletteSwap(\n displayManager: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string\n): DisplaySpritePaletteSwap | undefined {\n return displayManager\n .getSprite(spriteName)\n ?.paletteSwaps?.find((paletteSwap) => paletteSwap.name == paletteSwapName);\n}\n\nexport function assertSpriteSheetPalette(\n displayManagerInterface: DisplayManagerInterface,\n paletteName: string\n) {\n const spriteSheetPalette =\n displayManagerInterface.getSpriteSheetPalette(paletteName);\n _console.assertWithError(\n spriteSheetPalette,\n `no spriteSheetPalette found with name \"${paletteName}\"`\n );\n}\nexport function assertSpriteSheetPaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n paletteSwapName: string\n) {\n const spriteSheetPaletteSwap =\n displayManagerInterface.getSpriteSheetPaletteSwap(paletteSwapName);\n _console.assertWithError(\n spriteSheetPaletteSwap,\n `no paletteSwapName found with name \"${paletteSwapName}\"`\n );\n}\nexport function assertSpritePaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string\n) {\n const spritePaletteSwap = displayManagerInterface.getSpritePaletteSwap(\n spriteName,\n paletteSwapName\n );\n _console.assertWithError(\n spritePaletteSwap,\n `no spritePaletteSwap found for sprite \"${spriteName}\" name \"${paletteSwapName}\"`\n );\n}\nexport async function selectSpriteSheetPalette(\n displayManagerInterface: DisplayManagerInterface,\n paletteName: string,\n offset?: number,\n indicesOnly?: boolean,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n\n displayManagerInterface.assertAnySelectedSpriteSheet();\n displayManagerInterface.assertSpriteSheetPalette(paletteName);\n const palette = displayManagerInterface.getSpriteSheetPalette(paletteName)!;\n\n _console.assertWithError(\n palette.numberOfColors + offset <= displayManagerInterface.numberOfColors,\n `invalid offset ${offset} and palette.numberOfColors ${palette.numberOfColors} (max ${displayManagerInterface.numberOfColors})`\n );\n\n //_console.log({ indicesOnly });\n for (let index = 0; index < palette.numberOfColors; index++) {\n if (!indicesOnly) {\n const color = palette.colors[index];\n let opacity = palette.opacities?.[index];\n if (opacity == undefined) {\n opacity = 1;\n }\n //_console.log({ index, offset, color });\n displayManagerInterface.setColor(index + offset, color, false);\n displayManagerInterface.setColorOpacity(index + offset, opacity, false);\n }\n displayManagerInterface.selectSpriteColor(index, index + offset);\n }\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\nexport async function selectSpriteSheetPaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n displayManagerInterface.assertAnySelectedSpriteSheet();\n displayManagerInterface.assertSpriteSheetPaletteSwap(paletteSwapName);\n\n const paletteSwap =\n displayManagerInterface.getSpriteSheetPaletteSwap(paletteSwapName)!;\n\n const spriteColorPairs: DisplaySpriteColorPair[] = [];\n for (\n let spriteColorIndex = 0;\n spriteColorIndex < paletteSwap.numberOfColors;\n spriteColorIndex++\n ) {\n const colorIndex = paletteSwap.spriteColorIndices[spriteColorIndex];\n spriteColorPairs.push({\n spriteColorIndex: spriteColorIndex + offset,\n colorIndex,\n });\n }\n displayManagerInterface.selectSpriteColors(spriteColorPairs, false);\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\nexport async function selectSpritePaletteSwap(\n displayManagerInterface: DisplayManagerInterface,\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n) {\n offset = offset || 0;\n displayManagerInterface.assertAnySelectedSpriteSheet();\n\n const paletteSwap = displayManagerInterface.getSpritePaletteSwap(\n spriteName,\n paletteSwapName\n )!;\n\n const spriteColorPairs: DisplaySpriteColorPair[] = [];\n for (\n let spriteColorIndex = 0;\n spriteColorIndex < paletteSwap.numberOfColors;\n spriteColorIndex++\n ) {\n const colorIndex = paletteSwap.spriteColorIndices[spriteColorIndex];\n spriteColorPairs.push({\n spriteColorIndex: spriteColorIndex + offset,\n colorIndex,\n });\n }\n displayManagerInterface.selectSpriteColors(spriteColorPairs, false);\n\n if (sendImmediately) {\n displayManagerInterface.flushContextCommands();\n }\n}\n\nexport async function drawSpriteFromSpriteSheet(\n displayManagerInterface: DisplayManagerInterface,\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n) {\n const reducedSpriteSheet = reduceSpriteSheet(spriteSheet, [spriteName]);\n await displayManagerInterface.uploadSpriteSheet(reducedSpriteSheet);\n await displayManagerInterface.selectSpriteSheet(spriteSheet.name);\n await displayManagerInterface.drawSprite(\n offsetX,\n offsetY,\n spriteName,\n sendImmediately\n );\n if (paletteName != undefined) {\n await displayManagerInterface.selectSpriteSheetPalette(paletteName);\n }\n}\n","import Device, { SendMessageCallback } from \"./Device.ts\";\nimport {\n concatenateArrayBuffers,\n UInt8ByteBuffer,\n} from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport autoBind from \"auto-bind\";\nimport {\n clamp,\n degToRad,\n normalizeRadians,\n Vector2,\n} from \"./utils/MathUtils.ts\";\nimport { rgbToHex, stringToRGB } from \"./utils/ColorUtils.ts\";\nimport DisplayContextStateHelper from \"./utils/DisplayContextStateHelper.ts\";\nimport {\n assertValidColor,\n assertValidDisplayBrightness,\n assertValidSegmentCap,\n DisplayScaleDirection,\n DisplayBitmapScaleDirectionToCommandType,\n DisplayColorRGB,\n DisplayCropDirection,\n DisplayCropDirections,\n DisplayCropDirectionToCommandType,\n DisplayCropDirectionToStateKey,\n DisplayRotationCropDirectionToCommandType,\n DisplayRotationCropDirectionToStateKey,\n maxDisplayScale,\n roundScale,\n DisplaySpriteScaleDirectionToCommandType,\n minDisplayScale,\n assertValidAlignment,\n DisplayAlignmentDirectionToCommandType,\n DisplayAlignmentDirectionToStateKey,\n assertValidDirection,\n assertValidAlignmentDirection,\n assertValidWireframe,\n trimWireframe,\n assertValidNumberOfControlPoints,\n assertValidPathNumberOfControlPoints,\n assertValidPath,\n isWireframePolygon,\n} from \"./utils/DisplayUtils.ts\";\nimport {\n assertValidBitmapPixels,\n drawBitmapHeaderLength,\n getBitmapNumberOfBytes,\n imageToBitmap,\n quantizeImage,\n resizeAndQuantizeImage,\n} from \"./utils/DisplayBitmapUtils.ts\";\nimport {\n DefaultDisplayContextState,\n DisplayAlignment,\n DisplayAlignmentDirection,\n DisplayContextState,\n DisplayContextStateKey,\n DisplayDirection,\n DisplaySegmentCap,\n PartialDisplayContextState,\n} from \"./utils/DisplayContextState.ts\";\nimport {\n DisplayContextCommand,\n DisplayContextCommandType,\n DisplayContextCommandTypes,\n serializeContextCommand,\n} from \"./utils/DisplayContextCommand.ts\";\nimport {\n assertAnySelectedSpriteSheet,\n assertLoadedSpriteSheet,\n assertSelectedSpriteSheet,\n assertSprite,\n assertSpritePaletteSwap,\n assertSpriteSheetPalette,\n assertSpriteSheetPaletteSwap,\n DisplayManagerInterface,\n drawSpriteFromSpriteSheet,\n getSprite,\n getSpritePaletteSwap,\n getSpriteSheetPalette,\n getSpriteSheetPaletteSwap,\n runDisplayContextCommand,\n runDisplayContextCommands,\n selectSpritePaletteSwap,\n selectSpriteSheetPalette,\n selectSpriteSheetPaletteSwap,\n} from \"./utils/DisplayManagerInterface.ts\";\nimport { SendFileCallback } from \"./FileTransferManager.ts\";\nimport { textDecoder, textEncoder } from \"./utils/Text.ts\";\nimport {\n DisplaySprite,\n DisplaySpritePaletteSwap,\n DisplaySpriteSheetPalette,\n DisplaySpriteSheetPaletteSwap,\n serializeSpriteSheet,\n DisplaySpriteSheet,\n DisplaySpriteLines,\n stringToSpriteLines,\n DisplaySpriteSerializedSubLine,\n DisplaySpriteSerializedLine,\n DisplaySpriteSerializedLines,\n stringToSpriteLinesMetrics,\n} from \"./utils/DisplaySpriteSheetUtils.ts\";\nimport { wait } from \"./utils/Timer.ts\";\n\nconst _console = createConsole(\"DisplayManager\", { log: false });\n\nexport const DefaultNumberOfDisplayColors = 16;\n\nexport const DisplayCommands = [\"sleep\", \"wake\"] as const;\nexport type DisplayCommand = (typeof DisplayCommands)[number];\n\nexport const DisplayStatuses = [\"awake\", \"asleep\"] as const;\nexport type DisplayStatus = (typeof DisplayStatuses)[number];\n\nexport const DisplayInformationTypes = [\n \"type\",\n \"width\",\n \"height\",\n \"pixelDepth\",\n] as const;\nexport type DisplayInformationType = (typeof DisplayInformationTypes)[number];\n\nexport const DisplayTypes = [\n \"none\",\n \"generic\",\n \"monocularLeft\",\n \"monocularRight\",\n \"binocular\",\n] as const;\nexport type DisplayType = (typeof DisplayTypes)[number];\n\nexport const DisplayPixelDepths = [\"1\", \"2\", \"4\"] as const;\nexport type DisplayPixelDepth = (typeof DisplayPixelDepths)[number];\n\nexport const DisplayBrightnesses = [\n \"veryLow\",\n \"low\",\n \"medium\",\n \"high\",\n \"veryHigh\",\n] as const;\nexport type DisplayBrightness = (typeof DisplayBrightnesses)[number];\n\nexport const DisplayMessageTypes = [\n \"isDisplayAvailable\",\n \"displayStatus\",\n \"displayInformation\",\n \"displayCommand\",\n \"getDisplayBrightness\",\n \"setDisplayBrightness\",\n \"displayContextCommands\",\n \"displayReady\",\n \"getSpriteSheetName\",\n \"setSpriteSheetName\",\n \"spriteSheetIndex\",\n] as const;\nexport type DisplayMessageType = (typeof DisplayMessageTypes)[number];\n\nexport type DisplaySize = {\n width: number;\n height: number;\n};\nexport type DisplayInformation = {\n type: DisplayType;\n width: number;\n height: number;\n pixelDepth: DisplayPixelDepth;\n};\n\nexport type DisplayBitmapColorPair = {\n bitmapColorIndex: number;\n colorIndex: number;\n};\n\nexport type DisplaySpriteColorPair = {\n spriteColorIndex: number;\n colorIndex: number;\n};\n\nexport type DisplayWireframeEdge = {\n startIndex: number;\n endIndex: number;\n};\nexport type DisplaySegment = {\n start: Vector2;\n end: Vector2;\n};\nexport type DisplayWireframe = {\n points: Vector2[];\n edges: DisplayWireframeEdge[];\n};\n\nexport const DisplayBezierCurveTypes = [\n \"segment\",\n \"quadratic\",\n \"cubic\",\n] as const;\nexport type DisplayBezierCurveType = (typeof DisplayBezierCurveTypes)[number];\nexport type DisplayBezierCurve = {\n type: DisplayBezierCurveType;\n controlPoints: Vector2[];\n};\n\nexport const displayCurveTypeBitWidth = 2;\nexport const displayCurveTypesPerByte = 8 / displayCurveTypeBitWidth;\n\nexport const DisplayPointDataTypes = [\"int8\", \"int16\", \"float\"] as const;\nexport type DisplayPointDataType = (typeof DisplayPointDataTypes)[number];\nexport const displayPointDataTypeToSize: Record<DisplayPointDataType, number> =\n {\n int8: 1 * 2,\n int16: 2 * 2,\n float: 4 * 2,\n };\nexport const displayPointDataTypeToRange: Record<\n DisplayPointDataType,\n { min: number; max: number }\n> = {\n int8: { min: -(2 ** 7), max: 2 ** 7 - 1 },\n int16: { min: -(2 ** 15), max: 2 ** 15 - 1 },\n float: { min: -Infinity, max: Infinity },\n};\n\nexport const DisplayInformationValues = {\n type: DisplayTypes,\n pixelDepth: DisplayPixelDepths,\n};\n\nexport const RequiredDisplayMessageTypes: DisplayMessageType[] = [\n \"isDisplayAvailable\",\n \"displayInformation\",\n \"displayStatus\",\n \"getDisplayBrightness\",\n] as const;\n\nexport const DisplayEventTypes = [\n ...DisplayMessageTypes,\n \"displayContextState\",\n \"displayColor\",\n \"displayColorOpacity\",\n \"displayOpacity\",\n \"displaySpriteSheetUploadStart\",\n \"displaySpriteSheetUploadProgress\",\n \"displaySpriteSheetUploadComplete\",\n] as const;\nexport type DisplayEventType = (typeof DisplayEventTypes)[number];\n\nexport interface DisplayEventMessages {\n isDisplayAvailable: { isDisplayAvailable: boolean };\n displayStatus: {\n displayStatus: DisplayStatus;\n previousDisplayStatus: DisplayStatus;\n };\n displayInformation: {\n displayInformation: DisplayInformation;\n };\n getDisplayBrightness: {\n displayBrightness: DisplayBrightness;\n };\n displayContextState: {\n displayContextState: DisplayContextState;\n differences: DisplayContextStateKey[];\n };\n displayColor: {\n colorIndex: number;\n colorRGB: DisplayColorRGB;\n colorHex: string;\n };\n displayColorOpacity: {\n opacity: number;\n colorIndex: number;\n };\n displayOpacity: {\n opacity: number;\n };\n displayReady: {};\n getSpriteSheetName: {\n spriteSheetName: string;\n };\n\n displaySpriteSheetUploadStart: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n };\n displaySpriteSheetUploadProgress: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n progress: number;\n };\n displaySpriteSheetUploadComplete: {\n spriteSheetName: string;\n spriteSheet: DisplaySpriteSheet;\n };\n displayContextCommands: {};\n}\n\nexport type DisplayEventDispatcher = EventDispatcher<\n Device,\n DisplayEventType,\n DisplayEventMessages\n>;\nexport type SendDisplayMessageCallback =\n SendMessageCallback<DisplayMessageType>;\n\nexport const MinSpriteSheetNameLength = 1;\nexport const MaxSpriteSheetNameLength = 30;\n\nexport type DisplayBitmap = {\n width: number;\n height: number;\n numberOfColors: number;\n pixels: number[];\n};\n\nclass DisplayManager implements DisplayManagerInterface {\n constructor() {\n autoBind(this);\n }\n\n sendMessage!: SendDisplayMessageCallback;\n\n eventDispatcher!: DisplayEventDispatcher;\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n requestRequiredInformation() {\n _console.log(\"requesting required display information\");\n const messages = RequiredDisplayMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendMessage(messages, false);\n }\n\n // IS DISPLAY AVAILABLE\n #isAvailable = false;\n get isAvailable() {\n return this.#isAvailable;\n }\n\n #assertDisplayIsAvailable() {\n _console.assertWithError(this.#isAvailable, \"display is not available\");\n }\n\n #parseIsDisplayAvailable(dataView: DataView) {\n const newIsDisplayAvailable = dataView.getUint8(0) == 1;\n this.#isAvailable = newIsDisplayAvailable;\n _console.log({ isDisplayAvailable: this.#isAvailable });\n this.#dispatchEvent(\"isDisplayAvailable\", {\n isDisplayAvailable: this.#isAvailable,\n });\n }\n\n // DISPLAY CONTEXT STATE\n #contextStateHelper = new DisplayContextStateHelper();\n get contextState() {\n return this.#contextStateHelper.state;\n }\n #onContextStateUpdate(differences: DisplayContextStateKey[]) {\n this.#dispatchEvent(\"displayContextState\", {\n displayContextState: structuredClone(this.contextState),\n differences,\n });\n }\n async setContextState(\n newState: PartialDisplayContextState,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.diff(newState);\n if (differences.length == 0) {\n return;\n }\n differences.forEach((difference) => {\n switch (difference) {\n case \"backgroundColorIndex\":\n this.selectBackgroundColor(newState.backgroundColorIndex!);\n break;\n case \"fillBackground\":\n this.setFillBackground(newState.fillBackground!);\n break;\n case \"ignoreFill\":\n this.setIgnoreFill(newState.ignoreFill!);\n break;\n case \"ignoreLine\":\n this.setIgnoreLine(newState.ignoreLine!);\n break;\n case \"fillColorIndex\":\n this.selectFillColor(newState.fillColorIndex!);\n break;\n case \"lineColorIndex\":\n this.selectLineColor(newState.lineColorIndex!);\n break;\n case \"lineWidth\":\n this.setLineWidth(newState.lineWidth!);\n break;\n case \"horizontalAlignment\":\n this.setHorizontalAlignment(newState.horizontalAlignment!);\n break;\n case \"verticalAlignment\":\n this.setVerticalAlignment(newState.verticalAlignment!);\n break;\n case \"rotation\":\n this.setRotation(newState.rotation!, true);\n break;\n case \"segmentStartCap\":\n this.setSegmentStartCap(newState.segmentStartCap!);\n break;\n case \"segmentEndCap\":\n this.setSegmentEndCap(newState.segmentEndCap!);\n break;\n case \"segmentStartRadius\":\n this.setSegmentStartRadius(newState.segmentStartRadius!);\n break;\n case \"segmentEndRadius\":\n this.setSegmentEndRadius(newState.segmentEndRadius!);\n break;\n case \"cropTop\":\n this.setCropTop(newState.cropTop!);\n break;\n case \"cropRight\":\n this.setCropRight(newState.cropRight!);\n break;\n case \"cropBottom\":\n this.setCropBottom(newState.cropBottom!);\n break;\n case \"cropLeft\":\n this.setCropLeft(newState.cropLeft!);\n break;\n case \"rotationCropTop\":\n this.setRotationCropTop(newState.rotationCropTop!);\n break;\n case \"rotationCropRight\":\n this.setRotationCropRight(newState.rotationCropRight!);\n break;\n case \"rotationCropBottom\":\n this.setRotationCropBottom(newState.rotationCropBottom!);\n break;\n case \"rotationCropLeft\":\n this.setRotationCropLeft(newState.rotationCropLeft!);\n break;\n case \"bitmapColorIndices\":\n const bitmapColors: DisplayBitmapColorPair[] = [];\n newState.bitmapColorIndices!.forEach(\n (colorIndex, bitmapColorIndex) => {\n bitmapColors.push({ bitmapColorIndex, colorIndex });\n }\n );\n this.selectBitmapColors(bitmapColors);\n break;\n case \"bitmapScaleX\":\n this.setBitmapScaleX(newState.bitmapScaleX!);\n break;\n case \"bitmapScaleY\":\n this.setBitmapScaleY(newState.bitmapScaleY!);\n break;\n case \"spriteColorIndices\":\n const spriteColors: DisplaySpriteColorPair[] = [];\n newState.spriteColorIndices!.forEach(\n (colorIndex, spriteColorIndex) => {\n spriteColors.push({ spriteColorIndex, colorIndex });\n }\n );\n this.selectSpriteColors(spriteColors);\n break;\n case \"spriteScaleX\":\n this.setSpriteScaleX(newState.spriteScaleX!);\n break;\n case \"spriteScaleY\":\n this.setSpriteScaleY(newState.spriteScaleY!);\n break;\n case \"spritesLineHeight\":\n this.setSpritesLineHeight(newState.spritesLineHeight!);\n break;\n case \"spritesDirection\":\n this.setSpritesDirection(newState.spritesDirection!);\n break;\n case \"spritesLineDirection\":\n this.setSpritesLineDirection(newState.spritesLineDirection!);\n break;\n case \"spritesSpacing\":\n this.setSpritesSpacing(newState.spritesSpacing!);\n break;\n case \"spritesLineSpacing\":\n this.setSpritesLineSpacing(newState.spritesLineSpacing!);\n break;\n case \"spritesAlignment\":\n this.setSpritesAlignment(newState.spritesAlignment!);\n break;\n case \"spritesLineAlignment\":\n this.setSpritesLineAlignment(newState.spritesLineAlignment!);\n break;\n }\n });\n if (sendImmediately) {\n await this.#sendContextCommands();\n }\n }\n\n // DISPLAY STATUS\n #displayStatus!: DisplayStatus;\n get displayStatus() {\n return this.#displayStatus;\n }\n get isDisplayAwake() {\n return this.#displayStatus == \"awake\";\n }\n #parseDisplayStatus(dataView: DataView) {\n const displayStatusIndex = dataView.getUint8(0);\n const newDisplayStatus = DisplayStatuses[displayStatusIndex];\n this.#updateDisplayStatus(newDisplayStatus);\n }\n #updateDisplayStatus(newDisplayStatus: DisplayStatus) {\n _console.assertEnumWithError(newDisplayStatus, DisplayStatuses);\n if (newDisplayStatus == this.#displayStatus) {\n _console.log(`redundant displayStatus ${newDisplayStatus}`);\n return;\n }\n const previousDisplayStatus = this.#displayStatus;\n this.#displayStatus = newDisplayStatus;\n _console.log(`updated displayStatus to \"${this.displayStatus}\"`);\n this.#dispatchEvent(\"displayStatus\", {\n displayStatus: this.displayStatus,\n previousDisplayStatus,\n });\n }\n\n // DISPLAY COMMAND\n async #sendDisplayCommand(\n command: DisplayCommand,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(command, DisplayCommands);\n _console.log(`sending display command \"${command}\"`);\n\n const promise = this.waitForEvent(\"displayStatus\");\n _console.log(`setting command \"${command}\"`);\n const commandEnum = DisplayCommands.indexOf(command);\n\n this.sendMessage(\n [\n {\n type: \"displayCommand\",\n data: UInt8ByteBuffer(commandEnum),\n },\n ],\n sendImmediately\n );\n\n await promise;\n }\n #assertIsAwake() {\n _console.assertWithError(\n this.#displayStatus == \"awake\",\n `display is not awake - currently ${this.#displayStatus}`\n );\n }\n #assertIsNotAwake() {\n _console.assertWithError(\n this.#displayStatus != \"awake\",\n `display is awake`\n );\n }\n\n async wake() {\n this.#assertIsNotAwake();\n await this.#sendDisplayCommand(\"wake\");\n }\n async sleep() {\n this.#assertIsAwake();\n await this.#sendDisplayCommand(\"sleep\");\n }\n async toggle() {\n switch (this.displayStatus) {\n case \"asleep\":\n this.wake();\n break;\n case \"awake\":\n this.sleep();\n break;\n }\n }\n\n get numberOfColors() {\n return 2 ** Number(this.pixelDepth!);\n }\n\n // INFORMATION\n #displayInformation?: DisplayInformation;\n get displayInformation() {\n return this.#displayInformation!;\n }\n\n get pixelDepth() {\n return this.#displayInformation?.pixelDepth!;\n }\n get width() {\n return this.#displayInformation?.width!;\n }\n get height() {\n return this.#displayInformation?.width!;\n }\n get size() {\n return {\n width: this.width!,\n height: this.height!,\n };\n }\n get type() {\n return this.#displayInformation?.type!;\n }\n\n #parseDisplayInformation(dataView: DataView) {\n // @ts-expect-error\n const parsedDisplayInformation: DisplayInformation = {};\n\n let byteOffset = 0;\n while (byteOffset < dataView.byteLength) {\n const displayInformationTypeIndex = dataView.getUint8(byteOffset++);\n const displayInformationType =\n DisplayInformationTypes[displayInformationTypeIndex];\n _console.assertWithError(\n displayInformationType,\n `invalid displayInformationTypeIndex ${displayInformationType}`\n );\n _console.log({ displayInformationType });\n\n switch (displayInformationType) {\n case \"width\":\n case \"height\":\n {\n const value = dataView.getUint16(byteOffset, true);\n parsedDisplayInformation[displayInformationType] = value;\n byteOffset += 2;\n }\n break;\n case \"pixelDepth\":\n case \"type\":\n {\n const values = DisplayInformationValues[displayInformationType];\n let rawValue = dataView.getUint8(byteOffset++);\n const value = values[rawValue];\n _console.assertEnumWithError(value, values);\n // @ts-expect-error\n parsedDisplayInformation[displayInformationType] = value;\n }\n break;\n }\n }\n\n _console.log({ parsedDisplayInformation });\n const missingDisplayInformationType = DisplayInformationTypes.find(\n (type) => !(type in parsedDisplayInformation)\n );\n _console.assertWithError(\n !missingDisplayInformationType,\n `missingDisplayInformationType ${missingDisplayInformationType}`\n );\n this.#displayInformation = parsedDisplayInformation;\n this.#colors = new Array(this.numberOfColors).fill(\"#000000\");\n this.#opacities = new Array(this.numberOfColors).fill(1);\n this.contextState.bitmapColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.contextState.spriteColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.#dispatchEvent(\"displayInformation\", {\n displayInformation: this.#displayInformation,\n });\n }\n\n // DISPLAY BRIGHTNESS\n #brightness!: DisplayBrightness;\n get brightness() {\n return this.#brightness;\n }\n\n #parseDisplayBrightness(dataView: DataView) {\n const newDisplayBrightnessEnum = dataView.getUint8(0);\n const newDisplayBrightness = DisplayBrightnesses[newDisplayBrightnessEnum];\n assertValidDisplayBrightness(newDisplayBrightness);\n\n this.#brightness = newDisplayBrightness;\n _console.log({ displayBrightness: this.#brightness });\n this.#dispatchEvent(\"getDisplayBrightness\", {\n displayBrightness: this.#brightness,\n });\n }\n\n async setBrightness(\n newDisplayBrightness: DisplayBrightness,\n sendImmediately?: boolean\n ) {\n this.#assertDisplayIsAvailable();\n assertValidDisplayBrightness(newDisplayBrightness);\n if (this.brightness == newDisplayBrightness) {\n _console.log(`redundant displayBrightness ${newDisplayBrightness}`);\n return;\n }\n const newDisplayBrightnessEnum =\n DisplayBrightnesses.indexOf(newDisplayBrightness);\n const newDisplayBrightnessData = UInt8ByteBuffer(newDisplayBrightnessEnum);\n\n const promise = this.waitForEvent(\"getDisplayBrightness\");\n this.sendMessage(\n [{ type: \"setDisplayBrightness\", data: newDisplayBrightnessData }],\n sendImmediately\n );\n await promise;\n }\n\n // DISPLAY CONTEXT\n #assertValidDisplayContextCommandType(\n displayContextCommand: DisplayContextCommandType\n ) {\n _console.assertEnumWithError(\n displayContextCommand,\n DisplayContextCommandTypes\n );\n }\n\n get #maxCommandDataLength() {\n return this.mtu - 7;\n }\n #contextCommandBuffers: ArrayBuffer[] = [];\n async #sendContextCommand(\n contextCommandType: DisplayContextCommandType,\n arrayBuffer?: ArrayBufferLike,\n sendImmediately?: boolean\n ) {\n this.#assertValidDisplayContextCommandType(contextCommandType);\n _console.log(\n \"sendContextCommand\",\n { displayContextCommand: contextCommandType, sendImmediately },\n arrayBuffer\n );\n const displayContextCommandEnum =\n DisplayContextCommandTypes.indexOf(contextCommandType);\n const _arrayBuffer = concatenateArrayBuffers(\n UInt8ByteBuffer(displayContextCommandEnum),\n arrayBuffer\n );\n const newLength = this.#contextCommandBuffers.reduce(\n (sum, buffer) => sum + buffer.byteLength,\n _arrayBuffer.byteLength\n );\n if (newLength > this.#maxCommandDataLength) {\n _console.log(\"displayContextCommandBuffers too full - sending now\");\n await this.#sendContextCommands();\n }\n this.#contextCommandBuffers.push(_arrayBuffer);\n if (sendImmediately) {\n await this.#sendContextCommands();\n }\n }\n async #sendContextCommands() {\n if (this.#contextCommandBuffers.length == 0) {\n return;\n }\n const data = concatenateArrayBuffers(this.#contextCommandBuffers);\n _console.log(\n `sending displayContextCommands`,\n this.#contextCommandBuffers.slice(),\n data\n );\n this.#contextCommandBuffers.length = 0;\n await this.sendMessage([{ type: \"displayContextCommands\", data }], true);\n this.#dispatchEvent(\"displayContextCommands\", {});\n }\n async flushContextCommands() {\n await this.#sendContextCommands();\n }\n async show(sendImmediately = true) {\n _console.log(\"showDisplay\");\n this.#isReady = false;\n this.#lastShowRequestTime = Date.now();\n await this.#sendContextCommand(\"show\", undefined, sendImmediately);\n }\n async clear(sendImmediately = true) {\n _console.log(\"clearDisplay\");\n this.#isReady = false;\n this.#lastShowRequestTime = Date.now();\n await this.#sendContextCommand(\"clear\", undefined, sendImmediately);\n }\n\n assertValidColorIndex(colorIndex: number) {\n _console.assertRangeWithError(\n \"colorIndex\",\n colorIndex,\n 0,\n this.numberOfColors\n );\n }\n #colors: string[] = [];\n get colors() {\n return this.#colors;\n }\n async setColor(\n colorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n let colorRGB: DisplayColorRGB;\n if (typeof color == \"string\") {\n colorRGB = stringToRGB(color);\n } else {\n colorRGB = color;\n }\n const colorHex = rgbToHex(colorRGB);\n if (this.colors[colorIndex] == colorHex) {\n _console.log(`redundant color #${colorIndex} ${colorHex}`);\n return;\n }\n\n //_console.log(`setting color #${colorIndex}`, colorRGB);\n this.assertValidColorIndex(colorIndex);\n assertValidColor(colorRGB);\n const dataView = new DataView(new ArrayBuffer(4));\n dataView.setUint8(0, colorIndex);\n dataView.setUint8(1, colorRGB.r);\n dataView.setUint8(2, colorRGB.g);\n dataView.setUint8(3, colorRGB.b);\n await this.#sendContextCommand(\n \"setColor\",\n dataView.buffer,\n sendImmediately\n );\n this.colors[colorIndex] = colorHex;\n this.#dispatchEvent(\"displayColor\", {\n colorIndex,\n colorRGB,\n colorHex,\n });\n }\n #opacities: number[] = [];\n get opacities() {\n return this.#opacities;\n }\n async setColorOpacity(\n colorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"setColorOpacity\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n colorIndex,\n opacity,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#opacities[colorIndex] = opacity;\n this.#dispatchEvent(\"displayColorOpacity\", { colorIndex, opacity });\n }\n async setOpacity(opacity: number, sendImmediately?: boolean) {\n const commandType: DisplayContextCommandType = \"setOpacity\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n opacity,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#opacities.fill(opacity);\n this.#dispatchEvent(\"displayOpacity\", { opacity });\n }\n\n #contextStack: DisplayContextState[] = [];\n #saveContext(sendImmediately?: boolean) {\n this.#contextStack.push(structuredClone(this.contextState));\n }\n #restoreContext(sendImmediately?: boolean) {\n const contextState = this.#contextStack.pop();\n if (!contextState) {\n _console.warn(\"#contextStack empty\");\n return;\n }\n this.setContextState(contextState, sendImmediately);\n }\n async saveContext(sendImmediately?: boolean) {\n if (true) {\n this.#saveContext(sendImmediately);\n } else {\n // const commandType: DisplayContextCommandType = \"saveContext\";\n // const dataView = serializeContextCommand(this, { type: commandType });\n // await this.#sendDisplayContextCommand(\n // commandType,\n // dataView?.buffer,\n // sendImmediately\n // );\n }\n }\n async restoreContext(sendImmediately?: boolean) {\n if (true) {\n this.#restoreContext(sendImmediately);\n } else {\n // const commandType: DisplayContextCommandType = \"restoreContext\";\n // const dataView = serializeContextCommand(this, { type: commandType });\n // await this.#sendDisplayContextCommand(\n // commandType,\n // dataView?.buffer,\n // sendImmediately\n // );\n }\n }\n\n async selectFillColor(fillColorIndex: number, sendImmediately?: boolean) {\n this.assertValidColorIndex(fillColorIndex);\n const differences = this.#contextStateHelper.update({\n fillColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectFillColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n fillColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async selectBackgroundColor(\n backgroundColorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(backgroundColorIndex);\n const differences = this.#contextStateHelper.update({\n backgroundColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBackgroundColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n backgroundColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async selectLineColor(lineColorIndex: number, sendImmediately?: boolean) {\n this.assertValidColorIndex(lineColorIndex);\n const differences = this.#contextStateHelper.update({\n lineColorIndex,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectLineColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n lineColorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setIgnoreFill(ignoreFill: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n ignoreFill,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setIgnoreFill\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n ignoreFill,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setIgnoreLine(ignoreLine: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n ignoreLine,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setIgnoreLine\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n ignoreLine,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setFillBackground(fillBackground: boolean, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n fillBackground,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setFillBackground\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n fillBackground,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n assertValidLineWidth(lineWidth: number) {\n _console.assertRangeWithError(\n \"lineWidth\",\n lineWidth,\n 0,\n Math.max(this.width, this.height)\n );\n }\n async setLineWidth(lineWidth: number, sendImmediately?: boolean) {\n this.assertValidLineWidth(lineWidth);\n const differences = this.#contextStateHelper.update({\n lineWidth,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setLineWidth\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n lineWidth,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setAlignment(\n alignmentDirection: DisplayAlignmentDirection,\n alignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n assertValidAlignmentDirection(alignmentDirection);\n const alignmentCommand =\n DisplayAlignmentDirectionToCommandType[alignmentDirection];\n const alignmentKey =\n DisplayAlignmentDirectionToStateKey[alignmentDirection];\n const differences = this.#contextStateHelper.update({\n [alignmentKey]: alignment,\n });\n _console.log({ alignmentKey, alignment, differences });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: alignmentCommand,\n [alignmentKey]: alignment,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n alignmentCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setHorizontalAlignment(\n horizontalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setAlignment(\"horizontal\", horizontalAlignment, sendImmediately);\n }\n async setVerticalAlignment(\n verticalAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setAlignment(\"vertical\", verticalAlignment, sendImmediately);\n }\n async resetAlignment(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n verticalAlignment: DefaultDisplayContextState.verticalAlignment,\n horizontalAlignment: DefaultDisplayContextState.horizontalAlignment,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetAlignment\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setRotation(\n rotation: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n rotation = isRadians ? rotation : degToRad(rotation);\n rotation = normalizeRadians(rotation);\n isRadians = true;\n const differences = this.#contextStateHelper.update({\n rotation,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setRotation\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n rotation,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async clearRotation(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n rotation: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearRotation\";\n const dataView = serializeContextCommand(this, { type: commandType });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSegmentStartCap(\n segmentStartCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentStartCap);\n const differences = this.#contextStateHelper.update({\n segmentStartCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentStartCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentStartCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentEndCap(\n segmentEndCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentEndCap);\n const differences = this.#contextStateHelper.update({\n segmentEndCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentEndCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentEndCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentCap(\n segmentCap: DisplaySegmentCap,\n sendImmediately?: boolean\n ) {\n assertValidSegmentCap(segmentCap);\n const differences = this.#contextStateHelper.update({\n segmentStartCap: segmentCap,\n segmentEndCap: segmentCap,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentCap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentCap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSegmentStartRadius(\n segmentStartRadius: number,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.update({\n segmentStartRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentStartRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentStartRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentEndRadius(\n segmentEndRadius: number,\n sendImmediately?: boolean\n ) {\n const differences = this.#contextStateHelper.update({\n segmentEndRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentEndRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentEndRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSegmentRadius(segmentRadius: number, sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n segmentStartRadius: segmentRadius,\n segmentEndRadius: segmentRadius,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSegmentRadius\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n segmentRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(cropDirection, DisplayCropDirections);\n crop = Math.max(0, crop);\n const cropCommand = DisplayCropDirectionToCommandType[cropDirection];\n const cropKey = DisplayCropDirectionToStateKey[cropDirection];\n const differences = this.#contextStateHelper.update({\n [cropKey]: crop,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: cropCommand,\n [cropKey]: crop,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n cropCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setCropTop(cropTop: number, sendImmediately?: boolean) {\n await this.setCrop(\"top\", cropTop, sendImmediately);\n }\n async setCropRight(cropRight: number, sendImmediately?: boolean) {\n await this.setCrop(\"right\", cropRight, sendImmediately);\n }\n async setCropBottom(cropBottom: number, sendImmediately?: boolean) {\n await this.setCrop(\"bottom\", cropBottom, sendImmediately);\n }\n async setCropLeft(cropLeft: number, sendImmediately?: boolean) {\n await this.setCrop(\"left\", cropLeft, sendImmediately);\n }\n async clearCrop(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n cropTop: 0,\n cropRight: 0,\n cropBottom: 0,\n cropLeft: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearCrop\";\n const dataView = serializeContextCommand(this, { type: commandType });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setRotationCrop(\n cropDirection: DisplayCropDirection,\n crop: number,\n sendImmediately?: boolean\n ) {\n _console.assertEnumWithError(cropDirection, DisplayCropDirections);\n const cropCommand =\n DisplayRotationCropDirectionToCommandType[cropDirection];\n const cropKey = DisplayRotationCropDirectionToStateKey[cropDirection];\n const differences = this.#contextStateHelper.update({\n [cropKey]: crop,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-ignore\n const dataView = serializeContextCommand(this, {\n type: cropCommand,\n [cropKey]: crop,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n cropCommand,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setRotationCropTop(rotationCropTop: number, sendImmediately?: boolean) {\n await this.setRotationCrop(\"top\", rotationCropTop, sendImmediately);\n }\n async setRotationCropRight(\n rotationCropRight: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"right\", rotationCropRight, sendImmediately);\n }\n async setRotationCropBottom(\n rotationCropBottom: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"bottom\", rotationCropBottom, sendImmediately);\n }\n async setRotationCropLeft(\n rotationCropLeft: number,\n sendImmediately?: boolean\n ) {\n await this.setRotationCrop(\"left\", rotationCropLeft, sendImmediately);\n }\n async clearRotationCrop(sendImmediately?: boolean) {\n const differences = this.#contextStateHelper.update({\n rotationCropTop: 0,\n rotationCropRight: 0,\n rotationCropBottom: 0,\n rotationCropLeft: 0,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"clearRotationCrop\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async selectBitmapColor(\n bitmapColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(bitmapColorIndex);\n this.assertValidColorIndex(colorIndex);\n const bitmapColorIndices = this.contextState.bitmapColorIndices.slice();\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n const differences = this.#contextStateHelper.update({\n bitmapColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBitmapColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n bitmapColorIndex,\n colorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n get bitmapColorIndices() {\n return this.contextState.bitmapColorIndices;\n }\n get bitmapColors() {\n return this.bitmapColorIndices.map((colorIndex) => this.colors[colorIndex]);\n }\n async selectBitmapColors(\n bitmapColorPairs: DisplayBitmapColorPair[],\n sendImmediately?: boolean\n ) {\n _console.assertRangeWithError(\n \"bitmapColors\",\n bitmapColorPairs.length,\n 1,\n this.numberOfColors\n );\n const bitmapColorIndices = this.contextState.bitmapColorIndices.slice();\n bitmapColorPairs.forEach(({ bitmapColorIndex, colorIndex }) => {\n this.assertValidColorIndex(bitmapColorIndex);\n this.assertValidColorIndex(colorIndex);\n bitmapColorIndices[bitmapColorIndex] = colorIndex;\n });\n\n const differences = this.#contextStateHelper.update({\n bitmapColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectBitmapColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n bitmapColorPairs,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setBitmapColor(\n bitmapColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n return this.setColor(\n this.bitmapColorIndices[bitmapColorIndex],\n color,\n sendImmediately\n );\n }\n async setBitmapColorOpacity(\n bitmapColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n return this.setColorOpacity(\n this.bitmapColorIndices[bitmapColorIndex],\n opacity,\n sendImmediately\n );\n }\n async setBitmapScaleDirection(\n direction: DisplayScaleDirection,\n bitmapScale: number,\n sendImmediately?: boolean\n ) {\n bitmapScale = clamp(bitmapScale, minDisplayScale, maxDisplayScale);\n bitmapScale = roundScale(bitmapScale);\n const commandType = DisplayBitmapScaleDirectionToCommandType[direction];\n _console.log({ [commandType]: bitmapScale });\n const newState: PartialDisplayContextState = {};\n let command: DisplayContextCommand;\n switch (direction) {\n case \"all\":\n newState.bitmapScaleX = bitmapScale;\n newState.bitmapScaleY = bitmapScale;\n command = { type: \"setBitmapScale\", bitmapScale };\n break;\n case \"x\":\n newState.bitmapScaleX = bitmapScale;\n command = { type: \"setBitmapScaleX\", bitmapScaleX: bitmapScale };\n break;\n case \"y\":\n newState.bitmapScaleY = bitmapScale;\n command = { type: \"setBitmapScaleY\", bitmapScaleY: bitmapScale };\n break;\n }\n const differences = this.#contextStateHelper.update(newState);\n if (differences.length == 0) {\n return;\n }\n const dataView = serializeContextCommand(this, command);\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async setBitmapScaleX(bitmapScaleX: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"x\", bitmapScaleX, sendImmediately);\n }\n async setBitmapScaleY(bitmapScaleY: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"y\", bitmapScaleY, sendImmediately);\n }\n async setBitmapScale(bitmapScale: number, sendImmediately?: boolean) {\n return this.setBitmapScaleDirection(\"all\", bitmapScale, sendImmediately);\n }\n async resetBitmapScale(sendImmediately?: boolean) {\n //return this.setBitmapScaleDirection(\"all\", 1, sendImmediately);\n\n const differences = this.#contextStateHelper.update({\n bitmapScaleX: 1,\n bitmapScaleY: 1,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetBitmapScale\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async selectSpriteColor(\n spriteColorIndex: number,\n colorIndex: number,\n sendImmediately?: boolean\n ) {\n this.assertValidColorIndex(spriteColorIndex);\n this.assertValidColorIndex(colorIndex);\n const spriteColorIndices = this.contextState.spriteColorIndices.slice();\n spriteColorIndices[spriteColorIndex] = colorIndex;\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectSpriteColor\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteColorIndex,\n colorIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n get spriteColorIndices() {\n return this.contextState.spriteColorIndices;\n }\n get spriteColors() {\n return this.spriteColorIndices.map((colorIndex) => this.colors[colorIndex]);\n }\n async selectSpriteColors(\n spriteColorPairs: DisplaySpriteColorPair[],\n sendImmediately?: boolean\n ) {\n _console.assertRangeWithError(\n \"spriteColors\",\n spriteColorPairs.length,\n 1,\n this.numberOfColors\n );\n const spriteColorIndices = this.contextState.spriteColorIndices.slice();\n spriteColorPairs.forEach(({ spriteColorIndex, colorIndex }) => {\n this.assertValidColorIndex(spriteColorIndex);\n this.assertValidColorIndex(colorIndex);\n spriteColorIndices[spriteColorIndex] = colorIndex;\n });\n\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"selectSpriteColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteColorPairs,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpriteColor(\n spriteColorIndex: number,\n color: DisplayColorRGB | string,\n sendImmediately?: boolean\n ) {\n return this.setColor(\n this.spriteColorIndices[spriteColorIndex],\n color,\n sendImmediately\n );\n }\n async setSpriteColorOpacity(\n spriteColorIndex: number,\n opacity: number,\n sendImmediately?: boolean\n ) {\n return this.setColorOpacity(\n this.spriteColorIndices[spriteColorIndex],\n opacity,\n sendImmediately\n );\n }\n\n async resetSpriteColors(sendImmediately?: boolean) {\n const spriteColorIndices = new Array(this.numberOfColors).fill(0);\n const differences = this.#contextStateHelper.update({\n spriteColorIndices,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetSpriteColors\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpriteScaleDirection(\n direction: DisplayScaleDirection,\n spriteScale: number,\n sendImmediately?: boolean\n ) {\n spriteScale = clamp(spriteScale, minDisplayScale, maxDisplayScale);\n spriteScale = roundScale(spriteScale);\n const commandType = DisplaySpriteScaleDirectionToCommandType[direction];\n _console.log({ [commandType]: spriteScale });\n const newState: PartialDisplayContextState = {};\n let command: DisplayContextCommand;\n switch (direction) {\n case \"all\":\n newState.spriteScaleX = spriteScale;\n newState.spriteScaleY = spriteScale;\n command = { type: \"setSpriteScale\", spriteScale };\n break;\n case \"x\":\n newState.spriteScaleX = spriteScale;\n command = { type: \"setSpriteScaleX\", spriteScaleX: spriteScale };\n break;\n case \"y\":\n newState.spriteScaleY = spriteScale;\n command = { type: \"setSpriteScaleY\", spriteScaleY: spriteScale };\n break;\n }\n const differences = this.#contextStateHelper.update(newState);\n if (differences.length == 0) {\n return;\n }\n const dataView = serializeContextCommand(this, command);\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n\n this.#onContextStateUpdate(differences);\n }\n async setSpriteScaleX(spriteScaleX: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"x\", spriteScaleX, sendImmediately);\n }\n async setSpriteScaleY(spriteScaleY: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"y\", spriteScaleY, sendImmediately);\n }\n async setSpriteScale(spriteScale: number, sendImmediately?: boolean) {\n return this.setSpriteScaleDirection(\"all\", spriteScale, sendImmediately);\n }\n async resetSpriteScale(sendImmediately?: boolean) {\n //return this.setSpriteScaleDirection(\"all\", 1, sendImmediately);\n\n const differences = this.#contextStateHelper.update({\n spriteScaleX: 1,\n spriteScaleY: 1,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"resetSpriteScale\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n });\n await this.#sendContextCommand(\n commandType,\n dataView?.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpritesLineHeight(\n spritesLineHeight: number,\n sendImmediately?: boolean\n ) {\n this.assertValidLineWidth(spritesLineHeight);\n const differences = this.#contextStateHelper.update({\n spritesLineHeight,\n });\n if (differences.length == 0) {\n return;\n }\n const commandType: DisplayContextCommandType = \"setSpritesLineHeight\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spritesLineHeight,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n\n async setSpritesDirectionGeneric(\n direction: DisplayDirection,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n assertValidDirection(direction);\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineDirection\"\n : \"spritesDirection\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineDirection\"\n : \"setSpritesDirection\";\n\n const differences = this.#contextStateHelper.update({\n [stateKey]: direction,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: direction,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesDirection(\n spritesDirection: DisplayDirection,\n sendImmediately?: boolean\n ) {\n await this.setSpritesDirectionGeneric(\n spritesDirection,\n false,\n sendImmediately\n );\n }\n async setSpritesLineDirection(\n spritesLineDirection: DisplayDirection,\n sendImmediately?: boolean\n ) {\n await this.setSpritesDirectionGeneric(\n spritesLineDirection,\n true,\n sendImmediately\n );\n }\n\n async setSpritesSpacingGeneric(\n spacing: number,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineSpacing\"\n : \"spritesSpacing\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineSpacing\"\n : \"setSpritesSpacing\";\n\n const differences = this.#contextStateHelper.update({\n [stateKey]: spacing,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: spacing,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesSpacing(spritesSpacing: number, sendImmediately?: boolean) {\n await this.setSpritesSpacingGeneric(spritesSpacing, false, sendImmediately);\n }\n async setSpritesLineSpacing(\n spritesSpacing: number,\n sendImmediately?: boolean\n ) {\n await this.setSpritesSpacingGeneric(spritesSpacing, true, sendImmediately);\n }\n\n async setSpritesAlignmentGeneric(\n alignment: DisplayAlignment,\n isOrthogonal: boolean,\n sendImmediately?: boolean\n ) {\n assertValidAlignment(alignment);\n const stateKey: DisplayContextStateKey = isOrthogonal\n ? \"spritesLineAlignment\"\n : \"spritesAlignment\";\n const commandType: DisplayContextCommandType = isOrthogonal\n ? \"setSpritesLineAlignment\"\n : \"setSpritesAlignment\";\n const differences = this.#contextStateHelper.update({\n [stateKey]: alignment,\n });\n if (differences.length == 0) {\n return;\n }\n // @ts-expect-error\n const dataView = serializeContextCommand(this, {\n type: commandType,\n [stateKey]: alignment,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async setSpritesAlignment(\n spritesAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setSpritesAlignmentGeneric(\n spritesAlignment,\n false,\n sendImmediately\n );\n }\n async setSpritesLineAlignment(\n spritesLineAlignment: DisplayAlignment,\n sendImmediately?: boolean\n ) {\n await this.setSpritesAlignmentGeneric(\n spritesLineAlignment,\n true,\n sendImmediately\n );\n }\n\n async clearRect(\n x: number,\n y: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"clearRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n x,\n y,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRoundRect(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n borderRadius: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRoundRect\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n borderRadius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawCircle(\n offsetX: number,\n offsetY: number,\n radius: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawCircle\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawEllipse\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawRegularPolygon(\n offsetX: number,\n offsetY: number,\n radius: number,\n numberOfSides: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawRegularPolygon\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n numberOfSides,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawPolygon(points: Vector2[], sendImmediately?: boolean) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n const commandType: DisplayContextCommandType = \"drawPolygon\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n points,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawWireframe(wireframe: DisplayWireframe, sendImmediately?: boolean) {\n wireframe = trimWireframe(wireframe);\n if (wireframe.points.length == 0) {\n return;\n }\n assertValidWireframe(wireframe);\n if (this.#contextStateHelper.isSegmentUniform) {\n const polygon = isWireframePolygon(wireframe);\n if (polygon) {\n return this.drawSegments(polygon, sendImmediately);\n }\n }\n const commandType: DisplayContextCommandType = \"drawWireframe\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n wireframe,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `wireframe data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawCurve(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n assertValidNumberOfControlPoints(curveType, controlPoints);\n const commandType: DisplayContextCommandType =\n curveType == \"cubic\"\n ? \"drawCubicBezierCurve\"\n : \"drawQuadraticBezierCurve\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n controlPoints,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawCurves(\n curveType: DisplayBezierCurveType,\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n assertValidPathNumberOfControlPoints(curveType, controlPoints);\n const commandType: DisplayContextCommandType =\n curveType == \"cubic\"\n ? \"drawCubicBezierCurves\"\n : \"drawQuadraticBezierCurves\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n controlPoints,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `curve data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawQuadraticBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurve(\"quadratic\", controlPoints, sendImmediately);\n }\n async drawQuadraticBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurves(\"quadratic\", controlPoints, sendImmediately);\n }\n\n async drawCubicBezierCurve(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurve(\"cubic\", controlPoints, sendImmediately);\n }\n async drawCubicBezierCurves(\n controlPoints: Vector2[],\n sendImmediately?: boolean\n ) {\n await this.drawCurves(\"cubic\", controlPoints, sendImmediately);\n }\n\n async _drawPath(\n isClosed: boolean,\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ) {\n assertValidPath(curves);\n\n const commandType: DisplayContextCommandType = isClosed\n ? \"drawClosedPath\"\n : \"drawPath\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n curves,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n _console.error(\n `path data ${dataView.byteLength} too large (max ${\n this.#maxCommandDataLength\n })`\n );\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawPath(curves: DisplayBezierCurve[], sendImmediately?: boolean) {\n await this._drawPath(false, curves, sendImmediately);\n }\n async drawClosedPath(\n curves: DisplayBezierCurve[],\n sendImmediately?: boolean\n ) {\n await this._drawPath(true, curves, sendImmediately);\n }\n\n async drawSegment(\n startX: number,\n startY: number,\n endX: number,\n endY: number,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawSegment\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n startX,\n startY,\n endX,\n endY,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawSegments(points: Vector2[], sendImmediately?: boolean) {\n _console.assertRangeWithError(\"numberOfPoints\", points.length, 2, 255);\n const commandType: DisplayContextCommandType = \"drawSegments\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n points,\n });\n if (!dataView) {\n return;\n }\n if (dataView.byteLength > this.#maxCommandDataLength) {\n const mid = Math.floor(points.length / 2);\n const firstHalf = points.slice(0, mid + 1);\n const secondHalf = points.slice(mid);\n _console.log({ firstHalf, secondHalf });\n _console.log(\"sending first half\", firstHalf);\n await this.drawSegments(firstHalf, false);\n _console.log(\"sending second half\", secondHalf);\n await this.drawSegments(secondHalf, sendImmediately);\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawArc(\n offsetX: number,\n offsetY: number,\n radius: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawArc\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radius,\n startAngle,\n angleOffset,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async drawArcEllipse(\n offsetX: number,\n offsetY: number,\n radiusX: number,\n radiusY: number,\n startAngle: number,\n angleOffset: number,\n isRadians?: boolean,\n sendImmediately?: boolean\n ) {\n const commandType: DisplayContextCommandType = \"drawArcEllipse\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n radiusX,\n radiusY,\n startAngle,\n angleOffset,\n isRadians,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n assertValidNumberOfColors(numberOfColors: number) {\n _console.assertRangeWithError(\n \"numberOfColors\",\n numberOfColors,\n 2,\n this.numberOfColors\n );\n }\n\n assertValidBitmap(bitmap: DisplayBitmap, checkSize?: boolean) {\n this.assertValidNumberOfColors(bitmap.numberOfColors);\n assertValidBitmapPixels(bitmap);\n if (checkSize) {\n this.#assertValidBitmapSize(bitmap);\n }\n }\n #assertValidBitmapSize(bitmap: DisplayBitmap) {\n const pixelDataLength = getBitmapNumberOfBytes(bitmap);\n _console.assertRangeWithError(\n \"bitmap.pixels.length\",\n pixelDataLength,\n 1,\n this.#maxCommandDataLength - drawBitmapHeaderLength\n );\n }\n async drawBitmap(\n offsetX: number,\n offsetY: number,\n bitmap: DisplayBitmap,\n sendImmediately?: boolean\n ) {\n this.assertValidBitmap(bitmap, true);\n const commandType: DisplayContextCommandType = \"drawBitmap\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n bitmap,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async imageToBitmap(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors?: number\n ) {\n return imageToBitmap(\n image,\n width,\n height,\n this.colors,\n this.bitmapColorIndices,\n numberOfColors\n );\n }\n async quantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number\n ) {\n return quantizeImage(image, width, height, numberOfColors);\n }\n async resizeAndQuantizeImage(\n image: HTMLImageElement,\n width: number,\n height: number,\n numberOfColors: number,\n colors?: string[]\n ) {\n return resizeAndQuantizeImage(image, width, height, numberOfColors, colors);\n }\n\n // CONTEXT COMMANDS\n\n async runContextCommand(\n command: DisplayContextCommand,\n sendImmediately?: boolean\n ) {\n return runDisplayContextCommand(this, command, sendImmediately);\n }\n async runContextCommands(\n commands: DisplayContextCommand[],\n sendImmediately?: boolean\n ) {\n return runDisplayContextCommands(this, commands, sendImmediately);\n }\n\n #isReady = true;\n get isReady() {\n return this.isAvailable && this.#isReady;\n }\n #lastReadyTime = 0;\n #lastShowRequestTime = 0;\n #minReadyInterval = 60; // Forced delay due to Frame's fpga timing...\n #waitBeforeReady = true;\n async #parseDisplayReady(dataView: DataView) {\n const now = Date.now();\n const timeSinceLastDraw = now - this.#lastShowRequestTime;\n const timeSinceLastReady = now - this.#lastReadyTime;\n //_console.log(`${timeSinceLastReady}ms since last render`);\n _console.log(`${timeSinceLastDraw}ms draw time`);\n if (this.#waitBeforeReady && timeSinceLastReady < this.#minReadyInterval) {\n const timeToWait = this.#minReadyInterval - timeSinceLastReady;\n _console.log(`waiting ${timeToWait}ms`);\n await wait(timeToWait);\n }\n this.#isReady = true;\n this.#lastReadyTime = Date.now();\n this.#dispatchEvent(\"displayReady\", {});\n }\n\n // SPRITE SHEET\n #spriteSheets: Record<string, DisplaySpriteSheet> = {};\n #spriteSheetIndices: Record<string, number> = {};\n get spriteSheets() {\n return this.#spriteSheets;\n }\n get spriteSheetIndices() {\n return this.#spriteSheetIndices;\n }\n async #setSpriteSheetName(\n spriteSheetName: string,\n sendImmediately?: boolean\n ) {\n if (typeof spriteSheetName == \"number\") {\n // @ts-expect-error\n spriteSheetName = spriteSheetName.toString();\n }\n _console.assertTypeWithError(spriteSheetName, \"string\");\n _console.assertRangeWithError(\n \"newName\",\n spriteSheetName.length,\n MinSpriteSheetNameLength,\n MaxSpriteSheetNameLength\n );\n const setSpriteSheetNameData = textEncoder.encode(spriteSheetName);\n _console.log({ setSpriteSheetNameData });\n\n const promise = this.waitForEvent(\"getSpriteSheetName\");\n this.sendMessage(\n [{ type: \"setSpriteSheetName\", data: setSpriteSheetNameData.buffer }],\n sendImmediately\n );\n await promise;\n }\n #pendingSpriteSheet?: DisplaySpriteSheet;\n get pendingSpriteSheet() {\n return this.#pendingSpriteSheet;\n }\n #pendingSpriteSheetName?: string;\n get pendingSpriteSheetName() {\n return this.#pendingSpriteSheetName;\n }\n #updateSpriteSheetName(updatedSpriteSheetName: string) {\n _console.assertTypeWithError(updatedSpriteSheetName, \"string\");\n this.#pendingSpriteSheetName = updatedSpriteSheetName;\n _console.log({ updatedSpriteSheetName: this.#pendingSpriteSheetName });\n this.#dispatchEvent(\"getSpriteSheetName\", {\n spriteSheetName: this.#pendingSpriteSheetName,\n });\n }\n sendFile!: SendFileCallback;\n serializeSpriteSheet(spriteSheet: DisplaySpriteSheet): ArrayBuffer {\n return serializeSpriteSheet(this, spriteSheet);\n }\n async uploadSpriteSheet(spriteSheet: DisplaySpriteSheet) {\n if (spriteSheet.sprites.length == 0) {\n _console.log(\"no sprites in spriteSheet\");\n return;\n }\n if (this.#pendingSpriteSheet) {\n await this.waitForEvent(\"displaySpriteSheetUploadComplete\");\n await this.uploadSpriteSheet(spriteSheet);\n return;\n }\n spriteSheet = structuredClone(spriteSheet);\n this.#pendingSpriteSheet = spriteSheet;\n const buffer = this.serializeSpriteSheet(this.#pendingSpriteSheet);\n await this.#setSpriteSheetName(this.#pendingSpriteSheet.name);\n const promise = this.waitForEvent(\"displaySpriteSheetUploadComplete\");\n this.sendFile(\"spriteSheet\", buffer, true);\n await promise;\n }\n async uploadSpriteSheets(spriteSheets: DisplaySpriteSheet[]) {\n for (const spriteSheet of spriteSheets) {\n await this.uploadSpriteSheet(spriteSheet);\n }\n }\n assertLoadedSpriteSheet(spriteSheetName: string) {\n assertLoadedSpriteSheet(this, spriteSheetName);\n }\n assertSelectedSpriteSheet(spriteSheetName: string) {\n assertSelectedSpriteSheet(this, spriteSheetName);\n }\n assertAnySelectedSpriteSheet() {\n assertAnySelectedSpriteSheet(this);\n }\n assertSprite(spriteName: string) {\n return assertSprite(this, spriteName);\n }\n getSprite(spriteName: string): DisplaySprite | undefined {\n return getSprite(this, spriteName);\n }\n getSpriteSheetPalette(\n paletteName: string\n ): DisplaySpriteSheetPalette | undefined {\n return getSpriteSheetPalette(this, paletteName);\n }\n getSpriteSheetPaletteSwap(\n paletteSwapName: string\n ): DisplaySpriteSheetPaletteSwap | undefined {\n return getSpriteSheetPaletteSwap(this, paletteSwapName);\n }\n getSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string\n ): DisplaySpritePaletteSwap | undefined {\n return getSpritePaletteSwap(this, spriteName, paletteSwapName);\n }\n\n get selectedSpriteSheet() {\n if (this.contextState.spriteSheetName) {\n return this.#spriteSheets[this.contextState.spriteSheetName];\n }\n }\n get selectedSpriteSheetName() {\n return this.selectedSpriteSheet?.name;\n }\n async selectSpriteSheet(spriteSheetName: string, sendImmediately?: boolean) {\n this.assertLoadedSpriteSheet(spriteSheetName);\n const differences = this.#contextStateHelper.update({\n spriteSheetName,\n });\n if (differences.length == 0) {\n return;\n }\n const spriteSheetIndex = this.spriteSheetIndices[spriteSheetName];\n //_console.log(\"selecting\", { spriteSheetIndex, spriteSheetName });\n const commandType: DisplayContextCommandType = \"selectSpriteSheet\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n spriteSheetIndex,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n this.#onContextStateUpdate(differences);\n }\n async drawSprite(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n this.selectedSpriteSheet,\n \"no spriteSheet selected\"\n );\n _console.log(\n `drawing sprite \"${spriteName}\" in selectedSpriteSheet`,\n this.selectedSpriteSheet\n );\n let spriteIndex = this.selectedSpriteSheet!.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n spriteIndex != -1,\n `sprite \"${spriteName}\" not found in spriteSheet`\n );\n spriteIndex = spriteIndex!;\n const commandType: DisplayContextCommandType = \"drawSprite\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n spriteIndex,\n use2Bytes: this.selectedSpriteSheet!.sprites.length > 255,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawSprites(\n offsetX: number,\n offsetY: number,\n spriteLines: DisplaySpriteLines,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n this.contextState.spritesLineHeight > 0,\n `spritesLineHeight must be >0`\n );\n const spriteSerializedLines: DisplaySpriteSerializedLines = [];\n spriteLines.forEach((spriteLine) => {\n const serializedLine: DisplaySpriteSerializedLine = [];\n spriteLine.forEach((spriteSubLine) => {\n this.assertLoadedSpriteSheet(spriteSubLine.spriteSheetName);\n const spriteSheet = this.spriteSheets[spriteSubLine.spriteSheetName];\n const spriteSheetIndex = this.spriteSheetIndices[spriteSheet.name];\n const serializedSubLine: DisplaySpriteSerializedSubLine = {\n spriteSheetIndex,\n spriteIndices: [],\n use2Bytes: spriteSheet.sprites.length > 255,\n };\n spriteSubLine.spriteNames.forEach((spriteName) => {\n let spriteIndex = spriteSheet.sprites.findIndex(\n (sprite) => sprite.name == spriteName\n );\n _console.assertWithError(\n spriteIndex != -1,\n `sprite \"${spriteName}\" not found`\n );\n spriteIndex = spriteIndex!;\n serializedSubLine.spriteIndices.push(spriteIndex);\n });\n serializedLine.push(serializedSubLine);\n });\n spriteSerializedLines.push(serializedLine);\n });\n _console.log(\"spriteSerializedLines\", spriteSerializedLines);\n const commandType: DisplayContextCommandType = \"drawSprites\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n spriteSerializedLines: spriteSerializedLines,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n\n async drawSpritesString(\n offsetX: number,\n offsetY: number,\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[],\n sendImmediately?: boolean\n ) {\n const spriteLines = this.stringToSpriteLines(\n string,\n requireAll,\n maxLineBreadth,\n separators\n );\n await this.drawSprites(offsetX, offsetY, spriteLines, sendImmediately);\n }\n stringToSpriteLines(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ): DisplaySpriteLines {\n return stringToSpriteLines(\n string,\n this.spriteSheets,\n this.contextState,\n requireAll,\n maxLineBreadth,\n separators\n );\n }\n stringToSpriteLinesMetrics(\n string: string,\n requireAll?: boolean,\n maxLineBreadth?: number,\n separators?: string[]\n ) {\n return stringToSpriteLinesMetrics(\n string,\n this.spriteSheets,\n this.contextState,\n requireAll,\n maxLineBreadth,\n separators\n );\n }\n\n async drawSpriteFromSpriteSheet(\n offsetX: number,\n offsetY: number,\n spriteName: string,\n spriteSheet: DisplaySpriteSheet,\n paletteName?: string,\n sendImmediately?: boolean\n ) {\n return drawSpriteFromSpriteSheet(\n this,\n offsetX,\n offsetY,\n spriteName,\n spriteSheet,\n paletteName,\n sendImmediately\n );\n }\n\n #parseSpriteSheetIndex(dataView: DataView) {\n const spriteSheetIndex = dataView.getUint8(0);\n _console.log({\n pendingSpriteSheet: this.#pendingSpriteSheet,\n spriteSheetName: this.#pendingSpriteSheetName,\n spriteSheetIndex,\n });\n if (this.isServerSide) {\n return;\n }\n _console.assertWithError(\n this.#pendingSpriteSheetName != undefined,\n \"expected spriteSheetName when receiving spriteSheetIndex\"\n );\n _console.assertWithError(\n this.#pendingSpriteSheet != undefined,\n \"expected pendingSpriteSheet when receiving spriteSheetIndex\"\n );\n this.#spriteSheets[this.#pendingSpriteSheetName!] =\n this.#pendingSpriteSheet!;\n this.#spriteSheetIndices[this.#pendingSpriteSheetName!] = spriteSheetIndex;\n _console.log(\n `finished uploading \"${this.#pendingSpriteSheetName!}\" spriteSheet`\n );\n this.#dispatchEvent(\"displaySpriteSheetUploadComplete\", {\n spriteSheetName: this.#pendingSpriteSheetName!,\n spriteSheet: this.#pendingSpriteSheet!,\n });\n this.#pendingSpriteSheet = undefined;\n }\n\n // MESSAGE\n parseMessage(messageType: DisplayMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n\n switch (messageType) {\n case \"isDisplayAvailable\":\n this.#parseIsDisplayAvailable(dataView);\n break;\n case \"displayStatus\":\n this.#parseDisplayStatus(dataView);\n break;\n case \"displayInformation\":\n this.#parseDisplayInformation(dataView);\n break;\n case \"getDisplayBrightness\":\n case \"setDisplayBrightness\":\n this.#parseDisplayBrightness(dataView);\n break;\n case \"displayReady\":\n this.#parseDisplayReady(dataView);\n break;\n case \"getSpriteSheetName\":\n case \"setSpriteSheetName\":\n const spriteSheetName = textDecoder.decode(\n dataView.buffer as ArrayBuffer\n );\n _console.log({ spriteSheetName });\n this.#updateSpriteSheetName(spriteSheetName);\n break;\n case \"spriteSheetIndex\":\n this.#parseSpriteSheetIndex(dataView);\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n // SPRITE SHEET PALETTES\n\n assertSpriteSheetPalette(paletteName: string) {\n assertSpriteSheetPalette(this, paletteName);\n }\n assertSpriteSheetPaletteSwap(paletteSwapName: string) {\n assertSpriteSheetPaletteSwap(this, paletteSwapName);\n }\n assertSpritePaletteSwap(spriteName: string, paletteSwapName: string) {\n assertSpritePaletteSwap(this, spriteName, paletteSwapName);\n }\n async selectSpriteSheetPalette(\n paletteName: string,\n offset?: number,\n indicesOnly?: boolean,\n sendImmediately?: boolean\n ) {\n await selectSpriteSheetPalette(\n this,\n paletteName,\n offset,\n indicesOnly,\n sendImmediately\n );\n }\n async selectSpriteSheetPaletteSwap(\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ) {\n await selectSpriteSheetPaletteSwap(\n this,\n paletteSwapName,\n offset,\n sendImmediately\n );\n }\n async selectSpritePaletteSwap(\n spriteName: string,\n paletteSwapName: string,\n offset?: number,\n sendImmediately?: boolean\n ) {\n await selectSpritePaletteSwap(\n this,\n spriteName,\n paletteSwapName,\n offset,\n sendImmediately\n );\n }\n\n #isDrawingBlankSprite = false;\n async startSprite(\n offsetX: number,\n offsetY: number,\n width: number,\n height: number,\n sendImmediately?: boolean\n ) {\n _console.assertWithError(\n !this.#isDrawingBlankSprite,\n `already drawing blank sprite`\n );\n this.#isDrawingBlankSprite = true;\n this.#saveContext(sendImmediately);\n this.#contextStateHelper.reset();\n this.contextState.bitmapColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n this.contextState.spriteColorIndices = new Array(this.numberOfColors).fill(\n 0\n );\n\n const commandType: DisplayContextCommandType = \"startSprite\";\n const dataView = serializeContextCommand(this, {\n type: commandType,\n offsetX,\n offsetY,\n width,\n height,\n });\n if (!dataView) {\n return;\n }\n await this.#sendContextCommand(\n commandType,\n dataView.buffer,\n sendImmediately\n );\n }\n async endSprite(sendImmediately?: boolean) {\n this.#restoreContext(sendImmediately);\n\n _console.assertWithError(\n this.#isDrawingBlankSprite,\n `not drawing blank sprite`\n );\n this.#isDrawingBlankSprite = false;\n\n // _console.log(\"endSprite\");\n await this.#sendContextCommand(\"endSprite\", undefined, sendImmediately);\n }\n\n reset() {\n _console.log(\"clearing displayManager\");\n // @ts-ignore\n this.#displayStatus = undefined;\n this.#isAvailable = false;\n this.#displayInformation = undefined;\n // @ts-ignore\n this.#brightness = undefined;\n this.#contextCommandBuffers = [];\n this.#isAvailable = false;\n\n this.#contextStateHelper.reset();\n this.#colors.length = 0;\n this.#opacities.length = 0;\n\n this.#isReady = true;\n this.#pendingSpriteSheet = undefined;\n this.#pendingSpriteSheetName = undefined;\n\n this.isServerSide = false;\n\n this.#isDrawingBlankSprite = false;\n\n Object.keys(this.#spriteSheetIndices).forEach(\n (spriteSheetName) => delete this.#spriteSheetIndices[spriteSheetName]\n );\n Object.keys(this.#spriteSheets).forEach(\n (spriteSheetName) => delete this.#spriteSheets[spriteSheetName]\n );\n }\n\n // MTU\n #mtu!: number;\n get mtu() {\n return this.#mtu;\n }\n set mtu(newMtu: number) {\n this.#mtu = newMtu;\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n //_console.log(\"redundant isServerSide assignment\");\n return;\n }\n this.#isServerSide = newIsServerSide;\n _console.log({ isServerSide: this.isServerSide });\n }\n}\n\nexport default DisplayManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport { Timer } from \"../utils/Timer.ts\";\nimport { FileTransferMessageTypes } from \"../FileTransferManager.ts\";\nimport { TfliteMessageTypes } from \"../TfliteManager.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport { parseMessage } from \"../utils/ParseUtils.ts\";\nimport { DeviceInformationTypes } from \"../DeviceInformationManager.ts\";\nimport { InformationMessageTypes } from \"../InformationManager.ts\";\nimport { VibrationMessageTypes } from \"../vibration/VibrationManager.ts\";\nimport { SensorConfigurationMessageTypes } from \"../sensor/SensorConfigurationManager.ts\";\nimport { SensorDataMessageTypes } from \"../sensor/SensorDataManager.ts\";\nimport { WifiMessageTypes } from \"../WifiManager.ts\";\nimport { CameraMessageTypes } from \"../CameraManager.ts\";\nimport { MicrophoneMessageTypes } from \"../MicrophoneManager.ts\";\nimport { DisplayMessageTypes } from \"../DisplayManager.ts\";\n\nconst _console = createConsole(\"BaseConnectionManager\", { log: false });\n\nexport const ConnectionTypes = [\n \"webBluetooth\",\n \"noble\",\n \"client\",\n \"webSocket\",\n \"udp\",\n] as const;\nexport type ConnectionType = (typeof ConnectionTypes)[number];\n\nexport const ClientConnectionTypes = [\"noble\", \"webSocket\", \"udp\"] as const;\nexport type ClientConnectionType = (typeof ClientConnectionTypes)[number];\n\ninterface BaseConnectOptions {\n type: \"client\" | \"webBluetooth\" | \"webSocket\" | \"udp\";\n}\nexport interface WebBluetoothConnectOptions extends BaseConnectOptions {\n type: \"webBluetooth\";\n}\ninterface BaseWifiConnectOptions extends BaseConnectOptions {\n ipAddress: string;\n}\nexport interface ClientConnectOptions extends BaseConnectOptions {\n type: \"client\";\n subType?: \"noble\" | \"webSocket\" | \"udp\";\n}\nexport interface WebSocketConnectOptions extends BaseWifiConnectOptions {\n type: \"webSocket\";\n isWifiSecure?: boolean;\n}\nexport interface UDPConnectOptions extends BaseWifiConnectOptions {\n type: \"udp\";\n //sendPort: number;\n receivePort?: number;\n}\nexport type ConnectOptions =\n | WebBluetoothConnectOptions\n | WebSocketConnectOptions\n | UDPConnectOptions\n | ClientConnectOptions;\n\nexport const ConnectionStatuses = [\n \"notConnected\",\n \"connecting\",\n \"connected\",\n \"disconnecting\",\n] as const;\nexport type ConnectionStatus = (typeof ConnectionStatuses)[number];\n\nexport const ConnectionEventTypes = [\n ...ConnectionStatuses,\n \"connectionStatus\",\n \"isConnected\",\n] as const;\nexport type ConnectionEventType = (typeof ConnectionEventTypes)[number];\n\nexport interface ConnectionStatusEventMessages {\n notConnected: any;\n connecting: any;\n connected: any;\n disconnecting: any;\n connectionStatus: { connectionStatus: ConnectionStatus };\n isConnected: { isConnected: boolean };\n}\n\nexport interface TxMessage {\n type: TxRxMessageType;\n data?: ArrayBuffer;\n}\n\nexport const TxRxMessageTypes = [\n ...InformationMessageTypes,\n ...SensorConfigurationMessageTypes,\n ...SensorDataMessageTypes,\n ...VibrationMessageTypes,\n ...FileTransferMessageTypes,\n ...TfliteMessageTypes,\n ...WifiMessageTypes,\n ...CameraMessageTypes,\n ...MicrophoneMessageTypes,\n ...DisplayMessageTypes,\n] as const;\nexport type TxRxMessageType = (typeof TxRxMessageTypes)[number];\n\nexport const SMPMessageTypes = [\"smp\"] as const;\nexport type SMPMessageType = (typeof SMPMessageTypes)[number];\n\nexport const BatteryLevelMessageTypes = [\"batteryLevel\"] as const;\nexport type BatteryLevelMessageType = (typeof BatteryLevelMessageTypes)[number];\n\nexport const MetaConnectionMessageTypes = [\"rx\", \"tx\"] as const;\nexport type MetaConnectionMessageType =\n (typeof MetaConnectionMessageTypes)[number];\n\nexport const ConnectionMessageTypes = [\n ...BatteryLevelMessageTypes,\n ...DeviceInformationTypes,\n ...MetaConnectionMessageTypes,\n ...TxRxMessageTypes,\n ...SMPMessageTypes,\n] as const;\nexport type ConnectionMessageType = (typeof ConnectionMessageTypes)[number];\n\nexport type ConnectionStatusCallback = (status: ConnectionStatus) => void;\nexport type MessageReceivedCallback = (\n messageType: ConnectionMessageType,\n dataView: DataView\n) => void;\nexport type MessagesReceivedCallback = () => void;\n\nabstract class BaseConnectionManager {\n static #AssertValidTxRxMessageType(messageType: TxRxMessageType) {\n _console.assertEnumWithError(messageType, TxRxMessageTypes);\n }\n\n abstract get bluetoothId(): string;\n\n // CALLBACKS\n onStatusUpdated?: ConnectionStatusCallback;\n onMessageReceived?: MessageReceivedCallback;\n onMessagesReceived?: MessagesReceivedCallback;\n\n protected get baseConstructor() {\n return this.constructor as typeof BaseConnectionManager;\n }\n static get isSupported() {\n return false;\n }\n get isSupported() {\n return this.baseConstructor.isSupported;\n }\n\n get canUpdateFirmware() {\n return false;\n }\n\n static type: ConnectionType;\n get type(): ConnectionType {\n return this.baseConstructor.type;\n }\n\n /** @throws {Error} if not supported */\n #assertIsSupported() {\n _console.assertWithError(this.isSupported, `${this.type} is not supported`);\n }\n\n constructor() {\n this.#assertIsSupported();\n }\n\n #status: ConnectionStatus = \"notConnected\";\n get status() {\n return this.#status;\n }\n protected set status(newConnectionStatus) {\n _console.assertEnumWithError(newConnectionStatus, ConnectionStatuses);\n if (this.#status == newConnectionStatus) {\n _console.log(\n `tried to assign same connection status \"${newConnectionStatus}\"`\n );\n return;\n }\n _console.log(`new connection status \"${newConnectionStatus}\"`);\n this.#status = newConnectionStatus;\n this.onStatusUpdated!(this.status);\n\n if (this.isConnected) {\n this.#timer.start();\n } else {\n this.#timer.stop();\n }\n\n if (this.#status == \"notConnected\") {\n this.mtu = this.defaultMtu;\n }\n }\n\n get isConnected() {\n return this.status == \"connected\";\n }\n\n get isAvailable() {\n return false;\n }\n\n /** @throws {Error} if connected */\n protected assertIsNotConnected() {\n _console.assertWithError(!this.isConnected, \"device is already connected\");\n }\n /** @throws {Error} if connecting */\n #assertIsNotConnecting() {\n _console.assertWithError(\n this.status != \"connecting\",\n \"device is already connecting\"\n );\n }\n /** @throws {Error} if not connected */\n protected assertIsConnected() {\n _console.assertWithError(this.isConnected, \"device is not connected\");\n }\n /** @throws {Error} if disconnecting */\n #assertIsNotDisconnecting() {\n _console.assertWithError(\n this.status != \"disconnecting\",\n \"device is already disconnecting\"\n );\n }\n /** @throws {Error} if not connected or is disconnecting */\n assertIsConnectedAndNotDisconnecting() {\n this.assertIsConnected();\n this.#assertIsNotDisconnecting();\n }\n\n async connect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return false;\n }\n if (this.#status == \"connecting\") {\n _console.log(\"already connecting\");\n return false;\n }\n // this.assertIsNotConnected();\n // this.#assertIsNotConnecting();\n this.status = \"connecting\";\n return true;\n }\n get canReconnect() {\n return false;\n }\n async reconnect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return false;\n }\n if (this.#status == \"connecting\") {\n _console.log(\"already connecting\");\n return false;\n }\n // this.assertIsNotConnected();\n // this.#assertIsNotConnecting();\n if (!this.canReconnect) {\n _console.warn(\"unable to reconnect\");\n return false;\n }\n // _console.assertWithError(this.canReconnect, \"unable to reconnect\");\n this.status = \"connecting\";\n _console.log(\"attempting to reconnect...\");\n return true;\n }\n async disconnect() {\n if (!this.isConnected) {\n _console.log(\"already not connected\");\n return false;\n }\n if (this.#status == \"disconnecting\") {\n _console.log(\"already disconnecting\");\n return false;\n }\n // this.assertIsConnected();\n // this.#assertIsNotDisconnecting();\n this.status = \"disconnecting\";\n _console.log(\"disconnecting from device...\");\n return true;\n }\n\n async sendSmpMessage(data: ArrayBuffer) {\n this.assertIsConnectedAndNotDisconnecting();\n _console.log(\"sending smp message\", data);\n }\n\n #pendingMessages: TxMessage[] = [];\n #isSendingMessages = false;\n async sendTxMessages(\n messages: TxMessage[] | undefined,\n sendImmediately: boolean = true\n ) {\n this.assertIsConnectedAndNotDisconnecting();\n\n if (messages) {\n this.#pendingMessages.push(...messages);\n _console.log(`appended ${messages.length} messages`);\n }\n\n if (!sendImmediately) {\n _console.log(\"not sending immediately - waiting until later\");\n return;\n }\n\n if (this.#isSendingMessages) {\n _console.log(\"already sending messages - waiting until later\");\n return;\n }\n if (this.#pendingMessages.length == 0) {\n _console.log(\"no pendingMessages\");\n return;\n }\n this.#isSendingMessages = true;\n\n _console.log(\"sendTxMessages\", this.#pendingMessages.slice());\n\n const arrayBuffers = this.#pendingMessages.map((message) => {\n BaseConnectionManager.#AssertValidTxRxMessageType(message.type);\n const messageTypeEnum = TxRxMessageTypes.indexOf(message.type);\n const dataLength = new DataView(new ArrayBuffer(2));\n dataLength.setUint16(0, message.data?.byteLength || 0, true);\n return concatenateArrayBuffers(messageTypeEnum, dataLength, message.data);\n });\n this.#pendingMessages.length = 0;\n\n if (this.mtu) {\n while (arrayBuffers.length > 0) {\n if (\n arrayBuffers.every(\n (arrayBuffer) => arrayBuffer.byteLength > this.mtu! - 3\n )\n ) {\n _console.log(\"every arrayBuffer is too big to send\");\n break;\n }\n _console.log(\"remaining arrayBuffers.length\", arrayBuffers.length);\n let arrayBufferByteLength = 0;\n let arrayBufferCount = 0;\n arrayBuffers.some((arrayBuffer) => {\n if (arrayBufferByteLength + arrayBuffer.byteLength > this.mtu! - 3) {\n _console.log(\n `stopping appending arrayBuffers ( length ${arrayBuffer.byteLength} too much)`\n );\n return true;\n }\n _console.log(\n `allowing arrayBuffer with length ${arrayBuffer.byteLength}`\n );\n arrayBufferCount++;\n arrayBufferByteLength += arrayBuffer.byteLength;\n });\n const arrayBuffersToSend = arrayBuffers.splice(0, arrayBufferCount);\n _console.log({ arrayBufferCount, arrayBuffersToSend });\n\n const arrayBuffer = concatenateArrayBuffers(...arrayBuffersToSend);\n _console.log(\"sending arrayBuffer (partitioned)\", arrayBuffer);\n await this.sendTxData(arrayBuffer);\n }\n } else {\n const arrayBuffer = concatenateArrayBuffers(...arrayBuffers);\n _console.log(\"sending arrayBuffer (all)\", arrayBuffer);\n await this.sendTxData(arrayBuffer);\n }\n\n this.#isSendingMessages = false;\n\n this.sendTxMessages(undefined, true);\n }\n\n protected defaultMtu = 23;\n //mtu?: number;\n mtu?: number = this.defaultMtu;\n\n async sendTxData(data: ArrayBuffer) {\n _console.log(\"sendTxData\", data);\n }\n\n parseRxMessage(dataView: DataView) {\n parseMessage(\n dataView,\n TxRxMessageTypes,\n this.#onRxMessage.bind(this),\n null,\n true\n );\n this.onMessagesReceived!();\n }\n\n #onRxMessage(messageType: TxRxMessageType, dataView: DataView) {\n _console.log({ messageType, dataView });\n this.onMessageReceived!(messageType, dataView);\n }\n\n #timer = new Timer(this.#checkConnection.bind(this), 5000);\n #checkConnection() {\n //console.log(\"checking connection...\");\n if (!this.isConnected) {\n _console.log(\"timer detected disconnection\");\n this.status = \"notConnected\";\n }\n }\n\n clear() {\n this.#isSendingMessages = false;\n this.#pendingMessages.length = 0;\n }\n\n remove() {\n this.clear();\n\n this.onStatusUpdated = undefined;\n this.onMessageReceived = undefined;\n this.onMessagesReceived = undefined;\n }\n}\n\nexport default BaseConnectionManager;\n","import { createConsole } from \"./Console.ts\";\nimport { spacesToPascalCase } from \"./stringUtils.ts\";\n\nconst _console = createConsole(\"EventUtils\", { log: false });\n\ntype BoundEventListeners = { [eventType: string]: EventListener };\nexport type BoundGenericEventListeners = { [eventType: string]: Function };\n\nexport function bindEventListeners(\n eventTypes: readonly string[],\n boundEventListeners: BoundGenericEventListeners,\n target: any\n) {\n _console.log(\"bindEventListeners\", { eventTypes, boundEventListeners, target });\n eventTypes.forEach((eventType) => {\n const _eventType = `_on${spacesToPascalCase(eventType)}`;\n _console.assertWithError(target[_eventType], `no event \"${_eventType}\" found in target`);\n _console.log(`binding eventType \"${eventType}\" as ${_eventType} from target`, target);\n const boundEvent = target[_eventType].bind(target);\n target[_eventType] = boundEvent;\n boundEventListeners[eventType] = boundEvent;\n });\n}\n\nexport function addEventListeners(target: any, boundEventListeners: BoundGenericEventListeners) {\n let addEventListener = target.addEventListener || target.addListener || target.on || target.AddEventListener;\n _console.assertWithError(addEventListener, \"no add listener function found for target\");\n addEventListener = addEventListener.bind(target);\n Object.entries(boundEventListeners).forEach(([eventType, eventListener]) => {\n addEventListener(eventType, eventListener);\n });\n}\n\nexport function removeEventListeners(target: any, boundEventListeners: BoundGenericEventListeners) {\n let removeEventListener = target.removeEventListener || target.removeListener || target.RemoveEventListener;\n _console.assertWithError(removeEventListener, \"no remove listener function found for target\");\n removeEventListener = removeEventListener.bind(target);\n Object.entries(boundEventListeners).forEach(([eventType, eventListener]) => {\n removeEventListener(eventType, eventListener);\n });\n}\n","import {\n isInBrowser,\n isInLensStudio,\n isInNode,\n} from \"../../utils/environment.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\n\nconst _console = createConsole(\"bluetoothUUIDs\", { log: false });\n\n/** NODE_START */\nimport * as webbluetooth from \"webbluetooth\";\nvar BluetoothUUID = webbluetooth.BluetoothUUID;\n/** NODE_END */\n\n/** BROWSER_START */\nif (isInBrowser) {\n var BluetoothUUID = window.BluetoothUUID;\n}\n/** BROWSER_END */\n\n/** LS_START */\n\nvar BluetoothUUID = {\n getService: (uuid: number | string): string => toUUID(uuid),\n\n getCharacteristic: (uuid: number | string): string => toUUID(uuid),\n\n getDescriptor: (uuid: number | string): string => toUUID(uuid),\n\n getCharacteristicName: (uuid: number | string): string | null => null,\n\n getServiceName: (uuid: number | string): string | null => null,\n\n getDescriptorName: (uuid: number | string): string | null => null,\n};\n\nfunction toUUID(uuid: number | string): string {\n if (typeof uuid === \"number\") {\n uuid = uuid.toString(16).padStart(4, \"0\");\n }\n\n if (/^[0-9a-fA-F]{4,8}$/.test(uuid)) {\n return `0000${uuid.padStart(8, \"0\")}-0000-1000-8000-00805f9b34fb`;\n }\n\n return uuid.toLowerCase();\n}\n\n/** LS_END */\n\nfunction generateBluetoothUUID(value: string): BluetoothServiceUUID {\n _console.assertTypeWithError(value, \"string\");\n _console.assertWithError(\n value.length == 4,\n \"value must be 4 characters long\"\n );\n return `ea6d${value}-a725-4f9b-893d-c3913e33b39f`;\n}\n\nfunction stringToCharacteristicUUID(\n identifier: string\n): BluetoothCharacteristicUUID {\n return BluetoothUUID?.getCharacteristic?.(identifier);\n}\n\nfunction stringToServiceUUID(identifier: string): BluetoothServiceUUID {\n return BluetoothUUID?.getService?.(identifier);\n}\n\nexport type BluetoothServiceName =\n | \"deviceInformation\"\n | \"battery\"\n | \"main\"\n | \"smp\";\nimport { DeviceInformationType } from \"../../DeviceInformationManager.ts\";\nexport type BluetoothCharacteristicName =\n | DeviceInformationType\n | \"batteryLevel\"\n | \"rx\"\n | \"tx\"\n | \"smp\";\n\ninterface BluetoothCharacteristicInformation {\n uuid: BluetoothCharacteristicUUID;\n}\ninterface BluetoothServiceInformation {\n uuid: BluetoothServiceUUID;\n characteristics: {\n [characteristicName in BluetoothCharacteristicName]?: BluetoothCharacteristicInformation;\n };\n}\ninterface BluetoothServicesInformation {\n services: {\n [serviceName in BluetoothServiceName]: BluetoothServiceInformation;\n };\n}\nconst bluetoothUUIDs: BluetoothServicesInformation = Object.freeze({\n services: {\n deviceInformation: {\n uuid: stringToServiceUUID(\"device_information\"),\n characteristics: {\n manufacturerName: {\n uuid: stringToCharacteristicUUID(\"manufacturer_name_string\"),\n },\n modelNumber: {\n uuid: stringToCharacteristicUUID(\"model_number_string\"),\n },\n hardwareRevision: {\n uuid: stringToCharacteristicUUID(\"hardware_revision_string\"),\n },\n firmwareRevision: {\n uuid: stringToCharacteristicUUID(\"firmware_revision_string\"),\n },\n softwareRevision: {\n uuid: stringToCharacteristicUUID(\"software_revision_string\"),\n },\n pnpId: {\n uuid: stringToCharacteristicUUID(\"pnp_id\"),\n },\n serialNumber: {\n uuid: stringToCharacteristicUUID(\"serial_number_string\"),\n },\n },\n },\n battery: {\n uuid: stringToServiceUUID(\"battery_service\"),\n characteristics: {\n batteryLevel: {\n uuid: stringToCharacteristicUUID(\"battery_level\"),\n },\n },\n },\n main: {\n uuid: generateBluetoothUUID(\"0000\"),\n characteristics: {\n rx: { uuid: generateBluetoothUUID(\"1000\") },\n tx: { uuid: generateBluetoothUUID(\"1001\") },\n },\n },\n smp: {\n uuid: \"8d53dc1d-1db7-4cd3-868b-8a527460aa84\",\n characteristics: {\n smp: { uuid: \"da2e7828-fbce-4e01-ae9e-261174997c48\" },\n },\n },\n },\n});\n\nexport const serviceUUIDs = [bluetoothUUIDs.services.main.uuid];\nexport const optionalServiceUUIDs = [\n bluetoothUUIDs.services.deviceInformation.uuid,\n bluetoothUUIDs.services.battery.uuid,\n bluetoothUUIDs.services.smp.uuid,\n];\nexport const allServiceUUIDs = [...serviceUUIDs, ...optionalServiceUUIDs];\n\nexport function getServiceNameFromUUID(\n serviceUUID: BluetoothServiceUUID\n): BluetoothServiceName | undefined {\n serviceUUID = serviceUUID.toString().toLowerCase();\n const serviceNames = Object.keys(\n bluetoothUUIDs.services\n ) as BluetoothServiceName[];\n return serviceNames.find((serviceName) => {\n const serviceInfo = bluetoothUUIDs.services[serviceName];\n let serviceInfoUUID = serviceInfo.uuid.toString();\n if (serviceUUID.length == 4) {\n serviceInfoUUID = serviceInfoUUID.slice(4, 8);\n }\n if (!serviceUUID.includes(\"-\")) {\n serviceInfoUUID = serviceInfoUUID.replaceAll(\"-\", \"\");\n }\n return serviceUUID == serviceInfoUUID;\n });\n}\n\nexport const characteristicUUIDs: BluetoothCharacteristicUUID[] = [];\nexport const allCharacteristicUUIDs: BluetoothCharacteristicUUID[] = [];\n\nexport const characteristicNames: BluetoothCharacteristicName[] = [];\nexport const allCharacteristicNames: BluetoothCharacteristicName[] = [];\n\nObject.values(bluetoothUUIDs.services).forEach((serviceInfo) => {\n if (!serviceInfo.characteristics) {\n return;\n }\n const characteristicNames = Object.keys(\n serviceInfo.characteristics\n ) as BluetoothCharacteristicName[];\n characteristicNames.forEach((characteristicName) => {\n const characteristicInfo = serviceInfo.characteristics[characteristicName]!;\n if (serviceUUIDs.includes(serviceInfo.uuid)) {\n characteristicUUIDs.push(characteristicInfo.uuid);\n characteristicNames.push(characteristicName);\n }\n allCharacteristicUUIDs.push(characteristicInfo.uuid);\n allCharacteristicNames.push(characteristicName);\n });\n}, []);\n\n//_console.log({ characteristicUUIDs, allCharacteristicUUIDs });\n\nexport function getCharacteristicNameFromUUID(\n characteristicUUID: BluetoothCharacteristicUUID\n): BluetoothCharacteristicName | undefined {\n //_console.log({ characteristicUUID });\n characteristicUUID = characteristicUUID.toString().toLowerCase();\n var characteristicName: BluetoothCharacteristicName | undefined;\n Object.values(bluetoothUUIDs.services).some((serviceInfo) => {\n const characteristicNames = Object.keys(\n serviceInfo.characteristics\n ) as BluetoothCharacteristicName[];\n characteristicName = characteristicNames.find((_characteristicName) => {\n const characteristicInfo =\n serviceInfo.characteristics[_characteristicName]!;\n let characteristicInfoUUID = characteristicInfo.uuid.toString();\n if (characteristicUUID.length == 4) {\n characteristicInfoUUID = characteristicInfoUUID.slice(4, 8);\n }\n if (!characteristicUUID.includes(\"-\")) {\n characteristicInfoUUID = characteristicInfoUUID.replaceAll(\"-\", \"\");\n }\n return characteristicUUID == characteristicInfoUUID;\n });\n return characteristicName;\n });\n return characteristicName;\n}\n\nexport function getCharacteristicProperties(\n characteristicName: BluetoothCharacteristicName\n): BluetoothCharacteristicProperties {\n const properties = {\n broadcast: false,\n read: true,\n writeWithoutResponse: false,\n write: false,\n notify: false,\n indicate: false,\n authenticatedSignedWrites: false,\n reliableWrite: false,\n writableAuxiliaries: false,\n };\n\n // read\n switch (characteristicName) {\n case \"rx\":\n case \"tx\":\n case \"smp\":\n properties.read = false;\n break;\n }\n\n // notify\n switch (characteristicName) {\n case \"batteryLevel\":\n case \"rx\":\n case \"smp\":\n properties.notify = true;\n break;\n }\n\n // write without response\n switch (characteristicName) {\n case \"smp\":\n properties.writeWithoutResponse = true;\n break;\n }\n\n // write\n switch (characteristicName) {\n case \"tx\":\n properties.write = true;\n break;\n }\n\n return properties;\n}\n\nexport const serviceDataUUID = \"0000\";\n","import { createConsole } from \"../../utils/Console.ts\";\nimport BaseConnectionManager from \"../BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"BluetoothConnectionManager\", { log: false });\n\nimport { BluetoothCharacteristicName } from \"./bluetoothUUIDs.ts\";\n\nabstract class BluetoothConnectionManager extends BaseConnectionManager {\n get isAvailable() {\n // no way to tell if the user has turned bluetooth on or off\n return true;\n }\n\n isInRange = true;\n\n protected onCharacteristicValueChanged(\n characteristicName: BluetoothCharacteristicName,\n dataView: DataView\n ) {\n if (characteristicName == \"rx\") {\n this.parseRxMessage(dataView);\n } else {\n this.onMessageReceived?.(characteristicName, dataView);\n }\n }\n\n protected async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n _console.log(\"writeCharacteristic\", ...arguments);\n }\n\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n await this.writeCharacteristic(\"smp\", data);\n }\n\n async sendTxData(data: ArrayBuffer) {\n super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n await this.writeCharacteristic(\"tx\", data);\n }\n}\n\nexport default BluetoothConnectionManager;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport {\n isInNode,\n isInBrowser,\n isInBluefy,\n isInWebBLE,\n} from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n serviceUUIDs,\n optionalServiceUUIDs,\n getServiceNameFromUUID,\n getCharacteristicNameFromUUID,\n getCharacteristicProperties,\n} from \"./bluetoothUUIDs.ts\";\nimport BluetoothConnectionManager from \"./BluetoothConnectionManager.ts\";\nimport {\n BluetoothCharacteristicName,\n BluetoothServiceName,\n} from \"./bluetoothUUIDs.ts\";\nimport { ConnectionType } from \"../BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"WebBluetoothConnectionManager\", { log: false });\n\ntype WebBluetoothInterface = webbluetooth.Bluetooth | Bluetooth;\n\ninterface BluetoothService extends BluetoothRemoteGATTService {\n name?: BluetoothServiceName;\n}\ninterface BluetoothCharacteristic extends BluetoothRemoteGATTCharacteristic {\n name?: BluetoothCharacteristicName;\n}\n\nvar bluetooth: WebBluetoothInterface | undefined;\n/** NODE_START */\nimport * as webbluetooth from \"webbluetooth\";\nif (isInNode) {\n bluetooth = webbluetooth.bluetooth;\n}\n/** NODE_END */\n\n/** BROWSER_START */\nif (isInBrowser) {\n bluetooth = window.navigator.bluetooth;\n}\n/** BROWSER_END */\n\nclass WebBluetoothConnectionManager extends BluetoothConnectionManager {\n get bluetoothId() {\n return this.device!.id;\n }\n\n get canUpdateFirmware() {\n return this.#characteristics.has(\"smp\");\n }\n\n #boundBluetoothCharacteristicEventListeners: {\n [eventType: string]: EventListener;\n } = {\n characteristicvaluechanged: this.#onCharacteristicvaluechanged.bind(this),\n };\n #boundBluetoothDeviceEventListeners: { [eventType: string]: EventListener } =\n {\n gattserverdisconnected: this.#onGattserverdisconnected.bind(this),\n };\n\n static get isSupported() {\n return Boolean(bluetooth);\n }\n static get type(): ConnectionType {\n return \"webBluetooth\";\n }\n\n #device?: BluetoothDevice;\n get device() {\n return this.#device;\n }\n set device(newDevice) {\n if (this.#device == newDevice) {\n _console.log(\"tried to assign the same BluetoothDevice\");\n return;\n }\n if (this.#device) {\n removeEventListeners(\n this.#device,\n this.#boundBluetoothDeviceEventListeners\n );\n }\n if (newDevice) {\n addEventListeners(newDevice, this.#boundBluetoothDeviceEventListeners);\n }\n this.#device = newDevice;\n }\n\n get server(): BluetoothRemoteGATTServer | undefined {\n return this.#device?.gatt;\n }\n get isConnected() {\n return this.server?.connected || false;\n }\n\n #services: Map<BluetoothServiceName, BluetoothService> = new Map();\n #characteristics: Map<BluetoothCharacteristicName, BluetoothCharacteristic> =\n new Map();\n\n async connect() {\n const canContinue = super.connect();\n if (!canContinue) {\n return false;\n }\n\n try {\n const device = await bluetooth!.requestDevice({\n filters: [{ services: serviceUUIDs }],\n optionalServices: isInBrowser ? optionalServiceUUIDs : [],\n });\n\n _console.log(\"got BluetoothDevice\");\n this.device = device;\n\n _console.log(\"connecting to device...\");\n const server = await this.server!.connect();\n _console.log(`connected to device? ${server.connected}`);\n\n await this.#getServicesAndCharacteristics();\n\n _console.log(\"fully connected\");\n\n this.status = \"connected\";\n return true;\n } catch (error) {\n _console.error(error);\n this.status = \"notConnected\";\n this.server?.disconnect();\n await this.#removeEventListeners();\n return false;\n }\n }\n async #getServicesAndCharacteristics() {\n this.#removeEventListeners();\n\n _console.log(\"getting services...\");\n const services = await this.server!.getPrimaryServices();\n _console.log(\"got services\", services.length);\n //const service = await this.server!.getPrimaryService(\"8d53dc1d-1db7-4cd3-868b-8a527460aa84\");\n\n _console.log(\"getting characteristics...\");\n for (const serviceIndex in services) {\n const service = services[serviceIndex] as BluetoothService;\n _console.log({ service });\n const serviceName = getServiceNameFromUUID(service.uuid)!;\n _console.assertWithError(\n serviceName,\n `no name found for service uuid \"${service.uuid}\"`\n );\n _console.log(`got \"${serviceName}\" service`);\n service.name = serviceName;\n this.#services.set(serviceName, service);\n _console.log(`getting characteristics for \"${serviceName}\" service`);\n const characteristics = await service.getCharacteristics();\n _console.log(`got characteristics for \"${serviceName}\" service`);\n for (const characteristicIndex in characteristics) {\n const characteristic = characteristics[\n characteristicIndex\n ] as BluetoothCharacteristic;\n _console.log({ characteristic });\n const characteristicName = getCharacteristicNameFromUUID(\n characteristic.uuid\n )!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic uuid \"${characteristic.uuid}\" in \"${serviceName}\" service`\n );\n _console.log(\n `got \"${characteristicName}\" characteristic in \"${serviceName}\" service`\n );\n characteristic.name = characteristicName;\n this.#characteristics.set(characteristicName, characteristic);\n addEventListeners(\n characteristic,\n this.#boundBluetoothCharacteristicEventListeners\n );\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.notify) {\n _console.log(\n `starting notifications for \"${characteristicName}\" characteristic`\n );\n await characteristic.startNotifications();\n }\n if (characteristicProperties.read) {\n _console.log(`reading \"${characteristicName}\" characteristic...`);\n await characteristic.readValue();\n if (isInBluefy || isInWebBLE) {\n this.#onCharacteristicValueChanged(characteristic);\n }\n }\n }\n }\n }\n async #removeEventListeners() {\n if (this.device) {\n removeEventListeners(\n this.device,\n this.#boundBluetoothDeviceEventListeners\n );\n }\n\n const promises = Array.from(this.#characteristics.keys()).map(\n (characteristicName) => {\n const characteristic = this.#characteristics.get(characteristicName)!;\n removeEventListeners(\n characteristic,\n this.#boundBluetoothCharacteristicEventListeners\n );\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.notify) {\n _console.log(\n `stopping notifications for \"${characteristicName}\" characteristic`\n );\n return characteristic.stopNotifications();\n }\n }\n );\n\n return Promise.allSettled(promises);\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n await this.#removeEventListeners();\n this.server?.disconnect();\n this.status = \"notConnected\";\n return true;\n }\n\n #onCharacteristicvaluechanged(event: Event) {\n _console.log(\"oncharacteristicvaluechanged\");\n\n const characteristic = event.target as BluetoothCharacteristic;\n this.#onCharacteristicValueChanged(characteristic);\n }\n\n #onCharacteristicValueChanged(characteristic: BluetoothCharacteristic) {\n _console.log(\"onCharacteristicValue\");\n\n const characteristicName = characteristic.name!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic with uuid \"${characteristic.uuid}\"`\n );\n\n _console.log(\n `oncharacteristicvaluechanged for \"${characteristicName}\" characteristic`\n );\n const dataView = characteristic.value!;\n _console.assertWithError(\n dataView,\n `no data found for \"${characteristicName}\" characteristic`\n );\n _console.log(\n `data for \"${characteristicName}\" characteristic`,\n Array.from(new Uint8Array(dataView.buffer))\n );\n\n try {\n this.onCharacteristicValueChanged(characteristicName, dataView);\n } catch (error) {\n _console.error(error);\n }\n }\n\n async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n super.writeCharacteristic(characteristicName, data);\n\n const characteristic = this.#characteristics.get(characteristicName)!;\n _console.assertWithError(\n characteristic,\n `${characteristicName} characteristic not found`\n );\n _console.log(\"writing characteristic\", characteristic, data);\n const characteristicProperties =\n characteristic.properties ||\n getCharacteristicProperties(characteristicName);\n if (characteristicProperties.writeWithoutResponse) {\n _console.log(\"writing without response\");\n await characteristic.writeValueWithoutResponse(data);\n } else {\n _console.log(\"writing with response\");\n await characteristic.writeValueWithResponse(data);\n }\n _console.log(\"wrote characteristic\");\n\n if (characteristicProperties.read && !characteristicProperties.notify) {\n _console.log(\"reading value after write...\");\n await characteristic.readValue();\n if (isInBluefy || isInWebBLE) {\n this.#onCharacteristicValueChanged(characteristic);\n }\n }\n }\n\n #onGattserverdisconnected() {\n _console.log(\"gattserverdisconnected\");\n this.status = \"notConnected\";\n }\n\n get canReconnect() {\n return Boolean(this.server && !this.server.connected && this.isInRange);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n try {\n await this.server!.connect();\n } catch (error) {\n _console.error(error);\n this.isInRange = false;\n return false;\n }\n\n if (this.isConnected) {\n _console.log(\"successfully reconnected!\");\n await this.#getServicesAndCharacteristics();\n this.status = \"connected\";\n return true;\n } else {\n _console.log(\"unable to reconnect\");\n this.status = \"notConnected\";\n return false;\n }\n }\n\n remove() {\n super.remove();\n this.device = undefined;\n }\n}\n\nexport default WebBluetoothConnectionManager;\n","/*\n * The MIT License (MIT)\n *\n * Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nconst POW_2_24 = 5.960464477539063e-8;\nconst POW_2_32 = 4294967296;\nconst POW_2_53 = 9007199254740992;\n\nexport function encode(value) {\n let data = new ArrayBuffer(256);\n let dataView = new DataView(data);\n let lastLength;\n let offset = 0;\n\n function prepareWrite(length) {\n let newByteLength = data.byteLength;\n const requiredLength = offset + length;\n while (newByteLength < requiredLength) {\n newByteLength <<= 1;\n }\n if (newByteLength !== data.byteLength) {\n const oldDataView = dataView;\n data = new ArrayBuffer(newByteLength);\n dataView = new DataView(data);\n const uint32count = (offset + 3) >> 2;\n for (let i = 0; i < uint32count; ++i) {\n dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));\n }\n }\n\n lastLength = length;\n return dataView;\n }\n function commitWrite() {\n offset += lastLength;\n }\n function writeFloat64(value) {\n commitWrite(prepareWrite(8).setFloat64(offset, value));\n }\n function writeUint8(value) {\n commitWrite(prepareWrite(1).setUint8(offset, value));\n }\n function writeUint8Array(value) {\n const dataView = prepareWrite(value.length);\n for (let i = 0; i < value.length; ++i) {\n dataView.setUint8(offset + i, value[i]);\n }\n commitWrite();\n }\n function writeUint16(value) {\n commitWrite(prepareWrite(2).setUint16(offset, value));\n }\n function writeUint32(value) {\n commitWrite(prepareWrite(4).setUint32(offset, value));\n }\n function writeUint64(value) {\n const low = value % POW_2_32;\n const high = (value - low) / POW_2_32;\n const dataView = prepareWrite(8);\n dataView.setUint32(offset, high);\n dataView.setUint32(offset + 4, low);\n commitWrite();\n }\n function writeTypeAndLength(type, length) {\n if (length < 24) {\n writeUint8((type << 5) | length);\n } else if (length < 0x100) {\n writeUint8((type << 5) | 24);\n writeUint8(length);\n } else if (length < 0x10000) {\n writeUint8((type << 5) | 25);\n writeUint16(length);\n } else if (length < 0x100000000) {\n writeUint8((type << 5) | 26);\n writeUint32(length);\n } else {\n writeUint8((type << 5) | 27);\n writeUint64(length);\n }\n }\n\n function encodeItem(value) {\n let i;\n const utf8data = [];\n let length;\n\n if (value === false) {\n return writeUint8(0xf4);\n }\n if (value === true) {\n return writeUint8(0xf5);\n }\n if (value === null) {\n return writeUint8(0xf6);\n }\n if (value === undefined) {\n return writeUint8(0xf7);\n }\n\n switch (typeof value) {\n case \"number\":\n if (Math.floor(value) === value) {\n if (value >= 0 && value <= POW_2_53) {\n return writeTypeAndLength(0, value);\n }\n if (-POW_2_53 <= value && value < 0) {\n return writeTypeAndLength(1, -(value + 1));\n }\n }\n writeUint8(0xfb);\n return writeFloat64(value);\n\n case \"string\":\n for (i = 0; i < value.length; ++i) {\n let charCode = value.charCodeAt(i);\n if (charCode < 0x80) {\n utf8data.push(charCode);\n } else if (charCode < 0x800) {\n utf8data.push(0xc0 | (charCode >> 6));\n utf8data.push(0x80 | (charCode & 0x3f));\n } else if (charCode < 0xd800) {\n utf8data.push(0xe0 | (charCode >> 12));\n utf8data.push(0x80 | ((charCode >> 6) & 0x3f));\n utf8data.push(0x80 | (charCode & 0x3f));\n } else {\n charCode = (charCode & 0x3ff) << 10;\n charCode |= value.charCodeAt(++i) & 0x3ff;\n charCode += 0x10000;\n\n utf8data.push(0xf0 | (charCode >> 18));\n utf8data.push(0x80 | ((charCode >> 12) & 0x3f));\n utf8data.push(0x80 | ((charCode >> 6) & 0x3f));\n utf8data.push(0x80 | (charCode & 0x3f));\n }\n }\n\n writeTypeAndLength(3, utf8data.length);\n return writeUint8Array(utf8data);\n\n default:\n if (Array.isArray(value)) {\n length = value.length;\n writeTypeAndLength(4, length);\n for (i = 0; i < length; ++i) {\n encodeItem(value[i]);\n }\n } else if (value instanceof Uint8Array) {\n writeTypeAndLength(2, value.length);\n writeUint8Array(value);\n } else {\n const keys = Object.keys(value);\n length = keys.length;\n writeTypeAndLength(5, length);\n for (i = 0; i < length; ++i) {\n const key = keys[i];\n encodeItem(key);\n encodeItem(value[key]);\n }\n }\n }\n }\n\n encodeItem(value);\n\n if (\"slice\" in data) {\n return data.slice(0, offset);\n }\n\n const ret = new ArrayBuffer(offset);\n const retView = new DataView(ret);\n for (let i = 0; i < offset; ++i) {\n retView.setUint8(i, dataView.getUint8(i));\n }\n return ret;\n}\n\nexport function decode(data, tagger, simpleValue) {\n const dataView = new DataView(data);\n let offset = 0;\n\n if (typeof tagger !== \"function\") {\n tagger = function (value) {\n return value;\n };\n }\n if (typeof simpleValue !== \"function\") {\n simpleValue = function () {\n return undefined;\n };\n }\n\n function commitRead(length, value) {\n offset += length;\n return value;\n }\n function readArrayBuffer(length) {\n return commitRead(length, new Uint8Array(data, offset, length));\n }\n function readFloat16() {\n const tempArrayBuffer = new ArrayBuffer(4);\n const tempDataView = new DataView(tempArrayBuffer);\n const value = readUint16();\n\n const sign = value & 0x8000;\n let exponent = value & 0x7c00;\n const fraction = value & 0x03ff;\n\n if (exponent === 0x7c00) {\n exponent = 0xff << 10;\n } else if (exponent !== 0) {\n exponent += (127 - 15) << 10;\n } else if (fraction !== 0) {\n return (sign ? -1 : 1) * fraction * POW_2_24;\n }\n\n tempDataView.setUint32(0, (sign << 16) | (exponent << 13) | (fraction << 13));\n return tempDataView.getFloat32(0);\n }\n function readFloat32() {\n return commitRead(4, dataView.getFloat32(offset));\n }\n function readFloat64() {\n return commitRead(8, dataView.getFloat64(offset));\n }\n function readUint8() {\n return commitRead(1, dataView.getUint8(offset));\n }\n function readUint16() {\n return commitRead(2, dataView.getUint16(offset));\n }\n function readUint32() {\n return commitRead(4, dataView.getUint32(offset));\n }\n function readUint64() {\n return readUint32() * POW_2_32 + readUint32();\n }\n function readBreak() {\n if (dataView.getUint8(offset) !== 0xff) {\n return false;\n }\n offset += 1;\n return true;\n }\n function readLength(additionalInformation) {\n if (additionalInformation < 24) {\n return additionalInformation;\n }\n if (additionalInformation === 24) {\n return readUint8();\n }\n if (additionalInformation === 25) {\n return readUint16();\n }\n if (additionalInformation === 26) {\n return readUint32();\n }\n if (additionalInformation === 27) {\n return readUint64();\n }\n if (additionalInformation === 31) {\n return -1;\n }\n throw new Error(\"Invalid length encoding\");\n }\n function readIndefiniteStringLength(majorType) {\n const initialByte = readUint8();\n if (initialByte === 0xff) {\n return -1;\n }\n const length = readLength(initialByte & 0x1f);\n if (length < 0 || initialByte >> 5 !== majorType) {\n throw new Error(\"Invalid indefinite length element\");\n }\n return length;\n }\n\n function appendUtf16Data(utf16data, length) {\n for (let i = 0; i < length; ++i) {\n let value = readUint8();\n if (value & 0x80) {\n if (value < 0xe0) {\n value = ((value & 0x1f) << 6) | (readUint8() & 0x3f);\n length -= 1;\n } else if (value < 0xf0) {\n value = ((value & 0x0f) << 12) | ((readUint8() & 0x3f) << 6) | (readUint8() & 0x3f);\n length -= 2;\n } else {\n value =\n ((value & 0x0f) << 18) | ((readUint8() & 0x3f) << 12) | ((readUint8() & 0x3f) << 6) | (readUint8() & 0x3f);\n length -= 3;\n }\n }\n\n if (value < 0x10000) {\n utf16data.push(value);\n } else {\n value -= 0x10000;\n utf16data.push(0xd800 | (value >> 10));\n utf16data.push(0xdc00 | (value & 0x3ff));\n }\n }\n }\n\n function decodeItem() {\n const initialByte = readUint8();\n const majorType = initialByte >> 5;\n const additionalInformation = initialByte & 0x1f;\n let i;\n let length;\n\n if (majorType === 7) {\n switch (additionalInformation) {\n case 25:\n return readFloat16();\n case 26:\n return readFloat32();\n case 27:\n return readFloat64();\n }\n }\n\n length = readLength(additionalInformation);\n if (length < 0 && (majorType < 2 || majorType > 6)) {\n throw new Error(\"Invalid length\");\n }\n\n const utf16data = [];\n let retArray;\n const retObject = {};\n\n switch (majorType) {\n case 0:\n return length;\n case 1:\n return -1 - length;\n case 2:\n if (length < 0) {\n const elements = [];\n let fullArrayLength = 0;\n while ((length = readIndefiniteStringLength(majorType)) >= 0) {\n fullArrayLength += length;\n elements.push(readArrayBuffer(length));\n }\n const fullArray = new Uint8Array(fullArrayLength);\n let fullArrayOffset = 0;\n for (i = 0; i < elements.length; ++i) {\n fullArray.set(elements[i], fullArrayOffset);\n fullArrayOffset += elements[i].length;\n }\n return fullArray;\n }\n return readArrayBuffer(length);\n case 3:\n if (length < 0) {\n while ((length = readIndefiniteStringLength(majorType)) >= 0) {\n appendUtf16Data(utf16data, length);\n }\n } else {\n appendUtf16Data(utf16data, length);\n }\n return String.fromCharCode.apply(null, utf16data);\n case 4:\n if (length < 0) {\n retArray = [];\n while (!readBreak()) {\n retArray.push(decodeItem());\n }\n } else {\n retArray = new Array(length);\n for (i = 0; i < length; ++i) {\n retArray[i] = decodeItem();\n }\n }\n return retArray;\n case 5:\n for (i = 0; i < length || (length < 0 && !readBreak()); ++i) {\n const key = decodeItem();\n retObject[key] = decodeItem();\n }\n return retObject;\n case 6:\n return tagger(decodeItem(), length);\n case 7:\n switch (length) {\n case 20:\n return false;\n case 21:\n return true;\n case 22:\n return null;\n case 23:\n return undefined;\n default:\n return simpleValue(length);\n }\n }\n }\n\n const ret = decodeItem();\n if (offset !== data.byteLength) {\n throw new Error(\"Remaining bytes\");\n }\n return ret;\n}\n\nexport const CBOR = {\n encode,\n decode,\n};\n","/*\n * The MIT License (MIT)\n *\n * Copyright (c) 2023 Laird Connectivity\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * @file mcumgr\n * @brief Provides MCU manager operation functions for the Xbit USB Shell.\n * This file is inspired by the MIT licensed mcumgr file originally\n * authored by Andras Barthazi (https://github.com/boogie/mcumgr-web),\n * updated to also support file upload/download over SMP.\n */\n\nimport { CBOR } from \"./cbor.js\";\nimport { createConsole } from \"./Console.ts\";\n\nconst _console = createConsole(\"mcumgr\", { log: false });\n\nexport const constants = {\n // Opcodes\n MGMT_OP_READ: 0,\n MGMT_OP_READ_RSP: 1,\n MGMT_OP_WRITE: 2,\n MGMT_OP_WRITE_RSP: 3,\n\n // Groups\n MGMT_GROUP_ID_OS: 0,\n MGMT_GROUP_ID_IMAGE: 1,\n MGMT_GROUP_ID_STAT: 2,\n MGMT_GROUP_ID_CONFIG: 3,\n MGMT_GROUP_ID_LOG: 4,\n MGMT_GROUP_ID_CRASH: 5,\n MGMT_GROUP_ID_SPLIT: 6,\n MGMT_GROUP_ID_RUN: 7,\n MGMT_GROUP_ID_FS: 8,\n MGMT_GROUP_ID_SHELL: 9,\n\n // OS group\n OS_MGMT_ID_ECHO: 0,\n OS_MGMT_ID_CONS_ECHO_CTRL: 1,\n OS_MGMT_ID_TASKSTAT: 2,\n OS_MGMT_ID_MPSTAT: 3,\n OS_MGMT_ID_DATETIME_STR: 4,\n OS_MGMT_ID_RESET: 5,\n\n // Image group\n IMG_MGMT_ID_STATE: 0,\n IMG_MGMT_ID_UPLOAD: 1,\n IMG_MGMT_ID_FILE: 2,\n IMG_MGMT_ID_CORELIST: 3,\n IMG_MGMT_ID_CORELOAD: 4,\n IMG_MGMT_ID_ERASE: 5,\n\n // Filesystem group\n FS_MGMT_ID_FILE: 0,\n};\n\nexport class MCUManager {\n constructor() {\n this._mtu = 256;\n this._messageCallback = null;\n this._imageUploadProgressCallback = null;\n this._imageUploadNextCallback = null;\n this._fileUploadProgressCallback = null;\n this._fileUploadNextCallback = null;\n this._uploadIsInProgress = false;\n this._downloadIsInProgress = false;\n this._buffer = new Uint8Array();\n this._seq = 0;\n }\n\n onMessage(callback) {\n this._messageCallback = callback;\n return this;\n }\n\n onImageUploadNext(callback) {\n this._imageUploadNextCallback = callback;\n return this;\n }\n\n onImageUploadProgress(callback) {\n this._imageUploadProgressCallback = callback;\n return this;\n }\n\n onImageUploadFinished(callback) {\n this._imageUploadFinishedCallback = callback;\n return this;\n }\n\n onFileUploadNext(callback) {\n this._fileUploadNextCallback = callback;\n return this;\n }\n\n onFileUploadProgress(callback) {\n this._fileUploadProgressCallback = callback;\n return this;\n }\n\n onFileUploadFinished(callback) {\n this._fileUploadFinishedCallback = callback;\n return this;\n }\n\n onFileDownloadNext(callback) {\n this._fileDownloadNextCallback = callback;\n return this;\n }\n\n onFileDownloadProgress(callback) {\n this._fileDownloadProgressCallback = callback;\n return this;\n }\n\n onFileDownloadFinished(callback) {\n this._fileDownloadFinishedCallback = callback;\n return this;\n }\n\n _getMessage(op, group, id, data) {\n const _flags = 0;\n let encodedData = [];\n if (typeof data !== \"undefined\") {\n encodedData = [...new Uint8Array(CBOR.encode(data))];\n }\n const lengthLo = encodedData.length & 255;\n const lengthHi = encodedData.length >> 8;\n const groupLo = group & 255;\n const groupHi = group >> 8;\n const message = [op, _flags, lengthHi, lengthLo, groupHi, groupLo, this._seq, id, ...encodedData];\n this._seq = (this._seq + 1) % 256;\n\n return message;\n }\n\n _notification(buffer) {\n _console.log(\"mcumgr - message received\");\n const message = new Uint8Array(buffer);\n this._buffer = new Uint8Array([...this._buffer, ...message]);\n const messageLength = this._buffer[2] * 256 + this._buffer[3];\n if (this._buffer.length < messageLength + 8) return;\n this._processMessage(this._buffer.slice(0, messageLength + 8));\n this._buffer = this._buffer.slice(messageLength + 8);\n }\n\n _processMessage(message) {\n const [op, , lengthHi, lengthLo, groupHi, groupLo, , id] = message;\n const data = CBOR.decode(message.slice(8).buffer);\n const length = lengthHi * 256 + lengthLo;\n const group = groupHi * 256 + groupLo;\n\n _console.log(\"mcumgr - Process Message - Group: \" + group + \", Id: \" + id + \", Off: \" + data.off);\n if (group === constants.MGMT_GROUP_ID_IMAGE && id === constants.IMG_MGMT_ID_UPLOAD && data.off) {\n this._uploadOffset = data.off;\n this._uploadNext();\n return;\n }\n if (\n op === constants.MGMT_OP_WRITE_RSP &&\n group === constants.MGMT_GROUP_ID_FS &&\n id === constants.FS_MGMT_ID_FILE &&\n data.off\n ) {\n this._uploadFileOffset = data.off;\n this._uploadFileNext();\n return;\n }\n if (op === constants.MGMT_OP_READ_RSP && group === constants.MGMT_GROUP_ID_FS && id === constants.FS_MGMT_ID_FILE) {\n this._downloadFileOffset += data.data.length;\n if (data.len != undefined) {\n this._downloadFileLength = data.len;\n }\n _console.log(\"downloaded \" + this._downloadFileOffset + \" bytes of \" + this._downloadFileLength);\n if (this._downloadFileLength > 0) {\n this._fileDownloadProgressCallback({\n percentage: Math.floor((this._downloadFileOffset / this._downloadFileLength) * 100),\n });\n }\n if (this._messageCallback) this._messageCallback({ op, group, id, data, length });\n this._downloadFileNext();\n return;\n }\n\n if (this._messageCallback) this._messageCallback({ op, group, id, data, length });\n }\n\n cmdReset() {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_OS, constants.OS_MGMT_ID_RESET);\n }\n\n smpEcho(message) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_OS, constants.OS_MGMT_ID_ECHO, {\n d: message,\n });\n }\n\n cmdImageState() {\n return this._getMessage(constants.MGMT_OP_READ, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE);\n }\n\n cmdImageErase() {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_ERASE, {});\n }\n\n cmdImageTest(hash) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE, {\n hash,\n confirm: false,\n });\n }\n\n cmdImageConfirm(hash) {\n return this._getMessage(constants.MGMT_OP_WRITE, constants.MGMT_GROUP_ID_IMAGE, constants.IMG_MGMT_ID_STATE, {\n hash,\n confirm: true,\n });\n }\n\n _hash(image) {\n return crypto.subtle.digest(\"SHA-256\", image);\n }\n\n async _uploadNext() {\n if (!this._uploadImage) {\n return;\n }\n\n if (this._uploadOffset >= this._uploadImage.byteLength) {\n this._uploadIsInProgress = false;\n this._imageUploadFinishedCallback();\n return;\n }\n\n const nmpOverhead = 8;\n const message = { data: new Uint8Array(), off: this._uploadOffset };\n if (this._uploadOffset === 0) {\n message.len = this._uploadImage.byteLength;\n message.sha = new Uint8Array(await this._hash(this._uploadImage));\n }\n this._imageUploadProgressCallback({\n percentage: Math.floor((this._uploadOffset / this._uploadImage.byteLength) * 100),\n });\n\n const length = this._mtu - CBOR.encode(message).byteLength - nmpOverhead - 3 - 5;\n\n message.data = new Uint8Array(this._uploadImage.slice(this._uploadOffset, this._uploadOffset + length));\n\n this._uploadOffset += length;\n\n const packet = this._getMessage(\n constants.MGMT_OP_WRITE,\n constants.MGMT_GROUP_ID_IMAGE,\n constants.IMG_MGMT_ID_UPLOAD,\n message\n );\n\n _console.log(\"mcumgr - _uploadNext: Message Length: \" + packet.length);\n\n this._imageUploadNextCallback({ packet });\n }\n async reset() {\n this._messageCallback = null;\n this._imageUploadProgressCallback = null;\n this._imageUploadNextCallback = null;\n this._fileUploadProgressCallback = null;\n this._fileUploadNextCallback = null;\n this._uploadIsInProgress = false;\n this._downloadIsInProgress = false;\n this._buffer = new Uint8Array();\n this._seq = 0;\n }\n\n async cmdUpload(image, slot = 0) {\n if (this._uploadIsInProgress) {\n _console.error(\"Upload is already in progress.\");\n return;\n }\n this._uploadIsInProgress = true;\n\n this._uploadOffset = 0;\n this._uploadImage = image;\n this._uploadSlot = slot;\n\n this._uploadNext();\n }\n\n async cmdUploadFile(filebuf, destFilename) {\n if (this._uploadIsInProgress) {\n _console.error(\"Upload is already in progress.\");\n return;\n }\n this._uploadIsInProgress = true;\n this._uploadFileOffset = 0;\n this._uploadFile = filebuf;\n this._uploadFilename = destFilename;\n\n this._uploadFileNext();\n }\n\n async _uploadFileNext() {\n _console.log(\"uploadFileNext - offset: \" + this._uploadFileOffset + \", length: \" + this._uploadFile.byteLength);\n\n if (this._uploadFileOffset >= this._uploadFile.byteLength) {\n this._uploadIsInProgress = false;\n this._fileUploadFinishedCallback();\n return;\n }\n\n const nmpOverhead = 8;\n const message = { data: new Uint8Array(), off: this._uploadFileOffset };\n if (this._uploadFileOffset === 0) {\n message.len = this._uploadFile.byteLength;\n }\n message.name = this._uploadFilename;\n this._fileUploadProgressCallback({\n percentage: Math.floor((this._uploadFileOffset / this._uploadFile.byteLength) * 100),\n });\n\n const length = this._mtu - CBOR.encode(message).byteLength - nmpOverhead;\n\n message.data = new Uint8Array(this._uploadFile.slice(this._uploadFileOffset, this._uploadFileOffset + length));\n\n this._uploadFileOffset += length;\n\n const packet = this._getMessage(\n constants.MGMT_OP_WRITE,\n constants.MGMT_GROUP_ID_FS,\n constants.FS_MGMT_ID_FILE,\n message\n );\n\n _console.log(\"mcumgr - _uploadNext: Message Length: \" + packet.length);\n\n this._fileUploadNextCallback({ packet });\n }\n\n async cmdDownloadFile(filename, destFilename) {\n if (this._downloadIsInProgress) {\n _console.error(\"Download is already in progress.\");\n return;\n }\n this._downloadIsInProgress = true;\n this._downloadFileOffset = 0;\n this._downloadFileLength = 0;\n this._downloadRemoteFilename = filename;\n this._downloadLocalFilename = destFilename;\n\n this._downloadFileNext();\n }\n\n async _downloadFileNext() {\n if (this._downloadFileLength > 0) {\n if (this._downloadFileOffset >= this._downloadFileLength) {\n this._downloadIsInProgress = false;\n this._fileDownloadFinishedCallback();\n return;\n }\n }\n\n const message = { off: this._downloadFileOffset };\n if (this._downloadFileOffset === 0) {\n message.name = this._downloadRemoteFilename;\n }\n\n const packet = this._getMessage(\n constants.MGMT_OP_READ,\n constants.MGMT_GROUP_ID_FS,\n constants.FS_MGMT_ID_FILE,\n message\n );\n _console.log(\"mcumgr - _downloadNext: Message Length: \" + packet.length);\n this._fileDownloadNextCallback({ packet });\n }\n\n async imageInfo(image) {\n const info = {};\n const view = new Uint8Array(image);\n\n // check header length\n if (view.length < 32) {\n throw new Error(\"Invalid image (too short file)\");\n }\n\n // check MAGIC bytes 0x96f3b83d\n if (view[0] !== 0x3d || view[1] !== 0xb8 || view[2] !== 0xf3 || view[3] !== 0x96) {\n throw new Error(\"Invalid image (wrong magic bytes)\");\n }\n\n // check load address is 0x00000000\n if (view[4] !== 0x00 || view[5] !== 0x00 || view[6] !== 0x00 || view[7] !== 0x00) {\n throw new Error(\"Invalid image (wrong load address)\");\n }\n\n const headerSize = view[8] + view[9] * 2 ** 8;\n\n // check protected TLV area size is 0\n if (view[10] !== 0x00 || view[11] !== 0x00) {\n throw new Error(\"Invalid image (wrong protected TLV area size)\");\n }\n\n const imageSize = view[12] + view[13] * 2 ** 8 + view[14] * 2 ** 16 + view[15] * 2 ** 24;\n info.imageSize = imageSize;\n\n // check image size is correct\n if (view.length < imageSize + headerSize) {\n throw new Error(\"Invalid image (wrong image size)\");\n }\n\n // check flags is 0x00000000\n if (view[16] !== 0x00 || view[17] !== 0x00 || view[18] !== 0x00 || view[19] !== 0x00) {\n throw new Error(\"Invalid image (wrong flags)\");\n }\n\n const version = `${view[20]}.${view[21]}.${view[22] + view[23] * 2 ** 8}`;\n info.version = version;\n\n info.hash = [...new Uint8Array(await this._hash(image.slice(0, imageSize + 32)))]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return info;\n }\n}\n","import Device, { SendSmpMessageCallback } from \"./Device.ts\";\nimport { getFileBuffer } from \"./utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher from \"./utils/EventDispatcher.ts\";\nimport { MCUManager, constants } from \"./utils/mcumgr.js\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport autoBind from \"auto-bind\";\n\nconst _console = createConsole(\"FirmwareManager\", { log: false });\n\nexport const FirmwareMessageTypes = [\"smp\"] as const;\nexport type FirmwareMessageType = (typeof FirmwareMessageTypes)[number];\n\nexport const FirmwareEventTypes = [\n ...FirmwareMessageTypes,\n \"firmwareImages\",\n \"firmwareUploadProgress\",\n \"firmwareStatus\",\n \"firmwareUploadComplete\",\n] as const;\nexport type FirmwareEventType = (typeof FirmwareEventTypes)[number];\n\nexport const FirmwareStatuses = [\"idle\", \"uploading\", \"uploaded\", \"pending\", \"testing\", \"erasing\"] as const;\nexport type FirmwareStatus = (typeof FirmwareStatuses)[number];\n\nexport interface FirmwareImage {\n slot: number;\n active: boolean;\n confirmed: boolean;\n pending: boolean;\n permanent: boolean;\n bootable: boolean;\n version: string;\n hash?: Uint8Array;\n empty?: boolean;\n}\n\nexport interface FirmwareEventMessages {\n smp: { dataView: DataView };\n firmwareImages: { firmwareImages: FirmwareImage[] };\n firmwareUploadProgress: { progress: number };\n firmwareStatus: { firmwareStatus: FirmwareStatus };\n //firmwareUploadComplete: {};\n}\n\nexport type FirmwareEventDispatcher = EventDispatcher<Device, FirmwareEventType, FirmwareEventMessages>;\n\nclass FirmwareManager {\n sendMessage!: SendSmpMessageCallback;\n\n constructor() {\n this.#assignMcuManagerCallbacks();\n autoBind(this);\n }\n\n eventDispatcher!: FirmwareEventDispatcher;\n get addEventListenter() {\n return this.eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n parseMessage(messageType: FirmwareMessageType, dataView: DataView) {\n _console.log({ messageType });\n\n switch (messageType) {\n case \"smp\":\n this.#mcuManager._notification(Array.from(new Uint8Array(dataView.buffer)));\n this.#dispatchEvent(\"smp\", { dataView });\n break;\n default:\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n async uploadFirmware(file: FileLike) {\n _console.log(\"uploadFirmware\", file);\n\n const promise = this.waitForEvent(\"firmwareUploadComplete\");\n\n await this.getImages();\n\n const arrayBuffer = await getFileBuffer(file);\n const imageInfo = await this.#mcuManager.imageInfo(arrayBuffer);\n _console.log({ imageInfo });\n\n this.#mcuManager.cmdUpload(arrayBuffer, 1);\n\n this.#updateStatus(\"uploading\");\n\n await promise;\n }\n\n #status: FirmwareStatus = \"idle\";\n get status() {\n return this.#status;\n }\n #updateStatus(newStatus: FirmwareStatus) {\n _console.assertEnumWithError(newStatus, FirmwareStatuses);\n if (this.#status == newStatus) {\n _console.log(`redundant firmwareStatus assignment \"${newStatus}\"`);\n return;\n }\n\n this.#status = newStatus;\n _console.log({ firmwareStatus: this.#status });\n this.#dispatchEvent(\"firmwareStatus\", { firmwareStatus: this.#status });\n }\n\n // COMMANDS\n\n #images!: FirmwareImage[];\n get images() {\n return this.#images;\n }\n #assertImages() {\n _console.assertWithError(this.#images, \"didn't get imageState\");\n }\n #assertValidImageIndex(imageIndex: number) {\n _console.assertTypeWithError(imageIndex, \"number\");\n _console.assertWithError(imageIndex == 0 || imageIndex == 1, \"imageIndex must be 0 or 1\");\n }\n async getImages() {\n const promise = this.waitForEvent(\"firmwareImages\");\n\n _console.log(\"getting firmware image state...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageState()).buffer);\n\n await promise;\n }\n\n async testImage(imageIndex: number = 1) {\n this.#assertValidImageIndex(imageIndex);\n this.#assertImages();\n if (!this.#images[imageIndex]) {\n _console.log(`image ${imageIndex} not found`);\n return;\n }\n if (this.#images[imageIndex].pending == true) {\n _console.log(`image ${imageIndex} is already pending`);\n return;\n }\n if (this.#images[imageIndex].empty) {\n _console.log(`image ${imageIndex} is empty`);\n return;\n }\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"testing firmware image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageTest(this.#images[imageIndex].hash)).buffer);\n\n await promise;\n }\n\n async eraseImage() {\n this.#assertImages();\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"erasing image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageErase()).buffer);\n\n this.#updateStatus(\"erasing\");\n\n await promise;\n await this.getImages();\n }\n\n async confirmImage(imageIndex: number = 0) {\n this.#assertValidImageIndex(imageIndex);\n this.#assertImages();\n if (this.#images[imageIndex].confirmed === true) {\n _console.log(`image ${imageIndex} is already confirmed`);\n return;\n }\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"confirming image...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdImageConfirm(this.#images[imageIndex].hash)).buffer);\n\n await promise;\n }\n\n async echo(string: string) {\n _console.assertTypeWithError(string, \"string\");\n\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"sending echo...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.smpEcho(string)).buffer);\n\n await promise;\n }\n\n async reset() {\n const promise = this.waitForEvent(\"smp\");\n\n _console.log(\"resetting...\");\n this.sendMessage(Uint8Array.from(this.#mcuManager.cmdReset()).buffer);\n\n await promise;\n }\n\n // MTU\n #mtu!: number;\n get mtu() {\n return this.#mtu;\n }\n set mtu(newMtu: number) {\n this.#mtu = newMtu;\n this.#mcuManager._mtu = newMtu;\n }\n\n // MCUManager\n #mcuManager = new MCUManager();\n\n #assignMcuManagerCallbacks() {\n this.#mcuManager.onMessage(this.#onMcuMessage.bind(this));\n\n this.#mcuManager.onFileDownloadNext(this.#onMcuFileDownloadNext);\n this.#mcuManager.onFileDownloadProgress(this.#onMcuFileDownloadProgress.bind(this));\n this.#mcuManager.onFileDownloadFinished(this.#onMcuFileDownloadFinished.bind(this));\n\n this.#mcuManager.onFileUploadNext(this.#onMcuFileUploadNext.bind(this));\n this.#mcuManager.onFileUploadProgress(this.#onMcuFileUploadProgress.bind(this));\n this.#mcuManager.onFileUploadFinished(this.#onMcuFileUploadFinished.bind(this));\n\n this.#mcuManager.onImageUploadNext(this.#onMcuImageUploadNext.bind(this));\n this.#mcuManager.onImageUploadProgress(this.#onMcuImageUploadProgress.bind(this));\n this.#mcuManager.onImageUploadFinished(this.#onMcuImageUploadFinished.bind(this));\n }\n\n #onMcuMessage({ op, group, id, data, length }: { op: number; group: number; id: number; data: any; length: number }) {\n _console.log(\"onMcuMessage\", ...arguments);\n\n switch (group) {\n case constants.MGMT_GROUP_ID_OS:\n switch (id) {\n case constants.OS_MGMT_ID_ECHO:\n _console.log(`echo \"${data.r}\"`);\n break;\n case constants.OS_MGMT_ID_TASKSTAT:\n _console.table(data.tasks);\n break;\n case constants.OS_MGMT_ID_MPSTAT:\n _console.log(data);\n break;\n }\n break;\n case constants.MGMT_GROUP_ID_IMAGE:\n switch (id) {\n case constants.IMG_MGMT_ID_STATE:\n this.#onMcuImageState(data);\n }\n break;\n default:\n throw Error(`uncaught mcuMessage group ${group}`);\n }\n }\n\n #onMcuFileDownloadNext() {\n _console.log(\"onMcuFileDownloadNext\", ...arguments);\n }\n #onMcuFileDownloadProgress() {\n _console.log(\"onMcuFileDownloadProgress\", ...arguments);\n }\n #onMcuFileDownloadFinished() {\n _console.log(\"onMcuFileDownloadFinished\", ...arguments);\n }\n\n #onMcuFileUploadNext() {\n _console.log(\"onMcuFileUploadNext\");\n }\n #onMcuFileUploadProgress() {\n _console.log(\"onMcuFileUploadProgress\");\n }\n #onMcuFileUploadFinished() {\n _console.log(\"onMcuFileUploadFinished\");\n }\n\n #onMcuImageUploadNext({ packet }: { packet: number[] }) {\n _console.log(\"onMcuImageUploadNext\");\n this.sendMessage(Uint8Array.from(packet).buffer);\n }\n #onMcuImageUploadProgress({ percentage }: { percentage: number }) {\n const progress = percentage / 100;\n _console.log(\"onMcuImageUploadProgress\", ...arguments);\n this.#dispatchEvent(\"firmwareUploadProgress\", { progress });\n }\n async #onMcuImageUploadFinished() {\n _console.log(\"onMcuImageUploadFinished\", ...arguments);\n\n await this.getImages();\n\n this.#dispatchEvent(\"firmwareUploadProgress\", { progress: 100 });\n this.#dispatchEvent(\"firmwareUploadComplete\", {});\n }\n\n #onMcuImageState({ images }: { images?: FirmwareImage[] }) {\n if (images) {\n this.#images = images;\n _console.log(\"images\", this.#images);\n } else {\n _console.log(\"no images found\");\n return;\n }\n\n let newStatus: FirmwareStatus = \"idle\";\n\n if (this.#images.length == 2) {\n if (!this.#images[1].bootable) {\n _console.warn('Slot 1 has a invalid image. Click \"Erase Image\" to erase it or upload a different image');\n } else if (!this.#images[0].confirmed) {\n _console.log(\n 'Slot 0 has a valid image. Click \"Confirm Image\" to confirm it or wait and the device will swap images back.'\n );\n newStatus = \"testing\";\n } else {\n if (this.#images[1].pending) {\n _console.log(\"reset to upload to the new firmware image\");\n newStatus = \"pending\";\n } else {\n _console.log(\"Slot 1 has a valid image. run testImage() to test it or upload a different image.\");\n newStatus = \"uploaded\";\n }\n }\n }\n\n if (this.#images.length == 1) {\n this.#images.push({\n slot: 1,\n empty: true,\n version: \"Empty\",\n pending: false,\n confirmed: false,\n bootable: false,\n active: false,\n permanent: false,\n });\n\n _console.log(\"Select a firmware upload image to upload to slot 1.\");\n }\n\n this.#updateStatus(newStatus);\n this.#dispatchEvent(\"firmwareImages\", { firmwareImages: this.#images });\n }\n}\n\nexport default FirmwareManager;\n","import { ConnectionStatus } from \"./connection/BaseConnectionManager.ts\";\nimport WebBluetoothConnectionManager from \"./connection/bluetooth/WebBluetoothConnectionManager.ts\";\nimport Device, { BoundDeviceEventListeners, DeviceEventMap } from \"./Device.ts\";\nimport { DeviceType } from \"./InformationManager.ts\";\nimport { createConsole } from \"./utils/Console.ts\";\nimport { isInBluefy, isInBrowser } from \"./utils/environment.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"./utils/EventDispatcher.ts\";\nimport { addEventListeners } from \"./utils/EventUtils.ts\";\n\nconst _console = createConsole(\"DeviceManager\", { log: false });\n\nexport interface LocalStorageDeviceInformation {\n type: DeviceType;\n bluetoothId: string;\n ipAddress?: string;\n isWifiSecure?: boolean;\n}\n\nexport interface LocalStorageConfiguration {\n devices: LocalStorageDeviceInformation[];\n}\n\nexport const DeviceManagerEventTypes = [\n \"deviceConnected\",\n \"deviceDisconnected\",\n \"deviceIsConnected\",\n \"availableDevices\",\n \"connectedDevices\",\n] as const;\nexport type DeviceManagerEventType = (typeof DeviceManagerEventTypes)[number];\n\ninterface DeviceManagerEventMessage {\n device: Device;\n}\nexport interface DeviceManagerEventMessages {\n deviceConnected: DeviceManagerEventMessage;\n deviceDisconnected: DeviceManagerEventMessage;\n deviceIsConnected: DeviceManagerEventMessage;\n availableDevices: { availableDevices: Device[] };\n connectedDevices: { connectedDevices: Device[] };\n}\n\nexport type DeviceManagerEventDispatcher = EventDispatcher<\n DeviceManager,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEventMap = EventMap<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEventListenerMap = EventListenerMap<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type DeviceManagerEvent = Event<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\nexport type BoundDeviceManagerEventListeners = BoundEventListeners<\n typeof Device,\n DeviceManagerEventType,\n DeviceManagerEventMessages\n>;\n\nclass DeviceManager {\n static readonly shared = new DeviceManager();\n\n constructor() {\n if (DeviceManager.shared && this != DeviceManager.shared) {\n throw Error(\"DeviceManager is a singleton - use DeviceManager.shared\");\n }\n\n if (this.CanUseLocalStorage) {\n this.UseLocalStorage = true;\n }\n }\n\n // DEVICE LISTENERS\n #boundDeviceEventListeners: BoundDeviceEventListeners = {\n getType: this.#onDeviceType.bind(this),\n isConnected: this.#OnDeviceIsConnected.bind(this),\n };\n /** @private */\n onDevice(device: Device) {\n addEventListeners(device, this.#boundDeviceEventListeners);\n }\n\n #onDeviceType(event: DeviceEventMap[\"getType\"]) {\n if (this.#UseLocalStorage) {\n this.#UpdateLocalStorageConfigurationForDevice(event.target);\n }\n }\n\n // CONNECTION STATUS\n /** @private */\n OnDeviceConnectionStatusUpdated(\n device: Device,\n connectionStatus: ConnectionStatus\n ) {\n if (\n connectionStatus == \"notConnected\" &&\n !device.canReconnect &&\n this.#AvailableDevices.includes(device)\n ) {\n const deviceIndex = this.#AvailableDevices.indexOf(device);\n this.AvailableDevices.splice(deviceIndex, 1);\n this.#DispatchAvailableDevices();\n }\n }\n\n // CONNECTED DEVICES\n\n #ConnectedDevices: Device[] = [];\n get ConnectedDevices() {\n return this.#ConnectedDevices;\n }\n\n #UseLocalStorage = false;\n get UseLocalStorage() {\n return this.#UseLocalStorage;\n }\n set UseLocalStorage(newUseLocalStorage) {\n this.#AssertLocalStorage();\n _console.assertTypeWithError(newUseLocalStorage, \"boolean\");\n this.#UseLocalStorage = newUseLocalStorage;\n if (this.#UseLocalStorage && !this.#LocalStorageConfiguration) {\n this.#LoadFromLocalStorage();\n }\n }\n\n #DefaultLocalStorageConfiguration: LocalStorageConfiguration = {\n devices: [],\n };\n #LocalStorageConfiguration?: LocalStorageConfiguration;\n\n get CanUseLocalStorage() {\n return isInBrowser && window.localStorage;\n }\n\n #AssertLocalStorage() {\n _console.assertWithError(\n isInBrowser,\n \"localStorage is only available in the browser\"\n );\n _console.assertWithError(window.localStorage, \"localStorage not found\");\n }\n #LocalStorageKey = \"BS.Device\";\n #SaveToLocalStorage() {\n this.#AssertLocalStorage();\n localStorage.setItem(\n this.#LocalStorageKey,\n JSON.stringify(this.#LocalStorageConfiguration)\n );\n }\n async #LoadFromLocalStorage() {\n this.#AssertLocalStorage();\n let localStorageString = localStorage.getItem(this.#LocalStorageKey);\n if (typeof localStorageString != \"string\") {\n _console.log(\"no info found in localStorage\");\n this.#LocalStorageConfiguration = Object.assign(\n {},\n this.#DefaultLocalStorageConfiguration\n );\n this.#SaveToLocalStorage();\n return;\n }\n try {\n const configuration = JSON.parse(localStorageString);\n _console.log({ configuration });\n this.#LocalStorageConfiguration = configuration;\n if (this.CanGetDevices) {\n await this.GetDevices(); // redundant?\n }\n } catch (error) {\n _console.error(error);\n }\n }\n\n #UpdateLocalStorageConfigurationForDevice(device: Device) {\n if (device.connectionType != \"webBluetooth\") {\n _console.log(\"localStorage is only for webBluetooth devices\");\n return;\n }\n this.#AssertLocalStorage();\n const deviceInformationIndex =\n this.#LocalStorageConfiguration!.devices.findIndex(\n (deviceInformation) => {\n return deviceInformation.bluetoothId == device.bluetoothId;\n }\n );\n if (deviceInformationIndex == -1) {\n return;\n }\n this.#LocalStorageConfiguration!.devices[deviceInformationIndex].type =\n device.type;\n this.#SaveToLocalStorage();\n }\n\n // AVAILABLE DEVICES\n #AvailableDevices: Device[] = [];\n get AvailableDevices() {\n return this.#AvailableDevices;\n }\n\n get CanGetDevices() {\n return isInBrowser && navigator.bluetooth?.getDevices;\n }\n /**\n * retrieves devices already connected via web bluetooth in other tabs/windows\n *\n * _only available on web-bluetooth enabled browsers_\n */\n async GetDevices(): Promise<Device[] | undefined> {\n if (!isInBrowser) {\n _console.warn(\"GetDevices is only available in the browser\");\n return;\n }\n\n if (!navigator.bluetooth) {\n _console.warn(\"bluetooth is not available in this browser\");\n return;\n }\n\n if (isInBluefy) {\n _console.warn(\"bluefy lists too many devices...\");\n return;\n }\n\n if (!navigator.bluetooth.getDevices) {\n _console.warn(\"bluetooth.getDevices() is not available in this browser\");\n return;\n }\n\n if (!this.CanGetDevices) {\n _console.log(\"CanGetDevices is false\");\n return;\n }\n\n if (!this.#LocalStorageConfiguration) {\n this.#LoadFromLocalStorage();\n }\n\n const configuration = this.#LocalStorageConfiguration!;\n if (!configuration.devices || configuration.devices.length == 0) {\n _console.log(\"no devices found in configuration\");\n return;\n }\n\n const bluetoothDevices = await navigator.bluetooth.getDevices();\n\n _console.log({ bluetoothDevices });\n\n bluetoothDevices.forEach((bluetoothDevice) => {\n if (!bluetoothDevice.gatt) {\n return;\n }\n let deviceInformation = configuration.devices.find(\n (deviceInformation) =>\n bluetoothDevice.id == deviceInformation.bluetoothId\n );\n if (!deviceInformation) {\n return;\n }\n\n let existingConnectedDevice = this.ConnectedDevices.filter(\n (device) => device.connectionType == \"webBluetooth\"\n ).find((device) => device.bluetoothId == bluetoothDevice.id);\n\n const existingAvailableDevice = this.AvailableDevices.filter(\n (device) => device.connectionType == \"webBluetooth\"\n ).find((device) => device.bluetoothId == bluetoothDevice.id);\n if (existingAvailableDevice) {\n if (\n existingConnectedDevice &&\n existingConnectedDevice?.bluetoothId ==\n existingAvailableDevice.bluetoothId &&\n existingConnectedDevice != existingAvailableDevice\n ) {\n this.AvailableDevices[\n this.#AvailableDevices.indexOf(existingAvailableDevice)\n ] = existingConnectedDevice;\n }\n return;\n }\n\n if (existingConnectedDevice) {\n this.AvailableDevices.push(existingConnectedDevice);\n return;\n }\n\n const device = new Device();\n const connectionManager = new WebBluetoothConnectionManager();\n connectionManager.device = bluetoothDevice;\n if (bluetoothDevice.name) {\n device._informationManager.updateName(bluetoothDevice.name);\n }\n device._informationManager.updateType(deviceInformation.type);\n device.connectionManager = connectionManager;\n this.AvailableDevices.push(device);\n });\n this.#DispatchAvailableDevices();\n return this.AvailableDevices;\n }\n\n // STATIC EVENTLISTENERS\n\n #EventDispatcher: DeviceManagerEventDispatcher = new EventDispatcher(\n this as DeviceManager,\n DeviceManagerEventTypes\n );\n\n get AddEventListener() {\n return this.#EventDispatcher.addEventListener;\n }\n get #DispatchEvent() {\n return this.#EventDispatcher.dispatchEvent;\n }\n get RemoveEventListener() {\n return this.#EventDispatcher.removeEventListener;\n }\n get RemoveEventListeners() {\n return this.#EventDispatcher.removeEventListeners;\n }\n get RemoveAllEventListeners() {\n return this.#EventDispatcher.removeAllEventListeners;\n }\n\n #OnDeviceIsConnected(event: DeviceEventMap[\"isConnected\"]) {\n const { target: device } = event;\n if (device.isConnected) {\n if (!this.#ConnectedDevices.includes(device)) {\n _console.log(\"adding device\", device);\n this.#ConnectedDevices.push(device);\n if (this.UseLocalStorage && device.connectionType == \"webBluetooth\") {\n const deviceInformation: LocalStorageDeviceInformation = {\n type: device.type,\n bluetoothId: device.bluetoothId!,\n ipAddress: device.ipAddress,\n isWifiSecure: device.isWifiSecure,\n };\n const deviceInformationIndex =\n this.#LocalStorageConfiguration!.devices.findIndex(\n (_deviceInformation) =>\n _deviceInformation.bluetoothId == deviceInformation.bluetoothId\n );\n if (deviceInformationIndex == -1) {\n this.#LocalStorageConfiguration!.devices.push(deviceInformation);\n } else {\n this.#LocalStorageConfiguration!.devices[deviceInformationIndex] =\n deviceInformation;\n }\n this.#SaveToLocalStorage();\n }\n this.#DispatchEvent(\"deviceConnected\", { device });\n this.#DispatchEvent(\"deviceIsConnected\", { device });\n this.#DispatchConnectedDevices();\n } else {\n _console.log(\"device already included\");\n }\n } else {\n if (this.#ConnectedDevices.includes(device)) {\n _console.log(\"removing device\", device);\n this.#ConnectedDevices.splice(\n this.#ConnectedDevices.indexOf(device),\n 1\n );\n this.#DispatchEvent(\"deviceDisconnected\", { device });\n this.#DispatchEvent(\"deviceIsConnected\", { device });\n this.#DispatchConnectedDevices();\n } else {\n _console.log(\"device already not included\");\n }\n }\n if (this.CanGetDevices) {\n this.GetDevices();\n }\n if (device.isConnected && !this.AvailableDevices.includes(device)) {\n const existingAvailableDevice = this.AvailableDevices.find(\n (_device) => _device.bluetoothId == device.bluetoothId\n );\n _console.log({ existingAvailableDevice });\n if (existingAvailableDevice) {\n this.AvailableDevices[\n this.AvailableDevices.indexOf(existingAvailableDevice)\n ] = device;\n } else {\n this.AvailableDevices.push(device);\n }\n this.#DispatchAvailableDevices();\n }\n this._CheckDeviceAvailability(device);\n }\n\n _CheckDeviceAvailability(device: Device) {\n if (\n !device.isConnected &&\n !device.isAvailable &&\n this.#AvailableDevices.includes(device)\n ) {\n _console.log(\"removing device from availableDevices...\");\n this.#AvailableDevices.splice(this.#AvailableDevices.indexOf(device), 1);\n this.#DispatchAvailableDevices();\n }\n }\n\n #DispatchAvailableDevices() {\n _console.log({ AvailableDevices: this.AvailableDevices });\n this.#DispatchEvent(\"availableDevices\", {\n availableDevices: this.AvailableDevices,\n });\n }\n #DispatchConnectedDevices() {\n _console.log({ ConnectedDevices: this.ConnectedDevices });\n this.#DispatchEvent(\"connectedDevices\", {\n connectedDevices: this.ConnectedDevices,\n });\n }\n}\n\nexport default DeviceManager.shared;\n","import { DeviceEventTypes } from \"../Device.ts\";\nimport {\n ConnectionMessageType,\n ConnectionMessageTypes,\n} from \"../connection/BaseConnectionManager.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { DeviceEventType } from \"../Device.ts\";\n\nconst _console = createConsole(\"ServerUtils\", { log: false });\n\nexport const ServerMessageTypes = [\n \"isScanningAvailable\",\n \"isScanning\",\n \"startScan\",\n \"stopScan\",\n \"discoveredDevice\",\n \"discoveredDevices\",\n \"expiredDiscoveredDevice\",\n \"connectToDevice\",\n \"disconnectFromDevice\",\n \"connectedDevices\",\n \"deviceMessage\",\n \"requiredDeviceInformation\",\n] as const;\nexport type ServerMessageType = (typeof ServerMessageTypes)[number];\n\nexport const DeviceMessageTypes = [\n \"connectionStatus\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"rx\",\n \"smp\",\n] as const;\nexport type DeviceMessageType = (typeof DeviceMessageTypes)[number];\n\n// MESSAGING\n\nexport type MessageLike =\n | number\n | number[]\n | ArrayBufferLike\n | DataView\n | boolean\n | string\n | any;\n\nexport interface Message<MessageType extends string> {\n type: MessageType;\n data?: MessageLike | MessageLike[];\n}\n\nexport function createMessage<MessageType extends string>(\n enumeration: readonly MessageType[],\n ...messages: (Message<MessageType> | MessageType)[]\n) {\n _console.log(\"createMessage\", ...messages);\n\n const messageBuffers = messages.map((message) => {\n if (typeof message == \"string\") {\n message = { type: message };\n }\n\n if (message.data != undefined) {\n if (!Array.isArray(message.data)) {\n message.data = [message.data];\n }\n } else {\n message.data = [];\n }\n\n const messageDataArrayBuffer = concatenateArrayBuffers(...message.data);\n const messageDataArrayBufferByteLength = messageDataArrayBuffer.byteLength;\n\n _console.assertEnumWithError(message.type, enumeration);\n const messageTypeEnum = enumeration.indexOf(message.type);\n\n const messageDataLengthDataView = new DataView(new ArrayBuffer(2));\n messageDataLengthDataView.setUint16(\n 0,\n messageDataArrayBufferByteLength,\n true\n );\n\n return concatenateArrayBuffers(\n messageTypeEnum,\n messageDataLengthDataView,\n messageDataArrayBuffer\n );\n });\n _console.log(\"messageBuffers\", ...messageBuffers);\n return concatenateArrayBuffers(...messageBuffers);\n}\n\nexport type ServerMessage = ServerMessageType | Message<ServerMessageType>;\nexport function createServerMessage(...messages: ServerMessage[]) {\n _console.log(\"createServerMessage\", ...messages);\n return createMessage(ServerMessageTypes, ...messages);\n}\n\nexport type DeviceMessage = DeviceEventType | Message<DeviceEventType>;\nexport function createDeviceMessage(...messages: DeviceMessage[]) {\n _console.log(\"createDeviceMessage\", ...messages);\n return createMessage(DeviceEventTypes, ...messages);\n}\n\nexport type ClientDeviceMessage =\n | ConnectionMessageType\n | Message<ConnectionMessageType>;\nexport function createClientDeviceMessage(...messages: ClientDeviceMessage[]) {\n _console.log(\"createClientDeviceMessage\", ...messages);\n return createMessage(ConnectionMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const isScanningAvailableRequestMessage = createServerMessage(\n \"isScanningAvailable\"\n);\nexport const isScanningRequestMessage = createServerMessage(\"isScanning\");\nexport const startScanRequestMessage = createServerMessage(\"startScan\");\nexport const stopScanRequestMessage = createServerMessage(\"stopScan\");\nexport const discoveredDevicesMessage =\n createServerMessage(\"discoveredDevices\");\n","import { createConsole } from \"../../utils/Console.ts\";\nimport { createMessage, Message } from \"../ServerUtils.ts\";\n\nconst _console = createConsole(\"WebSocketUtils\", { log: false });\n\nexport const webSocketPingTimeout = 30_000;\nexport const webSocketReconnectTimeout = 3_000;\n\nexport const WebSocketMessageTypes = [\"ping\", \"pong\", \"serverMessage\"] as const;\nexport type WebSocketMessageType = (typeof WebSocketMessageTypes)[number];\n\nexport type WebSocketMessage =\n | WebSocketMessageType\n | Message<WebSocketMessageType>;\nexport function createWebSocketMessage(...messages: WebSocketMessage[]) {\n _console.log(\"createWebSocketMessage\", ...messages);\n return createMessage(WebSocketMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const webSocketPingMessage = createWebSocketMessage(\"ping\");\nexport const webSocketPongMessage = createWebSocketMessage(\"pong\");\n","import { DeviceInformationTypes } from \"../../DeviceInformationManager.ts\";\nimport {\n createMessage,\n Message,\n MessageLike,\n} from \"../../server/ServerUtils.ts\";\nimport { webSocketPingTimeout } from \"../../server/websocket/WebSocketUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseConnectionManager, {\n ConnectionType,\n} from \"../BaseConnectionManager.ts\";\nimport type * as ws from \"ws\";\n\nconst _console = createConsole(\"WebSocketConnectionManager\", { log: false });\n\nconst WebSocketMessageTypes = [\n \"ping\",\n \"pong\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"message\",\n] as const;\ntype WebSocketMessageType = (typeof WebSocketMessageTypes)[number];\n\ntype WebSocketMessage = WebSocketMessageType | Message<WebSocketMessageType>;\nfunction createWebSocketMessage(...messages: WebSocketMessage[]) {\n _console.log(\"createWebSocketMessage\", ...messages);\n return createMessage(WebSocketMessageTypes, ...messages);\n}\n\nconst WebSocketDeviceInformationMessageTypes: WebSocketMessageType[] = [\n \"deviceInformation\",\n \"batteryLevel\",\n];\n\nclass WebSocketConnectionManager extends BaseConnectionManager {\n #bluetoothId?: string;\n get bluetoothId() {\n return this.#bluetoothId ?? \"\";\n }\n\n defaultMtu = 2 ** 10;\n\n constructor(\n ipAddress: string,\n isSecure: boolean = false,\n bluetoothId?: string\n ) {\n super();\n this.ipAddress = ipAddress;\n this.isSecure = isSecure;\n this.mtu = this.defaultMtu;\n this.#bluetoothId = bluetoothId;\n }\n\n get isAvailable() {\n return true;\n }\n\n static get isSupported() {\n return true;\n }\n static get type(): ConnectionType {\n return \"webSocket\";\n }\n\n // WEBSOCKET\n #webSocket?: WebSocket;\n get webSocket() {\n return this.#webSocket;\n }\n set webSocket(newWebSocket) {\n if (this.#webSocket == newWebSocket) {\n _console.log(\"redundant webSocket assignment\");\n return;\n }\n\n _console.log(\"assigning webSocket\", newWebSocket);\n\n if (this.#webSocket) {\n removeEventListeners(this.#webSocket, this.#boundWebSocketEventListeners);\n if (this.#webSocket.readyState == this.#webSocket.OPEN) {\n this.#webSocket.close();\n }\n }\n\n if (newWebSocket) {\n addEventListeners(newWebSocket, this.#boundWebSocketEventListeners);\n }\n this.#webSocket = newWebSocket;\n\n _console.log(\"assigned webSocket\");\n }\n\n // IP ADDRESS\n #ipAddress!: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n set ipAddress(newIpAddress) {\n this.assertIsNotConnected();\n if (this.#ipAddress == newIpAddress) {\n _console.log(`redundnant ipAddress assignment \"${newIpAddress}\"`);\n return;\n }\n this.#ipAddress = newIpAddress;\n _console.log(`updated ipAddress to \"${this.ipAddress}\"`);\n }\n\n // IS SECURE\n #isSecure = false;\n get isSecure() {\n return this.#isSecure;\n }\n set isSecure(newIsSecure) {\n this.assertIsNotConnected();\n if (this.#isSecure == newIsSecure) {\n _console.log(`redundant isSecure assignment ${newIsSecure}`);\n return;\n }\n this.#isSecure = newIsSecure;\n _console.log(`updated isSecure to \"${this.isSecure}\"`);\n }\n\n // URL\n get url() {\n return `${this.isSecure ? \"wss\" : \"ws\"}://${this.ipAddress}/ws`;\n }\n\n // CONNECTION\n async connect() {\n const canContinue = await super.connect();\n if (!canContinue) {\n return false;\n }\n try {\n this.webSocket = new WebSocket(this.url);\n return true;\n } catch (error) {\n _console.error(\"error connecting to webSocket\", error);\n this.status = \"notConnected\";\n return false;\n }\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n _console.log(\"closing websocket\");\n this.#pingTimer.stop();\n this.#webSocket?.close();\n return true;\n }\n\n get canReconnect() {\n return Boolean(this.webSocket);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n this.webSocket = new WebSocket(this.url);\n return true;\n }\n\n // BASE CONNECTION MANAGER\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n _console.error(\"smp not supported on webSockets\");\n }\n\n async sendTxData(data: ArrayBuffer) {\n await super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n this.#sendWebSocketMessage({ type: \"message\", data });\n }\n\n // WEBSOCKET MESSAGING\n #sendMessage(message: MessageLike) {\n this.assertIsConnected();\n _console.log(\"sending webSocket message\", message);\n this.#webSocket!.send(message);\n this.#pingTimer.restart();\n }\n\n #sendWebSocketMessage(...messages: WebSocketMessage[]) {\n this.#sendMessage(createWebSocketMessage(...messages));\n }\n\n // WEBSOCKET EVENTS\n #boundWebSocketEventListeners: { [eventType: string]: Function } = {\n open: this.#onWebSocketOpen.bind(this),\n message: this.#onWebSocketMessage.bind(this),\n close: this.#onWebSocketClose.bind(this),\n error: this.#onWebSocketError.bind(this),\n };\n\n #onWebSocketOpen(event: ws.Event) {\n _console.log(\"webSocket.open\", event);\n this.#pingTimer.start();\n this.status = \"connected\";\n this.#requestDeviceInformation();\n }\n async #onWebSocketMessage(event: ws.MessageEvent) {\n // this.#pingTimer.restart();\n //@ts-expect-error\n const arrayBuffer = await event.data.arrayBuffer();\n const dataView = new DataView(arrayBuffer);\n _console.log(`webSocket.message (${dataView.byteLength} bytes)`);\n this.#parseWebSocketMessage(dataView);\n }\n #onWebSocketClose(event: ws.CloseEvent) {\n _console.log(\"webSocket.close\", event);\n this.status = \"notConnected\";\n this.#pingTimer.stop();\n }\n #onWebSocketError(event: ws.ErrorEvent) {\n _console.error(\"webSocket.error\", event);\n }\n\n // PARSING\n #parseWebSocketMessage(dataView: DataView) {\n parseMessage(\n dataView,\n WebSocketMessageTypes,\n this.#onMessage.bind(this),\n null,\n true\n );\n }\n\n #onMessage(messageType: WebSocketMessageType, dataView: DataView) {\n _console.log(\n `received \"${messageType}\" message (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"ping\":\n this.#pong();\n break;\n case \"pong\":\n break;\n case \"batteryLevel\":\n this.onMessageReceived?.(\"batteryLevel\", dataView);\n break;\n case \"deviceInformation\":\n parseMessage(\n dataView,\n DeviceInformationTypes,\n (deviceInformationType, dataView) => {\n this.onMessageReceived!(deviceInformationType, dataView);\n }\n );\n break;\n case \"message\":\n this.parseRxMessage(dataView);\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // PING\n #pingTimer = new Timer(this.#ping.bind(this), webSocketPingTimeout - 1_000);\n #ping() {\n _console.log(\"pinging\");\n this.#sendWebSocketMessage(\"ping\");\n }\n #pong() {\n _console.log(\"ponging\");\n this.#sendWebSocketMessage(\"pong\");\n }\n\n // DEVICE INFORMATION\n #requestDeviceInformation() {\n this.#sendWebSocketMessage(...WebSocketDeviceInformationMessageTypes);\n }\n\n remove() {\n super.remove();\n this.webSocket = undefined;\n }\n}\n\nexport default WebSocketConnectionManager;\n","import { DeviceInformationTypes } from \"../../DeviceInformationManager.ts\";\nimport {\n createMessage,\n Message,\n MessageLike,\n} from \"../../server/ServerUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport { isInNode } from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseConnectionManager, {\n ConnectionType,\n} from \"../BaseConnectionManager.ts\";\n\nimport * as dgram from \"dgram\";\n\nconst _console = createConsole(\"UDPConnectionManager\", { log: false });\n\nexport const UDPSendPort = 3000;\n\nexport const UDPPingInterval = 2_000;\n\nconst SocketMessageTypes = [\n \"ping\",\n \"pong\",\n \"setRemoteReceivePort\",\n \"batteryLevel\",\n \"deviceInformation\",\n \"message\",\n] as const;\ntype SocketMessageType = (typeof SocketMessageTypes)[number];\n\ntype SocketMessage = SocketMessageType | Message<SocketMessageType>;\nfunction createSocketMessage(...messages: SocketMessage[]) {\n _console.log(\"createSocketMessage\", ...messages);\n return createMessage(SocketMessageTypes, ...messages);\n}\n\nconst SocketDeviceInformationMessageTypes: SocketMessageType[] = [\n \"deviceInformation\",\n \"batteryLevel\",\n];\n\nclass UDPConnectionManager extends BaseConnectionManager {\n #bluetoothId?: string;\n get bluetoothId() {\n return this.#bluetoothId ?? \"\";\n }\n\n defaultMtu = 2 ** 10;\n\n constructor(ipAddress: string, bluetoothId?: string, receivePort?: number) {\n super();\n this.ipAddress = ipAddress;\n this.mtu = this.defaultMtu;\n this.#bluetoothId = bluetoothId;\n if (receivePort) {\n this.receivePort = receivePort;\n }\n }\n\n get isAvailable() {\n return true;\n }\n static get isSupported() {\n return isInNode;\n }\n static get type(): ConnectionType {\n return \"udp\";\n }\n\n // IP ADDRESS\n #ipAddress!: string;\n get ipAddress() {\n return this.#ipAddress;\n }\n set ipAddress(newIpAddress) {\n this.assertIsNotConnected();\n if (this.#ipAddress == newIpAddress) {\n _console.log(`redundnant ipAddress assignment \"${newIpAddress}\"`);\n return;\n }\n this.#ipAddress = newIpAddress;\n _console.log(`updated ipAddress to \"${this.ipAddress}\"`);\n }\n\n // RECEIVE PORT\n #receivePort?: number;\n get receivePort() {\n return this.#receivePort;\n }\n set receivePort(newReceivePort) {\n this.assertIsNotConnected();\n if (this.#receivePort == newReceivePort) {\n _console.log(`redundnant receivePort assignment ${newReceivePort}`);\n return;\n }\n this.#receivePort = newReceivePort;\n _console.log(`updated receivePort to ${this.#receivePort}`);\n if (this.#receivePort) {\n this.#setRemoteReceivePortDataView.setUint16(0, this.#receivePort, true);\n }\n }\n\n // SET REMOTE RECEIVE PORT\n #didSetRemoteReceivePort = false;\n #setRemoteReceivePortDataView = new DataView(new ArrayBuffer(2));\n #parseReceivePort(dataView: DataView) {\n const parsedReceivePort = dataView.getUint16(0, true);\n if (parsedReceivePort != this.receivePort) {\n _console.error(\n `incorrect receivePort (expected ${this.receivePort}, got ${parsedReceivePort})`\n );\n return;\n }\n this.#didSetRemoteReceivePort = true;\n }\n\n // SOCKET\n #socket?: dgram.Socket;\n get socket() {\n return this.#socket;\n }\n set socket(newSocket) {\n if (this.#socket == newSocket) {\n _console.log(\"redundant socket assignment\");\n return;\n }\n\n _console.log(\"assigning socket\", newSocket);\n\n if (this.#socket) {\n _console.log(\"removing existing socket...\");\n removeEventListeners(this.#socket, this.#boundSocketEventListeners);\n try {\n this.#socket.close();\n } catch (error) {\n _console.error(error);\n }\n }\n\n if (newSocket) {\n addEventListeners(newSocket, this.#boundSocketEventListeners);\n }\n this.#socket = newSocket;\n\n _console.log(\"assigned socket\");\n }\n\n // SOCKET MESSAGING\n #sendMessage(message: MessageLike) {\n // this.assertIsConnected();\n _console.log(\"sending socket message\", message);\n const dataView = Buffer.from(message);\n this.#socket!.send(dataView);\n this.#pingTimer.restart();\n }\n\n #sendSocketMessage(...messages: SocketMessage[]) {\n this.#sendMessage(createSocketMessage(...messages));\n }\n\n // BASE CONNECTION MANAGER\n async sendSmpMessage(data: ArrayBuffer) {\n super.sendSmpMessage(data);\n _console.error(\"smp not supported on udp\");\n }\n\n async sendTxData(data: ArrayBuffer) {\n super.sendTxData(data);\n if (data.byteLength == 0) {\n return;\n }\n this.#sendSocketMessage({ type: \"message\", data });\n }\n\n // SOCKET EVENTS\n #boundSocketEventListeners: { [eventType: string]: Function } = {\n close: this.#onSocketClose.bind(this),\n connect: this.#onSocketConnect.bind(this),\n error: this.#onSocketError.bind(this),\n listening: this.#onSocketListening.bind(this),\n message: this.#onSocketMessage.bind(this),\n };\n\n #onSocketClose() {\n _console.log(\"socket.close\");\n this.status = \"notConnected\";\n this.clear();\n }\n #onSocketConnect() {\n _console.log(\"socket.connect\");\n this.#pingTimer.start(true);\n }\n #onSocketError(error: Error) {\n _console.error(\"socket.error\", error);\n }\n #onSocketListening() {\n const address = this.socket!.address();\n _console.log(`socket.listening on ${address.address}:${address.port}`);\n this.receivePort = address.port;\n this.socket!.connect(UDPSendPort, this.ipAddress);\n }\n #onSocketMessage(message: Buffer, remoteInfo: dgram.RemoteInfo) {\n this.#pongTimeoutTimer.stop();\n _console.log(\"socket.message\", message.byteLength, remoteInfo);\n const arrayBuffer = message.buffer.slice(\n message.byteOffset,\n message.byteOffset + message.byteLength\n );\n const dataView = new DataView(arrayBuffer);\n this.#parseSocketMessage(dataView);\n\n if (this.status == \"connecting\" && this.#didSetRemoteReceivePort) {\n this.status = \"connected\";\n this.#requestDeviceInformation();\n }\n }\n\n #setupSocket() {\n this.#didSetRemoteReceivePort = false;\n this.socket = dgram.createSocket({\n type: \"udp4\",\n });\n try {\n if (this.receivePort) {\n this.socket.bind(this.receivePort);\n } else {\n this.socket.bind();\n }\n } catch (error) {\n _console.error(error);\n this.disconnect();\n }\n }\n\n // CONNECTION\n async connect() {\n const canContinue = await super.connect();\n if (!canContinue) {\n return false;\n }\n this.#setupSocket();\n return true;\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n _console.log(\"closing socket\");\n this.#pingTimer.stop();\n try {\n this.#socket?.close();\n return true;\n } catch (error) {\n _console.error(error);\n return false;\n }\n }\n\n get canReconnect() {\n return Boolean(this.socket);\n }\n async reconnect() {\n const canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n this.#setupSocket();\n return true;\n }\n\n // PARSING\n #parseSocketMessage(dataView: DataView) {\n parseMessage(\n dataView,\n SocketMessageTypes,\n this.#onMessage.bind(this),\n null,\n true\n );\n }\n\n #onMessage(messageType: SocketMessageType, dataView: DataView) {\n _console.log(\n `received \"${messageType}\" message (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"ping\":\n this.#pong();\n break;\n case \"pong\":\n break;\n case \"setRemoteReceivePort\":\n this.#parseReceivePort(dataView);\n break;\n case \"batteryLevel\":\n this.onMessageReceived?.(\"batteryLevel\", dataView);\n break;\n case \"deviceInformation\":\n parseMessage(\n dataView,\n DeviceInformationTypes,\n (deviceInformationType, dataView) => {\n this.onMessageReceived!(deviceInformationType, dataView);\n }\n );\n break;\n case \"message\":\n this.parseRxMessage(dataView);\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // PING\n #pingTimer = new Timer(this.#ping.bind(this), UDPPingInterval);\n #ping() {\n _console.log(\"pinging\");\n if (this.#didSetRemoteReceivePort || !this.#receivePort) {\n this.#sendSocketMessage(\"ping\");\n } else {\n this.#sendSocketMessage({\n type: \"setRemoteReceivePort\",\n data: this.#setRemoteReceivePortDataView,\n });\n }\n if (this.isConnected) {\n this.#pongTimeoutTimer.start();\n }\n }\n #pong() {\n _console.log(\"ponging\");\n this.#sendSocketMessage(\"pong\");\n }\n\n #pongTimeout() {\n this.#pongTimeoutTimer.stop();\n _console.log(\"pong timeout\");\n this.disconnect();\n }\n #pongTimeoutTimer = new Timer(() => this.#pongTimeout(), 1_000);\n\n // DEVICE INFORMATION\n #requestDeviceInformation() {\n this.#sendSocketMessage(...SocketDeviceInformationMessageTypes);\n }\n\n clear() {\n super.clear();\n this.#didSetRemoteReceivePort = false;\n this.#pingTimer.stop();\n this.#pongTimeoutTimer.stop();\n }\n\n remove() {\n super.remove();\n this.socket = undefined;\n }\n}\n\nexport default UDPConnectionManager;\n","import { createConsole } from \"./utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"./utils/EventDispatcher.ts\";\nimport BaseConnectionManager, {\n TxMessage,\n TxRxMessageType,\n ConnectionStatus,\n ConnectionMessageType,\n MetaConnectionMessageTypes,\n BatteryLevelMessageTypes,\n ConnectionEventTypes,\n ConnectionStatusEventMessages,\n ConnectOptions,\n} from \"./connection/BaseConnectionManager.ts\";\nimport { isInBrowser, isInNode } from \"./utils/environment.ts\";\nimport WebBluetoothConnectionManager from \"./connection/bluetooth/WebBluetoothConnectionManager.ts\";\nimport SensorConfigurationManager, {\n SendSensorConfigurationMessageCallback,\n SensorConfiguration,\n SensorConfigurationEventDispatcher,\n SensorConfigurationEventMessages,\n SensorConfigurationEventTypes,\n SensorConfigurationMessageType,\n SensorConfigurationMessageTypes,\n} from \"./sensor/SensorConfigurationManager.ts\";\nimport SensorDataManager, {\n SensorDataEventMessages,\n SensorDataEventTypes,\n SensorDataMessageType,\n SensorDataMessageTypes,\n SensorType,\n ContinuousSensorTypes,\n SensorDataEventDispatcher,\n RequiredPressureMessageTypes,\n} from \"./sensor/SensorDataManager.ts\";\nimport VibrationManager, {\n SendVibrationMessageCallback,\n VibrationConfiguration,\n VibrationEventDispatcher,\n VibrationEventTypes,\n VibrationMessageType,\n VibrationMessageTypes,\n} from \"./vibration/VibrationManager.ts\";\nimport FileTransferManager, {\n FileTransferEventTypes,\n FileTransferEventMessages,\n FileTransferEventDispatcher,\n SendFileTransferMessageCallback,\n FileTransferMessageTypes,\n FileTransferMessageType,\n FileType,\n FileTypes,\n RequiredFileTransferMessageTypes,\n SendFileCallback,\n} from \"./FileTransferManager.ts\";\nimport TfliteManager, {\n TfliteEventTypes,\n TfliteEventMessages,\n TfliteEventDispatcher,\n SendTfliteMessageCallback,\n TfliteMessageTypes,\n TfliteMessageType,\n TfliteSensorTypes,\n TfliteFileConfiguration,\n TfliteSensorType,\n RequiredTfliteMessageTypes,\n} from \"./TfliteManager.ts\";\nimport FirmwareManager, {\n FirmwareEventDispatcher,\n FirmwareEventMessages,\n FirmwareEventTypes,\n FirmwareMessageType,\n FirmwareMessageTypes,\n} from \"./FirmwareManager.ts\";\nimport DeviceInformationManager, {\n DeviceInformationEventDispatcher,\n DeviceInformationEventTypes,\n DeviceInformationType,\n DeviceInformationTypes,\n DeviceInformationEventMessages,\n} from \"./DeviceInformationManager.ts\";\nimport InformationManager, {\n DeviceType,\n InformationEventDispatcher,\n InformationEventTypes,\n InformationMessageType,\n InformationMessageTypes,\n InformationEventMessages,\n SendInformationMessageCallback,\n} from \"./InformationManager.ts\";\nimport { FileLike } from \"./utils/ArrayBufferUtils.ts\";\nimport DeviceManager from \"./DeviceManager.ts\";\nimport CameraManager, {\n CameraEventDispatcher,\n CameraEventMessages,\n CameraEventTypes,\n CameraMessageType,\n CameraMessageTypes,\n RequiredCameraMessageTypes,\n SendCameraMessageCallback,\n} from \"./CameraManager.ts\";\nimport MicrophoneManager, {\n MicrophoneEventDispatcher,\n MicrophoneEventMessages,\n MicrophoneEventTypes,\n MicrophoneMessageType,\n MicrophoneMessageTypes,\n RequiredMicrophoneMessageTypes,\n SendMicrophoneMessageCallback,\n} from \"./MicrophoneManager.ts\";\nimport DisplayManager, {\n DisplayEventDispatcher,\n DisplayEventMessages,\n DisplayEventTypes,\n DisplayMessageType,\n DisplayMessageTypes,\n RequiredDisplayMessageTypes,\n SendDisplayMessageCallback,\n} from \"./DisplayManager.ts\";\nimport WifiManager, {\n RequiredWifiMessageTypes,\n SendWifiMessageCallback,\n WifiEventDispatcher,\n WifiEventMessages,\n WifiEventTypes,\n WifiMessageType,\n WifiMessageTypes,\n} from \"./WifiManager.ts\";\nimport WebSocketConnectionManager from \"./connection/websocket/WebSocketConnectionManager.ts\";\nimport ClientConnectionManager from \"./connection/ClientConnectionManager.ts\";\n\n/** NODE_START */\nimport UDPConnectionManager from \"./connection/udp/UDPConnectionManager.ts\";\nimport { DisplayManagerInterface } from \"./utils/DisplayManagerInterface.ts\";\n/** NODE_END */\n\nconst _console = createConsole(\"Device\", { log: false });\n\nexport const DeviceEventTypes = [\n \"connectionMessage\",\n ...ConnectionEventTypes,\n ...MetaConnectionMessageTypes,\n ...BatteryLevelMessageTypes,\n ...InformationEventTypes,\n ...DeviceInformationEventTypes,\n ...SensorConfigurationEventTypes,\n ...SensorDataEventTypes,\n ...VibrationEventTypes,\n ...FileTransferEventTypes,\n ...TfliteEventTypes,\n ...WifiEventTypes,\n ...CameraEventTypes,\n ...MicrophoneEventTypes,\n ...DisplayEventTypes,\n ...FirmwareEventTypes,\n] as const;\nexport type DeviceEventType = (typeof DeviceEventTypes)[number];\n\nexport interface DeviceEventMessages\n extends ConnectionStatusEventMessages,\n DeviceInformationEventMessages,\n InformationEventMessages,\n SensorDataEventMessages,\n SensorConfigurationEventMessages,\n TfliteEventMessages,\n FileTransferEventMessages,\n WifiEventMessages,\n CameraEventMessages,\n MicrophoneEventMessages,\n DisplayEventMessages,\n FirmwareEventMessages {\n batteryLevel: { batteryLevel: number };\n connectionMessage: { messageType: ConnectionMessageType; dataView: DataView };\n}\n\nexport type SendMessageCallback<MessageType extends string> = (\n messages?: { type: MessageType; data?: ArrayBuffer }[],\n sendImmediately?: boolean\n) => Promise<void>;\n\nexport type SendSmpMessageCallback = (data: ArrayBuffer) => Promise<void>;\n\nexport type DeviceEventDispatcher = EventDispatcher<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type DeviceEvent = Event<Device, DeviceEventType, DeviceEventMessages>;\nexport type DeviceEventMap = EventMap<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type DeviceEventListenerMap = EventListenerMap<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\nexport type BoundDeviceEventListeners = BoundEventListeners<\n Device,\n DeviceEventType,\n DeviceEventMessages\n>;\n\nexport const RequiredInformationConnectionMessages: TxRxMessageType[] = [\n \"isCharging\",\n \"getBatteryCurrent\",\n \"getId\",\n \"getMtu\",\n\n \"getName\",\n \"getType\",\n \"getCurrentTime\",\n \"getSensorConfiguration\",\n \"getSensorScalars\",\n\n \"getVibrationLocations\",\n\n \"getFileTypes\",\n\n \"isWifiAvailable\",\n];\n\nclass Device {\n get bluetoothId() {\n return this.#connectionManager?.bluetoothId;\n }\n\n get isAvailable() {\n return this.#connectionManager?.isAvailable;\n }\n\n constructor() {\n this.#deviceInformationManager.eventDispatcher = this\n .#eventDispatcher as DeviceInformationEventDispatcher;\n\n this._informationManager.sendMessage = this\n .sendTxMessages as SendInformationMessageCallback;\n this._informationManager.eventDispatcher = this\n .#eventDispatcher as InformationEventDispatcher;\n\n this.#sensorConfigurationManager.sendMessage = this\n .sendTxMessages as SendSensorConfigurationMessageCallback;\n this.#sensorConfigurationManager.eventDispatcher = this\n .#eventDispatcher as SensorConfigurationEventDispatcher;\n\n this.#sensorDataManager.eventDispatcher = this\n .#eventDispatcher as SensorDataEventDispatcher;\n\n this.#vibrationManager.sendMessage = this\n .sendTxMessages as SendVibrationMessageCallback;\n this.#vibrationManager.eventDispatcher = this\n .#eventDispatcher as VibrationEventDispatcher;\n\n this.#tfliteManager.sendMessage = this\n .sendTxMessages as SendTfliteMessageCallback;\n this.#tfliteManager.eventDispatcher = this\n .#eventDispatcher as TfliteEventDispatcher;\n\n this.#fileTransferManager.sendMessage = this\n .sendTxMessages as SendFileTransferMessageCallback;\n this.#fileTransferManager.eventDispatcher = this\n .#eventDispatcher as FileTransferEventDispatcher;\n\n this.#wifiManager.sendMessage = this\n .sendTxMessages as SendWifiMessageCallback;\n this.#wifiManager.eventDispatcher = this\n .#eventDispatcher as WifiEventDispatcher;\n\n this.#cameraManager.sendMessage = this\n .sendTxMessages as SendCameraMessageCallback;\n this.#cameraManager.eventDispatcher = this\n .#eventDispatcher as CameraEventDispatcher;\n\n this.#microphoneManager.sendMessage = this\n .sendTxMessages as SendMicrophoneMessageCallback;\n this.#microphoneManager.eventDispatcher = this\n .#eventDispatcher as MicrophoneEventDispatcher;\n\n this.#displayManager.sendMessage = this\n .sendTxMessages as SendDisplayMessageCallback;\n this.#displayManager.eventDispatcher = this\n .#eventDispatcher as DisplayEventDispatcher;\n this.#displayManager.sendFile = this.#fileTransferManager\n .send as SendFileCallback;\n\n this.#firmwareManager.sendMessage = this\n .sendSmpMessage as SendSmpMessageCallback;\n this.#firmwareManager.eventDispatcher = this\n .#eventDispatcher as FirmwareEventDispatcher;\n\n this.addEventListener(\"getMtu\", () => {\n _console.log(\"updating mtu...\");\n this.#firmwareManager.mtu = this.mtu;\n this.#fileTransferManager.mtu = this.mtu;\n this.connectionManager!.mtu = this.mtu;\n this.#displayManager.mtu = this.mtu;\n });\n this.addEventListener(\"getSensorConfiguration\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.sensorTypes.includes(\"pressure\")) {\n _console.log(\"requesting required pressure information\");\n const messages = RequiredPressureMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request pressure infomration\");\n }\n\n if (this.sensorTypes.includes(\"camera\")) {\n _console.log(\"requesting required camera information\");\n const messages = RequiredCameraMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request camera infomration\");\n }\n\n if (this.sensorTypes.includes(\"microphone\")) {\n _console.log(\"requesting required microphone information\");\n const messages = RequiredMicrophoneMessageTypes.map((messageType) => ({\n type: messageType,\n }));\n this.sendTxMessages(messages, false);\n } else {\n _console.log(\"don't need to request microphone infomration\");\n }\n });\n this.addEventListener(\"getFileTypes\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.fileTypes.length > 0) {\n this.#fileTransferManager.requestRequiredInformation();\n }\n if (this.fileTypes.includes(\"tflite\")) {\n this.#tfliteManager.requestRequiredInformation();\n }\n });\n this.addEventListener(\"isWifiAvailable\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.connectionType == \"client\" && !isInNode) {\n return;\n }\n if (this.isWifiAvailable) {\n if (this.connectionType != \"client\") {\n this.#wifiManager.requestRequiredInformation();\n }\n }\n });\n this.addEventListener(\"getType\", () => {\n if (this.connectionStatus != \"connecting\") {\n return;\n }\n if (this.type == \"glasses\") {\n this.#displayManager.requestRequiredInformation();\n }\n });\n this.addEventListener(\"fileTransferProgress\", (event) => {\n const { fileType, progress } = event.message;\n switch (fileType) {\n case \"spriteSheet\":\n this.#dispatchEvent(\"displaySpriteSheetUploadProgress\", {\n spriteSheet: this.#displayManager.pendingSpriteSheet!,\n spriteSheetName: this.#displayManager.pendingSpriteSheetName!,\n progress,\n });\n break;\n default:\n break;\n }\n });\n this.addEventListener(\"fileTransferStatus\", (event) => {\n const { fileType, fileTransferStatus } = event.message;\n switch (fileType) {\n case \"spriteSheet\":\n if (fileTransferStatus == \"sending\") {\n this.#dispatchEvent(\"displaySpriteSheetUploadStart\", {\n spriteSheet: this.#displayManager.pendingSpriteSheet!,\n spriteSheetName: this.#displayManager.pendingSpriteSheetName!,\n });\n }\n break;\n default:\n break;\n }\n });\n DeviceManager.onDevice(this);\n if (isInBrowser) {\n window.addEventListener(\"beforeunload\", () => {\n if (this.isConnected && this.clearSensorConfigurationOnLeave) {\n this.clearSensorConfiguration();\n }\n });\n }\n if (isInNode) {\n /** can add more node leave handlers https://gist.github.com/hyrious/30a878f6e6a057f09db87638567cb11a */\n process.on(\"exit\", () => {\n if (this.isConnected && this.clearSensorConfigurationOnLeave) {\n this.clearSensorConfiguration();\n }\n });\n }\n }\n\n static #DefaultConnectionManager(): BaseConnectionManager {\n return new WebBluetoothConnectionManager();\n }\n\n #eventDispatcher: DeviceEventDispatcher = new EventDispatcher(\n this as Device,\n DeviceEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n get removeEventListeners() {\n return this.#eventDispatcher.removeEventListeners;\n }\n get removeAllEventListeners() {\n return this.#eventDispatcher.removeAllEventListeners;\n }\n\n // CONNECTION MANAGER\n\n #connectionManager?: BaseConnectionManager;\n get connectionManager() {\n return this.#connectionManager;\n }\n set connectionManager(newConnectionManager) {\n if (this.connectionManager == newConnectionManager) {\n _console.log(\"same connectionManager is already assigned\");\n return;\n }\n\n if (this.connectionManager) {\n this.connectionManager.remove();\n }\n if (newConnectionManager) {\n newConnectionManager.onStatusUpdated =\n this.#onConnectionStatusUpdated.bind(this);\n newConnectionManager.onMessageReceived =\n this.#onConnectionMessageReceived.bind(this);\n newConnectionManager.onMessagesReceived =\n this.#onConnectionMessagesReceived.bind(this);\n }\n\n this.#connectionManager = newConnectionManager;\n _console.log(\"assigned new connectionManager\", this.#connectionManager);\n\n this._informationManager.connectionType = this.connectionType;\n }\n async #sendTxMessages(messages?: TxMessage[], sendImmediately?: boolean) {\n await this.#connectionManager?.sendTxMessages(messages, sendImmediately);\n }\n private sendTxMessages = this.#sendTxMessages.bind(this);\n\n async connect(options?: ConnectOptions) {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return;\n }\n if (this.connectionStatus == \"connecting\") {\n _console.log(\"already connecting\");\n return;\n }\n\n _console.log(\"connect options\", options);\n if (options) {\n switch (options.type) {\n case \"webBluetooth\":\n if (this.connectionType != \"webBluetooth\") {\n this.connectionManager = new WebBluetoothConnectionManager();\n }\n break;\n case \"webSocket\":\n {\n let createConnectionManager = false;\n if (this.connectionType == \"webSocket\") {\n const connectionManager = this\n .connectionManager as WebSocketConnectionManager;\n if (\n connectionManager.ipAddress != options.ipAddress ||\n connectionManager.isSecure != options.isWifiSecure\n ) {\n createConnectionManager = true;\n }\n } else {\n createConnectionManager = true;\n }\n if (createConnectionManager) {\n this.connectionManager = new WebSocketConnectionManager(\n options.ipAddress,\n options.isWifiSecure,\n this.bluetoothId\n );\n }\n }\n\n break;\n case \"udp\":\n {\n let createConnectionManager = false;\n if (this.connectionType == \"udp\") {\n const connectionManager = this\n .connectionManager as UDPConnectionManager;\n if (connectionManager.ipAddress != options.ipAddress) {\n createConnectionManager = true;\n }\n this.reconnectOnDisconnection = true;\n } else {\n createConnectionManager = true;\n }\n if (createConnectionManager) {\n this.connectionManager = new UDPConnectionManager(\n options.ipAddress,\n this.bluetoothId\n );\n }\n }\n break;\n }\n }\n if (!this.connectionManager) {\n this.connectionManager = Device.#DefaultConnectionManager();\n }\n this.#clear();\n\n if (options?.type == \"client\") {\n _console.assertWithError(\n this.connectionType == \"client\",\n \"expected clientConnectionManager\"\n );\n const clientConnectionManager = this\n .connectionManager as ClientConnectionManager;\n clientConnectionManager.subType = options.subType;\n return clientConnectionManager.connect();\n }\n _console.log(\"connectionManager type\", this.connectionManager.type);\n return this.connectionManager.connect();\n }\n #isConnected = false;\n get isConnected() {\n return this.#isConnected;\n }\n /** @throws {Error} if not connected */\n #assertIsConnected() {\n _console.assertWithError(this.isConnected, \"notConnected\");\n }\n\n #didReceiveMessageTypes(messageTypes: ConnectionMessageType[]) {\n return messageTypes.every((messageType) => {\n const hasConnectionMessage =\n this.latestConnectionMessages.has(messageType);\n if (!hasConnectionMessage) {\n _console.log(`didn't receive \"${messageType}\" message`);\n }\n return hasConnectionMessage;\n });\n }\n get #hasRequiredInformation() {\n let hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredInformationConnectionMessages\n );\n if (hasRequiredInformation && this.sensorTypes.includes(\"pressure\")) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredPressureMessageTypes\n );\n }\n if (hasRequiredInformation && this.isWifiAvailable) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredWifiMessageTypes\n );\n }\n if (hasRequiredInformation && this.fileTypes.length > 0) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredFileTransferMessageTypes\n );\n }\n if (hasRequiredInformation && this.fileTypes.includes(\"tflite\")) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredTfliteMessageTypes\n );\n }\n if (hasRequiredInformation && this.hasCamera) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredCameraMessageTypes\n );\n }\n if (hasRequiredInformation && this.hasMicrophone) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredMicrophoneMessageTypes\n );\n }\n if (hasRequiredInformation && this.isDisplayAvailable) {\n hasRequiredInformation = this.#didReceiveMessageTypes(\n RequiredDisplayMessageTypes\n );\n }\n return hasRequiredInformation;\n }\n #requestRequiredInformation() {\n _console.log(\"requesting required information\");\n const messages: TxMessage[] = RequiredInformationConnectionMessages.map(\n (messageType) => ({\n type: messageType,\n })\n );\n this.#sendTxMessages(messages);\n }\n\n get canReconnect() {\n return this.connectionManager?.canReconnect;\n }\n #assertCanReconnect() {\n _console.assertWithError(this.canReconnect, \"cannot reconnect to device\");\n }\n async reconnect() {\n if (this.isConnected) {\n _console.log(\"already connected\");\n return;\n }\n if (this.connectionStatus == \"connecting\") {\n _console.log(\"already connecting\");\n return;\n }\n if (!this.canReconnect) {\n _console.warn(\"cannot reconnect\");\n return false;\n }\n // this.#assertCanReconnect();\n _console.log(\"attempting to reconnect...\");\n this.#clear();\n _console.log(\"reconnecting...\");\n return this.connectionManager?.reconnect();\n }\n\n static async Connect() {\n const device = new Device();\n await device.connect();\n return device;\n }\n\n static #ReconnectOnDisconnection = false;\n static get ReconnectOnDisconnection() {\n return this.#ReconnectOnDisconnection;\n }\n static set ReconnectOnDisconnection(newReconnectOnDisconnection) {\n _console.assertTypeWithError(newReconnectOnDisconnection, \"boolean\");\n this.#ReconnectOnDisconnection = newReconnectOnDisconnection;\n }\n\n #reconnectOnDisconnection = Device.ReconnectOnDisconnection;\n get reconnectOnDisconnection() {\n return this.#reconnectOnDisconnection;\n }\n set reconnectOnDisconnection(newReconnectOnDisconnection) {\n _console.assertTypeWithError(newReconnectOnDisconnection, \"boolean\");\n this.#reconnectOnDisconnection = newReconnectOnDisconnection;\n }\n #reconnectIntervalId?: NodeJS.Timeout | number;\n\n get connectionType() {\n return this.connectionManager?.type;\n }\n async disconnect() {\n if (!this.isConnected) {\n _console.log(\"already not connected\");\n return;\n }\n if (this.connectionStatus == \"disconnecting\") {\n _console.log(\"already disconnecting\");\n return;\n }\n //this.#assertIsConnected();\n if (this.reconnectOnDisconnection) {\n this.reconnectOnDisconnection = false;\n this.addEventListener(\n \"isConnected\",\n () => {\n this.reconnectOnDisconnection = true;\n },\n { once: true }\n );\n }\n\n return this.connectionManager!.disconnect();\n }\n\n toggleConnection() {\n if (this.isConnected) {\n this.disconnect();\n } else if (this.canReconnect) {\n try {\n this.reconnect();\n } catch (error) {\n _console.error(\"error trying to reconnect\", error);\n this.connect();\n }\n } else {\n this.connect();\n }\n }\n\n get connectionStatus(): ConnectionStatus {\n switch (this.#connectionManager?.status) {\n case \"connected\":\n return this.isConnected ? \"connected\" : \"connecting\";\n case \"notConnected\":\n case \"connecting\":\n case \"disconnecting\":\n return this.#connectionManager.status;\n default:\n return \"notConnected\";\n }\n }\n get isConnectionBusy() {\n return (\n this.connectionStatus == \"connecting\" ||\n this.connectionStatus == \"disconnecting\"\n );\n }\n\n #onConnectionStatusUpdated(connectionStatus: ConnectionStatus) {\n _console.log({ connectionStatus });\n\n if (connectionStatus == \"notConnected\") {\n this.#clearConnection();\n\n if (this.canReconnect && this.reconnectOnDisconnection) {\n _console.log(\"starting reconnect interval...\");\n this.#reconnectIntervalId = setInterval(() => {\n _console.log(\"attempting reconnect...\");\n this.reconnect();\n }, 1000);\n }\n } else {\n if (this.#reconnectIntervalId != undefined) {\n _console.log(\"clearing reconnect interval\");\n clearInterval(this.#reconnectIntervalId);\n this.#reconnectIntervalId = undefined;\n }\n }\n\n this.#checkConnection();\n\n if (connectionStatus == \"connected\" && !this.#isConnected) {\n if (this.connectionType != \"client\") {\n this.#requestRequiredInformation();\n }\n }\n\n DeviceManager.OnDeviceConnectionStatusUpdated(this, connectionStatus);\n }\n\n #dispatchConnectionEvents(includeIsConnected: boolean = false) {\n this.#dispatchEvent(\"connectionStatus\", {\n connectionStatus: this.connectionStatus,\n });\n this.#dispatchEvent(this.connectionStatus, {});\n if (includeIsConnected) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n }\n #checkConnection() {\n this.#isConnected =\n Boolean(this.connectionManager?.isConnected) &&\n this.#hasRequiredInformation &&\n this._informationManager.isCurrentTimeSet;\n\n switch (this.connectionStatus) {\n case \"connected\":\n if (this.#isConnected) {\n this.#dispatchConnectionEvents(true);\n }\n break;\n case \"notConnected\":\n this.#dispatchConnectionEvents(true);\n break;\n default:\n this.#dispatchConnectionEvents(false);\n break;\n }\n }\n\n #clear() {\n this.#clearConnection();\n this._informationManager.clear();\n this.#deviceInformationManager.clear();\n this.#tfliteManager.clear();\n this.#fileTransferManager.clear();\n this.#wifiManager.clear();\n this.#cameraManager.clear();\n this.#microphoneManager.clear();\n this.#sensorConfigurationManager.clear();\n this.#displayManager.reset();\n this.#isServerSide = false;\n }\n #clearConnection() {\n this.connectionManager?.clear();\n this.latestConnectionMessages.clear();\n }\n\n #onConnectionMessageReceived(\n messageType: ConnectionMessageType,\n dataView: DataView\n ) {\n _console.log({ messageType, dataView });\n switch (messageType) {\n case \"batteryLevel\":\n const batteryLevel = dataView.getUint8(0);\n _console.log(\"received battery level\", { batteryLevel });\n this.#updateBatteryLevel(batteryLevel);\n break;\n\n default:\n if (\n FileTransferMessageTypes.includes(\n messageType as FileTransferMessageType\n )\n ) {\n this.#fileTransferManager.parseMessage(\n messageType as FileTransferMessageType,\n dataView\n );\n } else if (\n TfliteMessageTypes.includes(messageType as TfliteMessageType)\n ) {\n this.#tfliteManager.parseMessage(\n messageType as TfliteMessageType,\n dataView\n );\n } else if (\n SensorDataMessageTypes.includes(messageType as SensorDataMessageType)\n ) {\n this.#sensorDataManager.parseMessage(\n messageType as SensorDataMessageType,\n dataView\n );\n } else if (\n FirmwareMessageTypes.includes(messageType as FirmwareMessageType)\n ) {\n this.#firmwareManager.parseMessage(\n messageType as FirmwareMessageType,\n dataView\n );\n } else if (\n DeviceInformationTypes.includes(messageType as DeviceInformationType)\n ) {\n this.#deviceInformationManager.parseMessage(\n messageType as DeviceInformationType,\n dataView\n );\n } else if (\n InformationMessageTypes.includes(\n messageType as InformationMessageType\n )\n ) {\n this._informationManager.parseMessage(\n messageType as InformationMessageType,\n dataView\n );\n } else if (\n SensorConfigurationMessageTypes.includes(\n messageType as SensorConfigurationMessageType\n )\n ) {\n this.#sensorConfigurationManager.parseMessage(\n messageType as SensorConfigurationMessageType,\n dataView\n );\n } else if (\n VibrationMessageTypes.includes(messageType as VibrationMessageType)\n ) {\n this.#vibrationManager.parseMessage(\n messageType as VibrationMessageType,\n dataView\n );\n } else if (WifiMessageTypes.includes(messageType as WifiMessageType)) {\n this.#wifiManager.parseMessage(\n messageType as WifiMessageType,\n dataView\n );\n } else if (\n CameraMessageTypes.includes(messageType as CameraMessageType)\n ) {\n this.#cameraManager.parseMessage(\n messageType as CameraMessageType,\n dataView\n );\n } else if (\n MicrophoneMessageTypes.includes(messageType as MicrophoneMessageType)\n ) {\n this.#microphoneManager.parseMessage(\n messageType as MicrophoneMessageType,\n dataView\n );\n } else if (\n DisplayMessageTypes.includes(messageType as DisplayMessageType)\n ) {\n this.#displayManager.parseMessage(\n messageType as DisplayMessageType,\n dataView\n );\n } else {\n throw Error(`uncaught messageType ${messageType}`);\n }\n }\n\n this.latestConnectionMessages.set(messageType, dataView);\n if (messageType.startsWith(\"set\")) {\n this.latestConnectionMessages.set(\n // @ts-expect-error\n messageType.replace(\"set\", \"get\"),\n dataView\n );\n }\n this.#dispatchEvent(\"connectionMessage\", { messageType, dataView });\n }\n #onConnectionMessagesReceived() {\n if (!this.isConnected && this.#hasRequiredInformation) {\n this.#checkConnection();\n }\n if (\n this.connectionStatus == \"notConnected\" ||\n this.connectionStatus == \"disconnecting\"\n ) {\n return;\n }\n this.#sendTxMessages();\n }\n\n latestConnectionMessages: Map<ConnectionMessageType, DataView> = new Map();\n\n // DEVICE INFORMATION\n #deviceInformationManager = new DeviceInformationManager();\n get deviceInformation() {\n return this.#deviceInformationManager.information;\n }\n\n // BATTERY LEVEL\n #batteryLevel = 0;\n get batteryLevel() {\n return this.#batteryLevel;\n }\n #updateBatteryLevel(updatedBatteryLevel: number) {\n _console.assertTypeWithError(updatedBatteryLevel, \"number\");\n if (this.#batteryLevel == updatedBatteryLevel) {\n _console.log(`duplicate batteryLevel assignment ${updatedBatteryLevel}`);\n return;\n }\n this.#batteryLevel = updatedBatteryLevel;\n _console.log({ updatedBatteryLevel: this.#batteryLevel });\n this.#dispatchEvent(\"batteryLevel\", { batteryLevel: this.#batteryLevel });\n }\n\n // INFORMATION\n /** @private */\n _informationManager = new InformationManager();\n\n get id() {\n return this._informationManager.id;\n }\n\n get isCharging() {\n return this._informationManager.isCharging;\n }\n get batteryCurrent() {\n return this._informationManager.batteryCurrent;\n }\n get getBatteryCurrent() {\n return this._informationManager.getBatteryCurrent;\n }\n\n get name() {\n return this._informationManager.name;\n }\n get setName() {\n return this._informationManager.setName;\n }\n\n get type() {\n return this._informationManager.type;\n }\n get setType() {\n return this._informationManager.setType;\n }\n\n get isInsole() {\n return this._informationManager.isInsole;\n }\n get isGlove() {\n return this._informationManager.isGlove;\n }\n get side() {\n return this._informationManager.side;\n }\n\n get mtu() {\n return this._informationManager.mtu;\n }\n\n // SENSOR TYPES\n get sensorTypes() {\n return Object.keys(this.sensorConfiguration) as SensorType[];\n }\n get continuousSensorTypes() {\n return ContinuousSensorTypes.filter((sensorType) =>\n this.sensorTypes.includes(sensorType)\n );\n }\n\n // SENSOR CONFIGURATION\n\n #sensorConfigurationManager = new SensorConfigurationManager();\n\n get sensorConfiguration() {\n return this.#sensorConfigurationManager.configuration;\n }\n\n get setSensorConfiguration() {\n return this.#sensorConfigurationManager.setConfiguration;\n }\n\n async clearSensorConfiguration() {\n return this.#sensorConfigurationManager.clearSensorConfiguration();\n }\n\n static #ClearSensorConfigurationOnLeave = true;\n static get ClearSensorConfigurationOnLeave() {\n return this.#ClearSensorConfigurationOnLeave;\n }\n static set ClearSensorConfigurationOnLeave(\n newClearSensorConfigurationOnLeave\n ) {\n _console.assertTypeWithError(newClearSensorConfigurationOnLeave, \"boolean\");\n this.#ClearSensorConfigurationOnLeave = newClearSensorConfigurationOnLeave;\n }\n\n #clearSensorConfigurationOnLeave = Device.ClearSensorConfigurationOnLeave;\n get clearSensorConfigurationOnLeave() {\n return this.#clearSensorConfigurationOnLeave;\n }\n set clearSensorConfigurationOnLeave(newClearSensorConfigurationOnLeave) {\n _console.assertTypeWithError(newClearSensorConfigurationOnLeave, \"boolean\");\n this.#clearSensorConfigurationOnLeave = newClearSensorConfigurationOnLeave;\n }\n\n // PRESSURE\n get numberOfPressureSensors() {\n return this.#sensorDataManager.pressureSensorDataManager.numberOfSensors;\n }\n\n // SENSOR DATA\n #sensorDataManager = new SensorDataManager();\n resetPressureRange() {\n this.#sensorDataManager.pressureSensorDataManager.resetRange();\n }\n\n // VIBRATION\n get vibrationLocations() {\n return this.#vibrationManager.vibrationLocations;\n }\n\n #vibrationManager = new VibrationManager();\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately?: boolean\n ) {\n this.#vibrationManager.triggerVibration(\n vibrationConfigurations,\n sendImmediately\n );\n }\n\n // FILE TRANSFER\n #fileTransferManager = new FileTransferManager();\n\n get fileTypes() {\n return this.#fileTransferManager.fileTypes;\n }\n get maxFileLength() {\n return this.#fileTransferManager.maxLength;\n }\n get validFileTypes() {\n return FileTypes.filter((fileType) => {\n if (fileType.includes(\"wifi\") && !this.isWifiAvailable) {\n return false;\n }\n return true;\n });\n }\n\n async sendFile(fileType: FileType, file: FileLike) {\n _console.assertWithError(\n this.validFileTypes.includes(fileType),\n `invalid fileType ${fileType}`\n );\n const promise = this.waitForEvent(\"fileTransferComplete\");\n this.#fileTransferManager.send(fileType, file);\n await promise;\n }\n async receiveFile(fileType: FileType) {\n const promise = this.waitForEvent(\"fileTransferComplete\");\n this.#fileTransferManager.receive(fileType);\n await promise;\n }\n\n get fileTransferStatus() {\n return this.#fileTransferManager.status;\n }\n\n cancelFileTransfer() {\n this.#fileTransferManager.cancel();\n }\n\n // TFLITE\n #tfliteManager = new TfliteManager();\n\n get isTfliteAvailable() {\n return this.fileTypes.includes(\"tflite\");\n }\n get tfliteName() {\n return this.#tfliteManager.name;\n }\n get setTfliteName() {\n return this.#tfliteManager.setName;\n }\n\n async sendTfliteConfiguration(configuration: TfliteFileConfiguration) {\n configuration.type = \"tflite\";\n this.#tfliteManager.sendConfiguration(configuration, false);\n const didSendFile = await this.#fileTransferManager.send(\n configuration.type,\n configuration.file\n );\n if (!didSendFile) {\n this.#sendTxMessages();\n }\n }\n\n // TFLITE MODEL CONFIG\n get tfliteTask() {\n return this.#tfliteManager.task;\n }\n get setTfliteTask() {\n return this.#tfliteManager.setTask;\n }\n get tfliteSampleRate() {\n return this.#tfliteManager.sampleRate;\n }\n get setTfliteSampleRate() {\n return this.#tfliteManager.setSampleRate;\n }\n get tfliteSensorTypes() {\n return this.#tfliteManager.sensorTypes;\n }\n get allowedTfliteSensorTypes() {\n return this.sensorTypes.filter((sensorType) =>\n TfliteSensorTypes.includes(sensorType as TfliteSensorType)\n );\n }\n get setTfliteSensorTypes() {\n return this.#tfliteManager.setSensorTypes;\n }\n get tfliteIsReady() {\n return this.#tfliteManager.isReady;\n }\n\n // TFLITE INFERENCING\n\n get tfliteInferencingEnabled() {\n return this.#tfliteManager.inferencingEnabled;\n }\n get setTfliteInferencingEnabled() {\n return this.#tfliteManager.setInferencingEnabled;\n }\n async enableTfliteInferencing() {\n return this.setTfliteInferencingEnabled(true);\n }\n async disableTfliteInferencing() {\n return this.setTfliteInferencingEnabled(false);\n }\n get toggleTfliteInferencing() {\n return this.#tfliteManager.toggleInferencingEnabled;\n }\n\n // TFLITE INFERENCE CONFIG\n\n get tfliteCaptureDelay() {\n return this.#tfliteManager.captureDelay;\n }\n get setTfliteCaptureDelay() {\n return this.#tfliteManager.setCaptureDelay;\n }\n get tfliteThreshold() {\n return this.#tfliteManager.threshold;\n }\n get setTfliteThreshold() {\n return this.#tfliteManager.setThreshold;\n }\n\n // FIRMWARE MANAGER\n\n #firmwareManager = new FirmwareManager();\n\n get canUpdateFirmware() {\n return this.#connectionManager?.canUpdateFirmware;\n }\n #assertCanUpdateFirmware() {\n _console.assertWithError(this.canUpdateFirmware, \"can't update firmware\");\n }\n\n #sendSmpMessage(data: ArrayBuffer) {\n this.#assertCanUpdateFirmware();\n return this.#connectionManager!.sendSmpMessage(data);\n }\n private sendSmpMessage = this.#sendSmpMessage.bind(this);\n\n get uploadFirmware() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.uploadFirmware;\n }\n get canReset() {\n return this.canUpdateFirmware;\n }\n async reset() {\n _console.assertWithError(\n this.canReset,\n \"reset is not enabled for this device\"\n );\n await this.#firmwareManager.reset();\n return this.#connectionManager!.disconnect();\n }\n get firmwareStatus() {\n return this.#firmwareManager.status;\n }\n get getFirmwareImages() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.getImages;\n }\n get firmwareImages() {\n return this.#firmwareManager.images;\n }\n get eraseFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.eraseImage;\n }\n get confirmFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.confirmImage;\n }\n get testFirmwareImage() {\n this.#assertCanUpdateFirmware();\n return this.#firmwareManager.testImage;\n }\n\n // SERVER SIDE\n #isServerSide = false;\n get isServerSide() {\n return this.#isServerSide;\n }\n set isServerSide(newIsServerSide) {\n if (this.#isServerSide == newIsServerSide) {\n _console.log(\"redundant isServerSide assignment\");\n return;\n }\n _console.log({ newIsServerSide });\n this.#isServerSide = newIsServerSide;\n\n this.#fileTransferManager.isServerSide = this.isServerSide;\n this.#displayManager.isServerSide = this.isServerSide;\n }\n\n // UKATON\n get isUkaton() {\n return this.deviceInformation.modelNumber.includes(\"Ukaton\");\n }\n\n // WIFI MANAGER\n #wifiManager = new WifiManager();\n get isWifiAvailable() {\n return this.#wifiManager.isWifiAvailable;\n }\n get wifiSSID() {\n return this.#wifiManager.wifiSSID;\n }\n async setWifiSSID(newWifiSSID: string) {\n return this.#wifiManager.setWifiSSID(newWifiSSID);\n }\n get wifiPassword() {\n return this.#wifiManager.wifiPassword;\n }\n async setWifiPassword(newWifiPassword: string) {\n return this.#wifiManager.setWifiPassword(newWifiPassword);\n }\n get isWifiConnected() {\n return this.#wifiManager.isWifiConnected;\n }\n get ipAddress() {\n return this.#wifiManager.ipAddress;\n }\n get wifiConnectionEnabled() {\n return this.#wifiManager.wifiConnectionEnabled;\n }\n get enableWifiConnection() {\n return this.#wifiManager.enableWifiConnection;\n }\n get setWifiConnectionEnabled() {\n return this.#wifiManager.setWifiConnectionEnabled;\n }\n get disableWifiConnection() {\n return this.#wifiManager.disableWifiConnection;\n }\n get toggleWifiConnection() {\n return this.#wifiManager.toggleWifiConnection;\n }\n get isWifiSecure() {\n return this.#wifiManager.isWifiSecure;\n }\n\n async reconnectViaWebSockets() {\n _console.assertWithError(this.isWifiConnected, \"wifi is not connected\");\n _console.assertWithError(\n this.connectionType != \"webSocket\",\n \"already connected via webSockets\"\n );\n _console.assertTypeWithError(this.ipAddress, \"string\");\n _console.log(\"reconnecting via websockets...\");\n await this.disconnect();\n await this.connect({\n type: \"webSocket\",\n ipAddress: this.ipAddress!,\n isWifiSecure: this.isWifiSecure,\n });\n }\n\n async reconnectViaUDP() {\n _console.assertWithError(isInNode, \"udp is only available in node\");\n _console.assertWithError(this.isWifiConnected, \"wifi is not connected\");\n _console.assertWithError(\n this.connectionType != \"udp\",\n \"already connected via udp\"\n );\n _console.assertTypeWithError(this.ipAddress, \"string\");\n _console.log(\"reconnecting via udp...\");\n await this.disconnect();\n await this.connect({\n type: \"udp\",\n ipAddress: this.ipAddress!,\n });\n }\n\n // CAMERA MANAGER\n #cameraManager = new CameraManager();\n get hasCamera() {\n return this.sensorTypes.includes(\"camera\");\n }\n get cameraStatus() {\n return this.#cameraManager.cameraStatus;\n }\n #assertHasCamera() {\n _console.assertWithError(this.hasCamera, \"camera not available\");\n }\n async takePicture(sensorRate: number = 10) {\n this.#assertHasCamera();\n if (this.sensorConfiguration.camera == 0) {\n this.setSensorConfiguration({ camera: sensorRate }, false, false);\n }\n await this.#cameraManager.takePicture();\n }\n async focusCamera(sensorRate: number = 10) {\n this.#assertHasCamera();\n if (this.sensorConfiguration.camera == 0) {\n this.setSensorConfiguration({ camera: sensorRate }, false, false);\n }\n await this.#cameraManager.focus();\n }\n async stopCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.stop();\n }\n async wakeCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.wake();\n }\n async sleepCamera() {\n this.#assertHasCamera();\n await this.#cameraManager.sleep();\n }\n\n get cameraConfiguration() {\n return this.#cameraManager.cameraConfiguration;\n }\n get availableCameraConfigurationTypes() {\n return this.#cameraManager.availableCameraConfigurationTypes;\n }\n get cameraConfigurationRanges() {\n return this.#cameraManager.cameraConfigurationRanges;\n }\n\n get setCameraConfiguration() {\n return this.#cameraManager.setCameraConfiguration;\n }\n\n // MICROPHONE\n #microphoneManager = new MicrophoneManager();\n get hasMicrophone() {\n return this.sensorTypes.includes(\"microphone\");\n }\n get microphoneStatus() {\n return this.#microphoneManager.microphoneStatus;\n }\n #assertHasMicrophone() {\n _console.assertWithError(this.hasMicrophone, \"microphone not available\");\n }\n\n async startMicrophone(sensorRate: number = 10) {\n this.#assertHasMicrophone();\n if (this.sensorConfiguration.microphone == 0) {\n this.setSensorConfiguration({ microphone: sensorRate }, false, false);\n }\n await this.#microphoneManager.start();\n }\n async stopMicrophone() {\n this.#assertHasMicrophone();\n await this.#microphoneManager.stop();\n }\n async enableMicrophoneVad() {\n this.#assertHasMicrophone();\n await this.#microphoneManager.vad();\n }\n async toggleMicrophone(sensorRate: number = 10) {\n this.#assertHasMicrophone();\n if (this.sensorConfiguration.microphone == 0) {\n this.setSensorConfiguration({ microphone: sensorRate }, false, false);\n }\n await this.#microphoneManager.toggle();\n }\n\n get microphoneConfiguration() {\n return this.#microphoneManager.microphoneConfiguration;\n }\n get availableMicrophoneConfigurationTypes() {\n return this.#microphoneManager.availableMicrophoneConfigurationTypes;\n }\n get setMicrophoneConfiguration() {\n return this.#microphoneManager.setMicrophoneConfiguration;\n }\n\n #assertWebAudioSupport() {\n _console.assertWithError(AudioContext, \"WebAudio is not supported\");\n }\n\n get audioContext() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.audioContext;\n }\n set audioContext(newAudioContext) {\n this.#assertWebAudioSupport();\n this.#microphoneManager.audioContext = newAudioContext;\n }\n get microphoneMediaStreamDestination() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.mediaStreamDestination;\n }\n get microphoneGainNode() {\n this.#assertWebAudioSupport();\n return this.#microphoneManager.gainNode;\n }\n\n get isRecordingMicrophone() {\n return this.#microphoneManager.isRecording;\n }\n startRecordingMicrophone() {\n this.#microphoneManager.startRecording();\n }\n stopRecordingMicrophone() {\n this.#microphoneManager.stopRecording();\n }\n toggleMicrophoneRecording() {\n this.#microphoneManager.toggleRecording();\n }\n\n // DISPLAY\n #displayManager = new DisplayManager();\n\n get isDisplayAvailable() {\n return this.#displayManager.isAvailable;\n }\n get isDisplayReady() {\n return this.#displayManager.isReady;\n }\n get displayContextState() {\n return this.#displayManager.contextState;\n }\n get displayColors() {\n return this.#displayManager.colors;\n }\n get displayBitmapColors() {\n return this.#displayManager.bitmapColors;\n }\n get displayBitmapColorIndices() {\n return this.#displayManager.bitmapColorIndices;\n }\n get displayColorOpacities() {\n return this.#displayManager.opacities;\n }\n #assertDisplayIsAvailable() {\n _console.assertWithError(this.isDisplayAvailable, \"display not available\");\n }\n get displayStatus() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.displayStatus;\n }\n get displayBrightness() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.brightness;\n }\n get setDisplayBrightness() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBrightness;\n }\n\n get displayInformation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.displayInformation;\n }\n get numberOfDisplayColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.numberOfColors;\n }\n\n get wakeDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.wake;\n }\n get sleepDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.sleep;\n }\n get toggleDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.toggle;\n }\n get isDisplayAwake() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.isDisplayAwake;\n }\n\n get showDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.show;\n }\n get clearDisplay() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clear;\n }\n\n get setDisplayColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setColor;\n }\n get setDisplayColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setColorOpacity;\n }\n get setDisplayOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setOpacity;\n }\n\n get saveDisplayContext() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.saveContext;\n }\n get restoreDisplayContext() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.restoreContext;\n }\n\n get clearDisplayRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRect;\n }\n\n get selectDisplayBackgroundColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBackgroundColor;\n }\n get selectDisplayFillColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectFillColor;\n }\n get selectDisplayLineColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectLineColor;\n }\n get setDisplayIgnoreFill() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setIgnoreFill;\n }\n get setDisplayIgnoreLine() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setIgnoreLine;\n }\n get setDisplayFillBackground() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setFillBackground;\n }\n get setDisplayLineWidth() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setLineWidth;\n }\n get setDisplayRotation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotation;\n }\n get clearDisplayRotation() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRotation;\n }\n\n get setDisplaySegmentStartCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentStartCap;\n }\n get setDisplaySegmentEndCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentEndCap;\n }\n get setDisplaySegmentCap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentCap;\n }\n\n get setDisplaySegmentStartRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentStartRadius;\n }\n get setDisplaySegmentEndRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentEndRadius;\n }\n get setDisplaySegmentRadius() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSegmentRadius;\n }\n\n get setDisplayCropTop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropTop;\n }\n get setDisplayCropRight() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropRight;\n }\n get setDisplayCropBottom() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropBottom;\n }\n get setDisplayCropLeft() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCropLeft;\n }\n get setDisplayCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setCrop;\n }\n get clearDisplayCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearCrop;\n }\n\n get setDisplayRotationCropTop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropTop;\n }\n get setDisplayRotationCropRight() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropRight;\n }\n get setDisplayRotationCropBottom() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropBottom;\n }\n get setDisplayRotationCropLeft() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCropLeft;\n }\n get setDisplayRotationCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setRotationCrop;\n }\n get clearDisplayRotationCrop() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.clearRotationCrop;\n }\n get flushDisplayContextCommands() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.flushContextCommands;\n }\n\n get drawDisplayRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRect;\n }\n get drawDisplayCircle() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawCircle;\n }\n get drawDisplayEllipse() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawEllipse;\n }\n get drawDisplayRoundRect() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRoundRect;\n }\n get drawDisplayRegularPolygon() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawRegularPolygon;\n }\n get drawDisplayPolygon() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawPolygon;\n }\n get drawDisplayWireframe() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawWireframe;\n }\n get drawDisplaySegment() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSegment;\n }\n get drawDisplaySegments() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSegments;\n }\n get drawDisplayArc() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawArc;\n }\n get drawDisplayArcEllipse() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawArcEllipse;\n }\n get drawDisplayBitmap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawBitmap;\n }\n get imageToDisplayBitmap() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.imageToBitmap;\n }\n get quantizeDisplayImage() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.quantizeImage;\n }\n get resizeAndQuantizeDisplayImage() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resizeAndQuantizeImage;\n }\n\n get setDisplayContextState() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setContextState;\n }\n\n get selectDisplayBitmapColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBitmapColor;\n }\n get selectDisplayBitmapColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectBitmapColors;\n }\n get setDisplayBitmapColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapColor;\n }\n get setDisplayBitmapColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapColorOpacity;\n }\n\n get setDisplayBitmapScaleDirection() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleDirection;\n }\n get setDisplayBitmapScaleX() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleX;\n }\n get setDisplayBitmapScaleY() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScaleY;\n }\n get setDisplayBitmapScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setBitmapScale;\n }\n get resetDisplayBitmapScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetBitmapScale;\n }\n\n get selectDisplaySpriteColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteColor;\n }\n get selectDisplaySpriteColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteColors;\n }\n get setDisplaySpriteColor() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteColor;\n }\n get setDisplaySpriteColorOpacity() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteColorOpacity;\n }\n get resetDisplaySpriteColors() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetSpriteColors;\n }\n\n get setDisplaySpriteScaleDirection() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleDirection;\n }\n get setDisplaySpriteScaleX() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleX;\n }\n get setDisplaySpriteScaleY() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScaleY;\n }\n get setDisplaySpriteScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.setSpriteScale;\n }\n get resetDisplaySpriteScale() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.resetSpriteScale;\n }\n\n get displayManager() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager as DisplayManagerInterface;\n }\n\n get uploadDisplaySpriteSheet() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.uploadSpriteSheet;\n }\n get uploadDisplaySpriteSheets() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.uploadSpriteSheets;\n }\n get selectDisplaySpriteSheet() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.selectSpriteSheet;\n }\n get drawDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.drawSprite;\n }\n\n get startDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.startSprite;\n }\n get endDisplaySprite() {\n this.#assertDisplayIsAvailable();\n return this.#displayManager.endSprite;\n }\n\n get displaySpriteSheets() {\n return this.#displayManager.spriteSheets;\n }\n\n get serializeDisplaySpriteSheet() {\n return this.#displayManager.serializeSpriteSheet;\n }\n\n get setDisplayAlignment() {\n return this.#displayManager.setAlignment;\n }\n get setDisplayVerticalAlignment() {\n return this.#displayManager.setVerticalAlignment;\n }\n get setDisplayHorizontalAlignment() {\n return this.#displayManager.setHorizontalAlignment;\n }\n get resetDisplayAlignment() {\n return this.#displayManager.resetAlignment;\n }\n\n get setDisplaySpritesDirection() {\n return this.#displayManager.setSpritesDirection;\n }\n get setDisplaySpritesLineDirection() {\n return this.#displayManager.setSpritesLineDirection;\n }\n get setDisplaySpritesSpacing() {\n return this.#displayManager.setSpritesSpacing;\n }\n get setDisplaySpritesLineSpacing() {\n return this.#displayManager.setSpritesLineSpacing;\n }\n get setDisplaySpritesAlignment() {\n return this.#displayManager.setSpritesAlignment;\n }\n\n get drawDisplayQuadraticBezierCurve() {\n return this.#displayManager.drawQuadraticBezierCurve;\n }\n get drawDisplayQuadraticBezierCurves() {\n return this.#displayManager.drawQuadraticBezierCurves;\n }\n get drawDisplayCubicBezierCurve() {\n return this.#displayManager.drawCubicBezierCurve;\n }\n get drawDisplayCubicBezierCurves() {\n return this.#displayManager.drawCubicBezierCurves;\n }\n get drawDisplayPath() {\n return this.#displayManager.drawPath;\n }\n get drawDisplayClosedPath() {\n return this.#displayManager.drawClosedPath;\n }\n}\n\nexport default Device;\n","import { createConsole } from \"../utils/Console.ts\";\nimport CenterOfPressureHelper from \"../utils/CenterOfPressureHelper.ts\";\nimport {\n PressureData,\n PressureSensorPosition,\n PressureSensorValue,\n} from \"../sensor/PressureSensorDataManager.ts\";\nimport { CenterOfPressure } from \"../utils/CenterOfPressureHelper.ts\";\nimport { Side, Sides } from \"../InformationManager.ts\";\nimport { DeviceEventMap } from \"../Device.ts\";\nimport { RangeHelper } from \"../BS.ts\";\n\nconst _console = createConsole(\"DevicePairPressureSensorDataManager\", {\n log: false,\n});\n\nexport type DevicePairRawPressureData = { [side in Side]: PressureData };\n\nexport interface DevicePairPressureData {\n sensors: { [key in Side]: PressureSensorValue[] };\n scaledSum: number;\n normalizedSum: number;\n center?: CenterOfPressure;\n normalizedCenter?: CenterOfPressure;\n}\n\nexport interface DevicePairPressureDataEventMessage {\n pressure: DevicePairPressureData;\n}\n\nexport interface DevicePairPressureDataEventMessages {\n pressure: DevicePairPressureDataEventMessage;\n}\n\nclass DevicePairPressureSensorDataManager {\n #rawPressure: Partial<DevicePairRawPressureData> = {};\n\n #centerOfPressureHelper = new CenterOfPressureHelper();\n\n #normalizedSumRangeHelper = new RangeHelper();\n\n constructor() {\n this.resetPressureRange();\n }\n\n resetPressureRange() {\n this.#centerOfPressureHelper.reset();\n this.#normalizedSumRangeHelper.reset();\n }\n\n onDevicePressureData(event: DeviceEventMap[\"pressure\"]) {\n const { pressure } = event.message;\n const { side } = event.target;\n _console.log({ pressure, side });\n this.#rawPressure[side] = pressure;\n if (this.#hasAllPressureData) {\n return this.#updatePressureData();\n } else {\n _console.log(\"doesn't have all pressure data yet...\");\n }\n }\n\n get #hasAllPressureData() {\n return Sides.every((side) => side in this.#rawPressure);\n }\n\n #updatePressureData() {\n const pressure: DevicePairPressureData = {\n scaledSum: 0,\n normalizedSum: 0,\n sensors: { left: [], right: [] },\n };\n\n Sides.forEach((side) => {\n const sidePressure = this.#rawPressure[side]!;\n pressure.scaledSum += sidePressure.scaledSum;\n //pressure.normalizedSum += this.#rawPressure[side]!.normalizedSum;\n });\n pressure.normalizedSum +=\n this.#normalizedSumRangeHelper.updateAndGetNormalization(\n pressure.scaledSum,\n false\n );\n\n if (pressure.scaledSum > 0) {\n pressure.center = { x: 0, y: 0 };\n Sides.forEach((side) => {\n const sidePressure = this.#rawPressure[side]!;\n\n if (false) {\n const sidePressureWeight =\n sidePressure.scaledSum / pressure.scaledSum;\n if (sidePressureWeight > 0) {\n if (sidePressure.normalizedCenter?.y != undefined) {\n pressure.center!.y +=\n sidePressure.normalizedCenter!.y * sidePressureWeight;\n }\n if (side == \"right\") {\n pressure.center!.x = sidePressureWeight;\n }\n }\n } else {\n sidePressure.sensors.forEach((sensor) => {\n const _sensor: PressureSensorValue = structuredClone(sensor);\n _sensor.weightedValue = sensor.scaledValue / pressure.scaledSum;\n let { x, y } = sensor.position;\n x /= 2;\n if (side == \"right\") {\n x += 0.5;\n }\n _sensor.position = { x, y };\n pressure.center!.x += _sensor.position.x * _sensor.weightedValue;\n pressure.center!.y += _sensor.position.y * _sensor.weightedValue;\n pressure.sensors[side].push(_sensor);\n });\n }\n });\n\n pressure.normalizedCenter =\n this.#centerOfPressureHelper.updateAndGetNormalization(\n pressure.center,\n false\n );\n }\n\n _console.log({ devicePairPressure: pressure });\n\n return pressure;\n }\n}\n\nexport default DevicePairPressureSensorDataManager;\n","import DevicePairPressureSensorDataManager, {\n DevicePairPressureDataEventMessages,\n} from \"./DevicePairPressureSensorDataManager.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { Side } from \"../InformationManager.ts\";\nimport { SensorType } from \"../sensor/SensorDataManager.ts\";\nimport { DeviceEventMap } from \"../Device.ts\";\nimport EventDispatcher from \"../utils/EventDispatcher.ts\";\nimport DevicePair from \"./DevicePair.ts\";\nimport { AddKeysAsPropertyToInterface, ExtendInterfaceValues, ValueOf } from \"../utils/TypeScriptUtils.ts\";\n\nconst _console = createConsole(\"DevicePairSensorDataManager\", { log: false });\n\nexport const DevicePairSensorTypes = [\"pressure\", \"sensorData\"] as const;\nexport type DevicePairSensorType = (typeof DevicePairSensorTypes)[number];\n\nexport const DevicePairSensorDataEventTypes = DevicePairSensorTypes;\nexport type DevicePairSensorDataEventType = (typeof DevicePairSensorDataEventTypes)[number];\n\nexport type DevicePairSensorDataTimestamps = { [side in Side]: number };\n\ninterface BaseDevicePairSensorDataEventMessage {\n timestamps: DevicePairSensorDataTimestamps;\n}\n\ntype BaseDevicePairSensorDataEventMessages = DevicePairPressureDataEventMessages;\ntype _DevicePairSensorDataEventMessages = ExtendInterfaceValues<\n AddKeysAsPropertyToInterface<BaseDevicePairSensorDataEventMessages, \"sensorType\">,\n BaseDevicePairSensorDataEventMessage\n>;\n\nexport type DevicePairSensorDataEventMessage = ValueOf<_DevicePairSensorDataEventMessages>;\ninterface AnyDevicePairSensorDataEventMessages {\n sensorData: DevicePairSensorDataEventMessage;\n}\nexport type DevicePairSensorDataEventMessages = _DevicePairSensorDataEventMessages &\n AnyDevicePairSensorDataEventMessages;\n\nexport type DevicePairSensorDataEventDispatcher = EventDispatcher<\n DevicePair,\n DevicePairSensorDataEventType,\n DevicePairSensorDataEventMessages\n>;\n\nclass DevicePairSensorDataManager {\n eventDispatcher!: DevicePairSensorDataEventDispatcher;\n get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n\n #timestamps: { [sensorType in SensorType]?: Partial<DevicePairSensorDataTimestamps> } = {};\n\n pressureSensorDataManager = new DevicePairPressureSensorDataManager();\n resetPressureRange() {\n this.pressureSensorDataManager.resetPressureRange();\n }\n\n onDeviceSensorData(event: DeviceEventMap[\"sensorData\"]) {\n const { timestamp, sensorType } = event.message;\n\n _console.log({ sensorType, timestamp, event });\n\n if (!this.#timestamps[sensorType]) {\n this.#timestamps[sensorType] = {};\n }\n this.#timestamps[sensorType]![event.target.side] = timestamp;\n\n let value;\n switch (sensorType) {\n case \"pressure\":\n value = this.pressureSensorDataManager.onDevicePressureData(event as unknown as DeviceEventMap[\"pressure\"]);\n break;\n default:\n _console.log(`uncaught sensorType \"${sensorType}\"`);\n break;\n }\n\n if (value) {\n const timestamps = Object.assign({}, this.#timestamps[sensorType]) as DevicePairSensorDataTimestamps;\n // @ts-expect-error\n this.dispatchEvent(sensorType as DevicePairSensorDataEventType, { sensorType, timestamps, [sensorType]: value });\n // @ts-expect-error\n this.dispatchEvent(\"sensorData\", { sensorType, timestamps, [sensorType]: value });\n } else {\n _console.log(\"no value received\");\n }\n }\n}\n\nexport default DevicePairSensorDataManager;\n","import { createConsole } from \"../utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventListenerMap,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../utils/EventUtils.ts\";\nimport Device, {\n DeviceEvent,\n DeviceEventType,\n DeviceEventMessages,\n DeviceEventTypes,\n BoundDeviceEventListeners,\n DeviceEventMap,\n} from \"../Device.ts\";\nimport DevicePairSensorDataManager, {\n DevicePairSensorDataEventDispatcher,\n} from \"./DevicePairSensorDataManager.ts\";\nimport { capitalizeFirstCharacter } from \"../utils/stringUtils.ts\";\nimport { Side, Sides } from \"../InformationManager.ts\";\nimport { VibrationConfiguration } from \"../vibration/VibrationManager.ts\";\nimport { SensorConfiguration } from \"../sensor/SensorConfigurationManager.ts\";\nimport {\n DevicePairSensorDataEventMessages,\n DevicePairSensorDataEventTypes,\n} from \"./DevicePairSensorDataManager.ts\";\nimport {\n AddPrefixToInterfaceKeys,\n ExtendInterfaceValues,\n KeyOf,\n} from \"../utils/TypeScriptUtils.ts\";\nimport DeviceManager from \"../DeviceManager.ts\";\n\nconst _console = createConsole(\"DevicePair\", { log: false });\n\ninterface BaseDevicePairDeviceEventMessage {\n device: Device;\n side: Side;\n}\ntype DevicePairDeviceEventMessages = ExtendInterfaceValues<\n AddPrefixToInterfaceKeys<DeviceEventMessages, \"device\">,\n BaseDevicePairDeviceEventMessage\n>;\ntype DevicePairDeviceEventType = KeyOf<DevicePairDeviceEventMessages>;\nfunction getDevicePairDeviceEventType(deviceEventType: DeviceEventType) {\n return `device${capitalizeFirstCharacter(\n deviceEventType\n )}` as DevicePairDeviceEventType;\n}\nconst DevicePairDeviceEventTypes = DeviceEventTypes.map((eventType) =>\n getDevicePairDeviceEventType(eventType)\n) as DevicePairDeviceEventType[];\n\nexport const DevicePairConnectionEventTypes = [\"isConnected\"] as const;\nexport type DevicePairConnectionEventType =\n (typeof DevicePairConnectionEventTypes)[number];\n\nexport interface DevicePairConnectionEventMessages {\n isConnected: { isConnected: boolean };\n}\n\nexport const DevicePairEventTypes = [\n ...DevicePairConnectionEventTypes,\n ...DevicePairSensorDataEventTypes,\n ...DevicePairDeviceEventTypes,\n] as const;\nexport type DevicePairEventType = (typeof DevicePairEventTypes)[number];\n\nexport type DevicePairEventMessages = DevicePairConnectionEventMessages &\n DevicePairSensorDataEventMessages &\n DevicePairDeviceEventMessages;\n\nexport type DevicePairEventDispatcher = EventDispatcher<\n DevicePair,\n DevicePairEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEventMap = EventMap<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEventListenerMap = EventListenerMap<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type DevicePairEvent = Event<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\nexport type BoundDevicePairEventListeners = BoundEventListeners<\n DevicePair,\n DeviceEventType,\n DevicePairEventMessages\n>;\n\nexport const DevicePairTypes = [\"insoles\", \"gloves\"] as const;\nexport type DevicePairType = (typeof DevicePairTypes)[number];\n\nclass DevicePair {\n constructor(type: DevicePairType) {\n this.#type = type;\n this.#sensorDataManager.eventDispatcher = this\n .#eventDispatcher as DevicePairSensorDataEventDispatcher;\n }\n\n get sides() {\n return Sides;\n }\n\n #type: DevicePairType;\n get type() {\n return this.#type;\n }\n\n #eventDispatcher: DevicePairEventDispatcher = new EventDispatcher(\n this as DevicePair,\n DevicePairEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n get #dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n get removeEventListeners() {\n return this.#eventDispatcher.removeEventListeners;\n }\n get removeAllEventListeners() {\n return this.#eventDispatcher.removeAllEventListeners;\n }\n\n // SIDES\n #left?: Device;\n get left() {\n return this.#left;\n }\n\n #right?: Device;\n get right() {\n return this.#right;\n }\n\n get isConnected() {\n return Sides.every((side) => this[side]?.isConnected);\n }\n get isPartiallyConnected() {\n return Sides.some((side) => this[side]?.isConnected);\n }\n get isHalfConnected() {\n return this.isPartiallyConnected && !this.isConnected;\n }\n #assertIsConnected() {\n _console.assertWithError(this.isConnected, \"devicePair must be connected\");\n }\n\n #isDeviceCorrectType(device: Device) {\n switch (this.type) {\n case \"insoles\":\n return device.isInsole;\n case \"gloves\":\n return device.isGlove;\n }\n }\n\n assignDevice(device: Device) {\n if (!this.#isDeviceCorrectType(device)) {\n _console.log(\n `device is incorrect type ${device.type} for ${this.type} devicePair`\n );\n return;\n }\n const side = device.side;\n\n const currentDevice = this[side];\n\n if (device == currentDevice) {\n _console.log(\"device already assigned\");\n return;\n }\n\n if (currentDevice) {\n this.#removeDeviceEventListeners(currentDevice);\n }\n this.#addDeviceEventListeners(device);\n\n switch (side) {\n case \"left\":\n this.#left = device;\n break;\n case \"right\":\n this.#right = device;\n break;\n }\n\n _console.log(`assigned ${side} ${this.type} device`, device);\n\n this.resetPressureRange();\n\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n this.#dispatchEvent(\"deviceIsConnected\", {\n device,\n isConnected: device.isConnected,\n side,\n });\n\n return currentDevice;\n }\n\n #addDeviceEventListeners(device: Device) {\n addEventListeners(device, this.#boundDeviceEventListeners);\n DeviceEventTypes.forEach((deviceEventType) => {\n device.addEventListener(\n // @ts-expect-error\n deviceEventType,\n this.#redispatchDeviceEvent.bind(this)\n );\n });\n }\n #removeDeviceEventListeners(device: Device) {\n removeEventListeners(device, this.#boundDeviceEventListeners);\n DeviceEventTypes.forEach((deviceEventType) => {\n device.removeEventListener(\n // @ts-expect-error\n deviceEventType,\n this.#redispatchDeviceEvent.bind(this)\n );\n });\n }\n\n #removeDevice(device: Device) {\n const foundDevice = Sides.some((side) => {\n if (this[side] != device) {\n return false;\n }\n\n _console.log(`removing ${side} ${this.type} device`, device);\n removeEventListeners(device, this.#boundDeviceEventListeners);\n\n switch (side) {\n case \"left\":\n this.#left = undefined;\n break;\n case \"right\":\n this.#right = undefined;\n break;\n }\n\n return true;\n });\n if (foundDevice) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n return foundDevice;\n }\n\n #boundDeviceEventListeners: BoundDeviceEventListeners = {\n isConnected: this.#onDeviceIsConnected.bind(this),\n sensorData: this.#onDeviceSensorData.bind(this),\n getType: this.#onDeviceType.bind(this),\n };\n\n #redispatchDeviceEvent(deviceEvent: DeviceEvent) {\n const { type, target: device, message } = deviceEvent;\n this.#dispatchEvent(getDevicePairDeviceEventType(type), {\n ...message,\n device,\n side: device.side,\n });\n }\n\n #onDeviceIsConnected(deviceEvent: DeviceEventMap[\"isConnected\"]) {\n this.#dispatchEvent(\"isConnected\", { isConnected: this.isConnected });\n }\n\n #onDeviceType(deviceEvent: DeviceEventMap[\"getType\"]) {\n const { target: device } = deviceEvent;\n if (this[device.side] == device) {\n return;\n }\n const foundDevice = this.#removeDevice(device);\n if (!foundDevice) {\n return;\n }\n this.assignDevice(device);\n }\n\n // SENSOR CONFIGURATION\n async setSensorConfiguration(sensorConfiguration: SensorConfiguration) {\n for (let i = 0; i < Sides.length; i++) {\n const side = Sides[i];\n if (this[side]?.isConnected) {\n await this[side].setSensorConfiguration(sensorConfiguration);\n }\n }\n }\n\n // SENSOR DATA\n #sensorDataManager = new DevicePairSensorDataManager();\n #onDeviceSensorData(deviceEvent: DeviceEventMap[\"sensorData\"]) {\n if (this.isConnected) {\n this.#sensorDataManager.onDeviceSensorData(deviceEvent);\n }\n }\n resetPressureRange() {\n Sides.forEach((side) => this[side]?.resetPressureRange());\n this.#sensorDataManager.resetPressureRange();\n }\n\n // VIBRATION\n async triggerVibration(\n vibrationConfigurations: VibrationConfiguration[],\n sendImmediately?: boolean\n ) {\n const promises = Sides.map((side) => {\n return this[side]?.triggerVibration(\n vibrationConfigurations,\n sendImmediately\n );\n }).filter(Boolean);\n return Promise.allSettled(promises);\n }\n\n // SHARED INSTANCES\n static #insoles = new DevicePair(\"insoles\");\n static get insoles() {\n return this.#insoles;\n }\n static #gloves = new DevicePair(\"gloves\");\n static get gloves() {\n return this.#gloves;\n }\n static {\n DeviceManager.AddEventListener(\"deviceConnected\", (event) => {\n const { device } = event.message;\n if (device.isInsole) {\n this.#insoles.assignDevice(device);\n }\n if (device.isGlove) {\n this.#gloves.assignDevice(device);\n }\n });\n }\n}\n\nexport default DevicePair;\n","export function throttle<T extends (...args: any[]) => void>(\n fn: T,\n interval: number,\n trailing = false\n): (...args: Parameters<T>) => void {\n let lastTime = 0;\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let lastArgs: Parameters<T> | null = null;\n\n return function (...args: Parameters<T>) {\n const now = Date.now();\n const remaining = interval - (now - lastTime);\n\n if (remaining <= 0) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n lastTime = now;\n fn(...args);\n } else if (trailing) {\n lastArgs = args;\n if (!timeout) {\n timeout = setTimeout(() => {\n lastTime = Date.now();\n timeout = null;\n if (lastArgs) {\n fn(...lastArgs);\n lastArgs = null;\n }\n }, remaining);\n }\n }\n };\n}\n\nexport function debounce<T extends (...args: any[]) => void>(\n fn: T,\n interval: number,\n callImmediately = false\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return function (...args: Parameters<T>) {\n const callNow = callImmediately && !timeout;\n\n if (timeout) {\n clearTimeout(timeout);\n }\n\n timeout = setTimeout(() => {\n timeout = null;\n if (!callImmediately) {\n fn(...args);\n }\n }, interval);\n\n if (callNow) {\n fn(...args);\n }\n };\n}\n","import EventDispatcher, {\n BoundEventListeners,\n Event,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport { addEventListeners } from \"../utils/EventUtils.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { Timer } from \"../utils/Timer.ts\";\nimport { DeviceType } from \"../InformationManager.ts\";\nimport { ConnectionType } from \"../connection/BaseConnectionManager.ts\";\n\nconst _console = createConsole(\"BaseScanner\", { log: false });\n\nexport const ScannerEventTypes = [\n \"isScanningAvailable\",\n \"isScanning\",\n \"discoveredDevice\",\n \"expiredDiscoveredDevice\",\n \"scanningAvailable\",\n \"scanningNotAvailable\",\n \"scanning\",\n \"notScanning\",\n] as const;\nexport type ScannerEventType = (typeof ScannerEventTypes)[number];\n\nexport interface DiscoveredDevice {\n bluetoothId: string;\n name: string;\n deviceType: DeviceType;\n rssi: number;\n ipAddress?: string;\n isWifiSecure?: boolean;\n}\n\ninterface ScannerDiscoveredDeviceEventMessage {\n discoveredDevice: DiscoveredDevice;\n}\n\nexport interface ScannerEventMessages {\n discoveredDevice: ScannerDiscoveredDeviceEventMessage;\n expiredDiscoveredDevice: ScannerDiscoveredDeviceEventMessage;\n isScanningAvailable: { isScanningAvailable: boolean };\n isScanning: { isScanning: boolean };\n scanning: {};\n notScanning: {};\n scanningAvailable: {};\n scanningNotAvailable: {};\n}\n\nexport type ScannerEventDispatcher = EventDispatcher<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type ScannerEventMap = EventMap<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type ScannerEvent = Event<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\nexport type BoundScannerEventListeners = BoundEventListeners<\n BaseScanner,\n ScannerEventType,\n ScannerEventMessages\n>;\n\nexport type DiscoveredDevicesMap = { [deviceId: string]: DiscoveredDevice };\n\nabstract class BaseScanner {\n // IS SUPPORTED\n protected get baseConstructor() {\n return this.constructor as typeof BaseScanner;\n }\n static get isSupported() {\n return false;\n }\n get isSupported() {\n return this.baseConstructor.isSupported;\n }\n\n #assertIsSupported() {\n _console.assertWithError(\n this.isSupported,\n `${this.constructor.name} is not supported`\n );\n }\n\n // CONSTRUCTOR\n #assertIsSubclass() {\n _console.assertWithError(\n this.constructor != BaseScanner,\n `${this.constructor.name} must be subclassed`\n );\n }\n constructor() {\n this.#assertIsSubclass();\n this.#assertIsSupported();\n addEventListeners(this, this.#boundEventListeners);\n }\n\n #boundEventListeners: BoundScannerEventListeners = {\n discoveredDevice: this.#onDiscoveredDevice.bind(this),\n isScanning: this.#onIsScanning.bind(this),\n isScanningAvailable: this.#onIsScanningAvailable.bind(this),\n };\n\n // EVENT DISPATCHER\n #eventDispatcher: ScannerEventDispatcher = new EventDispatcher(\n this as BaseScanner,\n ScannerEventTypes\n );\n get addEventListener() {\n return this.#eventDispatcher.addEventListener;\n }\n protected get dispatchEvent() {\n return this.#eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.#eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.#eventDispatcher.waitForEvent;\n }\n\n // AVAILABILITY\n get isScanningAvailable() {\n return false;\n }\n #assertIsAvailable() {\n _console.assertWithError(this.isScanningAvailable, \"not available\");\n }\n\n // SCANNING\n get isScanning() {\n return false;\n }\n #assertIsScanning() {\n _console.assertWithError(this.isScanning, \"not scanning\");\n }\n #assertIsNotScanning() {\n _console.assertWithError(!this.isScanning, \"already scanning\");\n }\n\n startScan() {\n if (!this.isScanningAvailable) {\n _console.warn(\"scanning is not available\");\n return false;\n }\n if (this.isScanning) {\n _console.log(\"already scanning\");\n return false;\n }\n _console.log(\"startScan\");\n return true;\n // this.#assertIsAvailable();\n // this.#assertIsNotScanning();\n }\n stopScan() {\n if (!this.isScanning) {\n _console.log(\"already not scanning\");\n return false;\n }\n _console.log(\"stopScan\");\n return true;\n //this.#assertIsScanning();\n }\n #onIsScanning(event: ScannerEventMap[\"isScanning\"]) {\n if (this.isScanning) {\n this.#discoveredDevices = {};\n this.#discoveredDeviceTimestamps = {};\n } else {\n this.#checkDiscoveredDevicesExpirationTimer.stop();\n }\n\n if (this.isScanning) {\n this.#eventDispatcher.dispatchEvent(\"scanning\", {});\n } else {\n this.#eventDispatcher.dispatchEvent(\"notScanning\", {});\n }\n }\n #onIsScanningAvailable(event: ScannerEventMap[\"isScanningAvailable\"]) {\n if (this.isScanningAvailable) {\n this.#eventDispatcher.dispatchEvent(\"scanningAvailable\", {});\n } else {\n this.#eventDispatcher.dispatchEvent(\"scanningNotAvailable\", {});\n }\n }\n\n // DISCOVERED DEVICES\n #discoveredDevices: DiscoveredDevicesMap = {};\n get discoveredDevices(): Readonly<DiscoveredDevicesMap> {\n return this.#discoveredDevices;\n }\n get discoveredDevicesArray() {\n return Object.values(this.#discoveredDevices).sort((a, b) => {\n return (\n this.#discoveredDeviceTimestamps[a.bluetoothId] -\n this.#discoveredDeviceTimestamps[b.bluetoothId]\n );\n });\n }\n #assertValidDiscoveredDeviceId(discoveredDeviceId: string) {\n _console.assertWithError(\n this.#discoveredDevices[discoveredDeviceId],\n `no discovered device with id \"${discoveredDeviceId}\"`\n );\n }\n\n #onDiscoveredDevice(event: ScannerEventMap[\"discoveredDevice\"]) {\n const { discoveredDevice } = event.message;\n this.#discoveredDevices[discoveredDevice.bluetoothId] = discoveredDevice;\n this.#discoveredDeviceTimestamps[discoveredDevice.bluetoothId] = Date.now();\n this.#checkDiscoveredDevicesExpirationTimer.start();\n }\n\n #discoveredDeviceTimestamps: { [id: string]: number } = {};\n\n static #DiscoveredDeviceExpirationTimeout = 5000;\n static get DiscoveredDeviceExpirationTimeout() {\n return this.#DiscoveredDeviceExpirationTimeout;\n }\n get #discoveredDeviceExpirationTimeout() {\n return BaseScanner.DiscoveredDeviceExpirationTimeout;\n }\n #checkDiscoveredDevicesExpirationTimer = new Timer(\n this.#checkDiscoveredDevicesExpiration.bind(this),\n 1000\n );\n #checkDiscoveredDevicesExpiration() {\n const entries = Object.entries(this.#discoveredDevices);\n if (entries.length == 0) {\n this.#checkDiscoveredDevicesExpirationTimer.stop();\n return;\n }\n const now = Date.now();\n entries.forEach(([id, discoveredDevice]) => {\n const timestamp = this.#discoveredDeviceTimestamps[id];\n if (now - timestamp > this.#discoveredDeviceExpirationTimeout) {\n _console.log(\"discovered device timeout\");\n delete this.#discoveredDevices[id];\n delete this.#discoveredDeviceTimestamps[id];\n this.dispatchEvent(\"expiredDiscoveredDevice\", { discoveredDevice });\n }\n });\n }\n\n // DEVICE CONNECTION\n async connectToDevice(deviceId: string, connectionType?: ConnectionType) {\n this.#assertIsAvailable();\n }\n\n // RESET\n get canReset() {\n return false;\n }\n reset() {\n _console.log(\"resetting...\");\n }\n}\n\nexport default BaseScanner;\n","import { dataToArrayBuffer } from \"../../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport { isInNode } from \"../../utils/environment.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n BoundGenericEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n allServiceUUIDs,\n getServiceNameFromUUID,\n getCharacteristicNameFromUUID,\n allCharacteristicNames,\n getCharacteristicProperties,\n} from \"./bluetoothUUIDs.ts\";\nimport BluetoothConnectionManager from \"./BluetoothConnectionManager.ts\";\n\nconst _console = createConsole(\"NobleConnectionManager\", { log: false });\n\nlet filterUUIDs = true;\n\n/** NODE_START */\nimport type * as noble from \"@abandonware/noble\";\nimport os from \"os\";\nconst isLinux = os.platform() == \"linux\";\nfilterUUIDs = !isLinux;\n/** NODE_END */\n\nimport {\n BluetoothCharacteristicName,\n BluetoothServiceName,\n} from \"./bluetoothUUIDs.ts\";\nimport { ConnectionType } from \"../BaseConnectionManager.ts\";\nimport NobleScanner from \"../../scanner/NobleScanner.ts\";\n\ninterface HasConnectionManager {\n connectionManager: NobleConnectionManager | undefined;\n}\nexport interface NoblePeripheral\n extends noble.Peripheral,\n HasConnectionManager {\n scanner: NobleScanner;\n shouldConnect?: boolean;\n}\ninterface NobleService extends noble.Service, HasConnectionManager {\n name: BluetoothServiceName;\n}\ninterface NobleCharacteristic\n extends noble.Characteristic,\n HasConnectionManager {\n name: BluetoothCharacteristicName;\n}\n\nclass NobleConnectionManager extends BluetoothConnectionManager {\n get bluetoothId() {\n return this.#noblePeripheral!.id;\n }\n\n get canUpdateFirmware() {\n return this.#characteristics.has(\"smp\");\n }\n\n static get isSupported() {\n return isInNode;\n }\n static get type(): ConnectionType {\n return \"noble\";\n }\n\n get isConnected() {\n return this.#noblePeripheral?.state == \"connected\";\n }\n\n async connect() {\n const canConnect = await super.connect();\n _console.log({ canConnect });\n if (!canConnect) {\n return false;\n }\n if (isLinux) {\n _console.log(\"setting noblePeripheral.shouldConnect\");\n this.#noblePeripheral!.shouldConnect = true;\n } else {\n _console.log(\"noblePeripheral.connectAsync\");\n await this.#noblePeripheral!.connectAsync();\n _console.log(\"noblePeripheral.connectAsync done\");\n }\n return true;\n }\n async disconnect() {\n const canContinue = await super.disconnect();\n if (!canContinue) {\n return false;\n }\n _console.log(\"noblePeripheral.disconnectAsync\");\n await this.#noblePeripheral!.disconnectAsync();\n _console.log(\"noblePeripheral.disconnectAsync done\");\n return true;\n }\n\n async writeCharacteristic(\n characteristicName: BluetoothCharacteristicName,\n data: ArrayBuffer\n ) {\n const characteristic = this.#characteristics.get(characteristicName)!;\n _console.assertWithError(\n characteristic,\n `no characteristic found with name \"${characteristicName}\"`\n );\n // if (data instanceof DataView) {\n // data = data.buffer;\n // }\n const properties = getCharacteristicProperties(characteristicName);\n const buffer = Buffer.from(data);\n const writeWithoutResponse = properties.writeWithoutResponse;\n _console.log(\n `writing to ${characteristicName} ${\n writeWithoutResponse ? \"without\" : \"with\"\n } response`,\n buffer\n );\n await characteristic.writeAsync(buffer, writeWithoutResponse);\n if (characteristic.properties.includes(\"read\")) {\n await characteristic.readAsync();\n }\n }\n\n get canReconnect() {\n return this.#noblePeripheral!.connectable;\n }\n async reconnect() {\n let canContinue = await super.reconnect();\n if (!canContinue) {\n return false;\n }\n await this.#noblePeripheral!.connectAsync();\n return true;\n }\n\n // NOBLE\n #noblePeripheral?: NoblePeripheral;\n get noblePeripheral() {\n return this.#noblePeripheral;\n }\n set noblePeripheral(newNoblePeripheral) {\n if (newNoblePeripheral) {\n _console.assertTypeWithError(newNoblePeripheral, \"object\");\n }\n if (this.noblePeripheral == newNoblePeripheral) {\n _console.log(\"attempted to assign duplicate noblePeripheral\");\n return;\n }\n\n _console.log(\"newNoblePeripheral\", newNoblePeripheral?.id);\n\n if (this.#noblePeripheral) {\n removeEventListeners(\n this.#noblePeripheral,\n this.#unboundNoblePeripheralListeners\n );\n delete this.#noblePeripheral!.connectionManager;\n }\n\n if (newNoblePeripheral) {\n newNoblePeripheral.connectionManager = this;\n addEventListeners(\n newNoblePeripheral,\n this.#unboundNoblePeripheralListeners\n );\n }\n\n this.#noblePeripheral = newNoblePeripheral;\n }\n\n // NOBLE EVENTLISTENERS\n #unboundNoblePeripheralListeners: BoundGenericEventListeners = {\n connect: this.#onNoblePeripheralConnect,\n disconnect: this.#onNoblePeripheralDisconnect,\n rssiUpdate: this.#onNoblePeripheralRssiUpdate,\n servicesDiscover: this.#onNoblePeripheralServicesDiscover,\n };\n\n async #onNoblePeripheralConnect(this: NoblePeripheral) {\n //_console.log(\"#onNoblePeripheralConnect\",this.id);\n await this.connectionManager!.onNoblePeripheralConnect(this);\n }\n async onNoblePeripheralConnect(noblePeripheral: NoblePeripheral) {\n _console.log(\n \"onNoblePeripheralConnect\",\n noblePeripheral.id,\n noblePeripheral.state\n );\n if (noblePeripheral.state == \"connected\") {\n _console.log(\"discoverServicesAsync\", noblePeripheral.id, {\n allServiceUUIDs,\n });\n // services don't show up if on ubuntu if serviceUUIDs are supplied\n if (filterUUIDs) {\n await this.#noblePeripheral!.discoverServicesAsync(\n allServiceUUIDs as string[]\n );\n } else {\n await this.#noblePeripheral!.discoverServicesAsync();\n }\n }\n // this gets called when it connects and disconnects, so we use the noblePeripheral's \"state\" property instead\n await this.#onNoblePeripheralState();\n }\n\n async #onNoblePeripheralDisconnect(this: NoblePeripheral) {\n //_console.log(\"#onNoblePeripheralDisconnect\", this.id);\n await this.connectionManager!.onNoblePeripheralConnect(this);\n }\n async onNoblePeripheralDisconnect(noblePeripheral: NoblePeripheral) {\n _console.log(\"onNoblePeripheralDisconnect\", noblePeripheral.id);\n await this.#onNoblePeripheralState();\n }\n\n async #onNoblePeripheralState() {\n _console.log(\n `noblePeripheral ${this.bluetoothId} state ${\n this.#noblePeripheral!.state\n }`\n );\n\n switch (this.#noblePeripheral!.state) {\n case \"connected\":\n //this.status = \"connected\";\n break;\n case \"connecting\":\n //this.status = \"connecting\";\n break;\n case \"disconnected\":\n this.#removeEventListeners();\n this.status = \"notConnected\";\n break;\n case \"disconnecting\":\n this.status = \"disconnecting\";\n break;\n case \"error\":\n _console.error(\"noblePeripheral error\");\n break;\n default:\n _console.log(\n `uncaught noblePeripheral state ${this.#noblePeripheral!.state}`\n );\n break;\n }\n }\n\n #removeEventListeners() {\n _console.log(\"removing noblePeripheral eventListeners\");\n this.#services.forEach((service) => {\n removeEventListeners(service, this.#unboundNobleServiceListeners);\n });\n this.#services.clear();\n\n this.#characteristics.forEach((characteristic) => {\n _console.log(\n `removing listeners from characteristic \"${characteristic.name}\" has ${characteristic.listeners.length} listeners`\n );\n removeEventListeners(\n characteristic,\n this.#unboundNobleCharacteristicListeners\n );\n });\n this.#characteristics.clear();\n }\n\n async #onNoblePeripheralRssiUpdate(this: NoblePeripheral, rssi: number) {\n await this.connectionManager!.onNoblePeripheralRssiUpdate(this, rssi);\n }\n async onNoblePeripheralRssiUpdate(\n noblePeripheral: NoblePeripheral,\n rssi: number\n ) {\n _console.log(\"onNoblePeripheralRssiUpdate\", noblePeripheral.id, rssi);\n // TODO: - can this be useful?\n }\n\n async #onNoblePeripheralServicesDiscover(\n this: NoblePeripheral,\n services: NobleService[]\n ) {\n await this.connectionManager!.onNoblePeripheralServicesDiscover(\n this,\n services\n );\n }\n async onNoblePeripheralServicesDiscover(\n noblePeripheral: NoblePeripheral,\n services: NobleService[]\n ) {\n _console.log(\n \"onNoblePeripheralServicesDiscover\",\n noblePeripheral.id,\n services.map((service) => service.uuid)\n );\n for (const index in services) {\n const service = services[index];\n _console.log(\"service\", service.uuid);\n if (service.uuid == \"1800\") {\n _console.log(\"skipping 1800\");\n continue;\n }\n if (service.uuid == \"1801\") {\n _console.log(\"skipping 1801\");\n continue;\n }\n const serviceName = getServiceNameFromUUID(service.uuid)!;\n _console.assertWithError(\n serviceName,\n `no name found for service uuid \"${service.uuid}\"`\n );\n _console.log({ serviceName });\n this.#services.set(serviceName, service);\n service.name = serviceName;\n service.connectionManager = this;\n addEventListeners(service, this.#unboundNobleServiceListeners);\n await service.discoverCharacteristicsAsync();\n }\n }\n\n // NOBLE SERVICE\n #services: Map<BluetoothServiceName, NobleService> = new Map();\n\n #unboundNobleServiceListeners = {\n characteristicsDiscover: this.#onNobleServiceCharacteristicsDiscover,\n };\n\n async #onNobleServiceCharacteristicsDiscover(\n this: NobleService,\n characteristics: NobleCharacteristic[]\n ) {\n await this.connectionManager!.onNobleServiceCharacteristicsDiscover(\n this,\n characteristics\n );\n }\n async onNobleServiceCharacteristicsDiscover(\n service: NobleService,\n characteristics: NobleCharacteristic[]\n ) {\n _console.log(\n \"onNobleServiceCharacteristicsDiscover\",\n service.uuid,\n characteristics.map((characteristic) => characteristic.uuid)\n );\n\n for (const index in characteristics) {\n const characteristic = characteristics[index];\n _console.log(\"characteristic\", characteristic.uuid);\n const characteristicName = getCharacteristicNameFromUUID(\n characteristic.uuid\n )!;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic uuid \"${characteristic.uuid}\"`\n );\n _console.log({ characteristicName });\n this.#characteristics.set(characteristicName, characteristic);\n characteristic.name = characteristicName;\n characteristic.connectionManager = this;\n _console.log(\n `adding listeners to characteristic \"${characteristic.name}\" (currently has ${characteristic.listeners.length} listeners)`\n );\n addEventListeners(\n characteristic,\n this.#unboundNobleCharacteristicListeners\n );\n if (characteristic.properties.includes(\"read\")) {\n await characteristic.readAsync();\n }\n if (characteristic.properties.includes(\"notify\")) {\n await characteristic.subscribeAsync();\n }\n }\n\n if (this.#hasAllCharacteristics) {\n this.status = \"connected\";\n }\n }\n\n // NOBLE CHARACTERISRTIC\n #unboundNobleCharacteristicListeners = {\n data: this.#onNobleCharacteristicData,\n write: this.#onNobleCharacteristicWrite,\n notify: this.#onNobleCharacteristicNotify,\n };\n\n #characteristics: Map<BluetoothCharacteristicName, NobleCharacteristic> =\n new Map();\n\n get #hasAllCharacteristics() {\n return allCharacteristicNames.every((characteristicName) => {\n if (characteristicName == \"smp\") {\n return true;\n }\n return this.#characteristics.has(characteristicName);\n });\n }\n\n #onNobleCharacteristicData(\n this: NobleCharacteristic,\n data: Buffer,\n isNotification: boolean\n ) {\n this.connectionManager!.onNobleCharacteristicData(\n this,\n data,\n isNotification\n );\n }\n onNobleCharacteristicData(\n characteristic: NobleCharacteristic,\n data: Buffer,\n isNotification: boolean\n ) {\n _console.log(\n \"onNobleCharacteristicData\",\n characteristic.uuid,\n data,\n isNotification\n );\n const dataView = new DataView(dataToArrayBuffer(data));\n\n const characteristicName: BluetoothCharacteristicName = characteristic.name;\n _console.assertWithError(\n Boolean(characteristicName),\n `no name found for characteristic with uuid \"${characteristic.uuid}\"`\n );\n\n this.onCharacteristicValueChanged(characteristicName, dataView);\n }\n\n #onNobleCharacteristicWrite(this: NobleCharacteristic) {\n this.connectionManager!.onNobleCharacteristicWrite(this);\n }\n onNobleCharacteristicWrite(characteristic: NobleCharacteristic) {\n _console.log(\"onNobleCharacteristicWrite\", characteristic.uuid);\n // TODO: - can this be useful?\n }\n\n #onNobleCharacteristicNotify(\n this: NobleCharacteristic,\n isSubscribed: boolean\n ) {\n this.connectionManager!.onNobleCharacteristicNotify(this, isSubscribed);\n }\n onNobleCharacteristicNotify(\n characteristic: NobleCharacteristic,\n isSubscribed: boolean\n ) {\n _console.log(\n \"onNobleCharacteristicNotify\",\n characteristic.uuid,\n isSubscribed\n );\n }\n\n remove() {\n super.remove();\n this.noblePeripheral = undefined;\n }\n}\n\nexport default NobleConnectionManager;\n","import BaseScanner, {\n DiscoveredDevice,\n ScannerEventMap,\n} from \"./BaseScanner.ts\";\nimport { createConsole } from \"../utils/Console.ts\";\nimport { addEventListeners } from \"../utils/EventUtils.ts\";\nimport {\n serviceDataUUID,\n serviceUUIDs,\n} from \"../connection/bluetooth/bluetoothUUIDs.ts\";\nimport Device from \"../Device.ts\";\nimport NobleConnectionManager, {\n NoblePeripheral,\n} from \"../connection/bluetooth/NobleConnectionManager.ts\";\n\nconst _console = createConsole(\"NobleScanner\", { log: false });\n\nlet isSupported = false;\nlet filterManually = true;\nconst filterServiceUuid = (serviceUUIDs[0] as string).replaceAll(\"-\", \"\");\n\nlet isLinux = false;\n/** NODE_START */\nimport noble from \"@abandonware/noble\";\nimport { DeviceTypes } from \"../InformationManager.ts\";\nimport DeviceManager from \"../DeviceManager.ts\";\nimport { ClientConnectionType } from \"../connection/BaseConnectionManager.ts\";\nisSupported = true;\nimport os from \"os\";\nconst platform = os.platform();\nisLinux = platform == \"linux\";\nfilterManually = isLinux;\n_console.log({ platform, filterManually, filterServiceUuid });\n/** NODE_END */\n\nexport const NobleStates = [\n \"unknown\",\n \"resetting\",\n \"unsupported\",\n \"unauthorized\",\n \"poweredOff\",\n \"poweredOn\",\n] as const;\nexport type NobleState = (typeof NobleStates)[number];\n\nclass NobleScanner extends BaseScanner {\n static get isSupported() {\n return isSupported;\n }\n\n // SCANNING\n #_isScanning = false;\n get #isScanning() {\n return this.#_isScanning;\n }\n set #isScanning(newIsScanning) {\n _console.assertTypeWithError(newIsScanning, \"boolean\");\n if (this.isScanning == newIsScanning) {\n _console.log(\"duplicate isScanning assignment\");\n return;\n }\n this.#_isScanning = newIsScanning;\n this.dispatchEvent(\"isScanning\", { isScanning: this.isScanning });\n }\n get isScanning() {\n return this.#isScanning;\n }\n\n // NOBLE STATE\n #_nobleState: NobleState = \"unknown\";\n get #nobleState() {\n return this.#_nobleState;\n }\n set #nobleState(newNobleState) {\n _console.assertTypeWithError(newNobleState, \"string\");\n if (this.#nobleState == newNobleState) {\n _console.log(\"duplicate nobleState assignment\");\n return;\n }\n this.#_nobleState = newNobleState;\n _console.log({ newNobleState });\n this.dispatchEvent(\"isScanningAvailable\", {\n isScanningAvailable: this.isScanningAvailable,\n });\n }\n\n // NOBLE LISTENERS\n #boundNobleListeners = {\n scanStart: this.#onNobleScanStart.bind(this),\n scanStop: this.#onNobleScanStop.bind(this),\n stateChange: this.#onNobleStateChange.bind(this),\n discover: this.#onNobleDiscover.bind(this),\n };\n #onNobleScanStart() {\n _console.log(\"OnNobleScanStart\");\n this.#isScanning = true;\n }\n #onNobleScanStop() {\n _console.log(\"OnNobleScanStop\");\n this.#isScanning = false;\n }\n #onNobleStateChange(state: NobleState) {\n _console.log(\"onNobleStateChange\", state);\n this.#nobleState = state;\n }\n #isBusy = false;\n async #onNobleDiscover(noblePeripheral: NoblePeripheral) {\n _console.log(\"advertisement\", noblePeripheral.advertisement);\n if (filterManually) {\n const serviceUuid = noblePeripheral.advertisement.serviceUuids?.[0];\n _console.log(\"onNobleDiscover.filterManually\", { serviceUuid });\n if (serviceUuid != filterServiceUuid) {\n return;\n }\n }\n\n _console.log(\"onNobleDiscover\", noblePeripheral.id);\n if (!this.#noblePeripherals[noblePeripheral.id]) {\n noblePeripheral.scanner = this;\n this.#noblePeripherals[noblePeripheral.id] = noblePeripheral;\n } else {\n const _noblePeripheral = this.#noblePeripherals[noblePeripheral.id];\n if (\n isLinux &&\n _noblePeripheral.shouldConnect &&\n !this.#isBusy &&\n _noblePeripheral.state == \"disconnected\"\n ) {\n this.#isBusy = true;\n _noblePeripheral.shouldConnect = false;\n _console.log(\"noblePeripheral.connectAsync\");\n await _noblePeripheral.connectAsync();\n _console.log(\"noblePeripheral.connectAsync done\");\n this.#isBusy = false;\n }\n }\n\n _console.log(\"advertisement\", noblePeripheral.advertisement);\n\n let deviceType;\n let ipAddress;\n let isWifiSecure;\n const { manufacturerData, serviceData } = noblePeripheral.advertisement;\n if (manufacturerData) {\n _console.log(\"manufacturerData\", manufacturerData);\n if (manufacturerData.byteLength >= 3) {\n const deviceTypeEnum = manufacturerData.readUint8(2);\n deviceType = DeviceTypes[deviceTypeEnum];\n _console;\n }\n if (manufacturerData.byteLength >= 3 + 4) {\n ipAddress = new Uint8Array(\n manufacturerData.buffer.slice(3, 3 + 4)\n ).join(\".\");\n _console.log({ ipAddress });\n }\n if (manufacturerData.byteLength >= 3 + 4 + 1) {\n isWifiSecure = manufacturerData.readUint8(3 + 4) != 0;\n _console.log({ isWifiSecure });\n }\n }\n if (serviceData) {\n _console.log(\"serviceData\", serviceData);\n const deviceTypeServiceData = serviceData.find((serviceDatum) => {\n return serviceDatum.uuid == serviceDataUUID;\n });\n _console.log(\"deviceTypeServiceData\", deviceTypeServiceData);\n if (deviceTypeServiceData) {\n const deviceTypeEnum = deviceTypeServiceData.data.readUint8(0);\n deviceType = DeviceTypes[deviceTypeEnum];\n }\n }\n if (deviceType == undefined) {\n _console.log(\"skipping device - no deviceType\");\n return;\n }\n\n const discoveredDevice: DiscoveredDevice = {\n name: noblePeripheral.advertisement.localName,\n bluetoothId: noblePeripheral.id,\n deviceType,\n rssi: noblePeripheral.rssi,\n ipAddress,\n isWifiSecure,\n };\n this.dispatchEvent(\"discoveredDevice\", { discoveredDevice });\n }\n\n // CONSTRUCTOR\n constructor() {\n super();\n addEventListeners(noble, this.#boundNobleListeners);\n addEventListeners(this, this.#boundBaseScannerListeners);\n }\n\n // AVAILABILITY\n get isScanningAvailable() {\n return this.#nobleState == \"poweredOn\";\n }\n\n // SCANNING\n startScan() {\n if (!super.startScan()) {\n return false;\n }\n _console.log(\"noble.startScan\");\n noble.startScanningAsync(\n filterManually ? [] : (serviceUUIDs as string[]),\n true\n );\n return true;\n }\n stopScan() {\n if (!super.stopScan()) {\n return false;\n }\n _console.log(\"noble.stopScan\");\n noble.stopScanningAsync();\n return true;\n }\n\n // RESET\n get canReset() {\n return true;\n }\n reset() {\n super.reset();\n noble.reset();\n }\n\n // BASESCANNER LISTENERS\n #boundBaseScannerListeners = {\n expiredDiscoveredDevice: this.#onExpiredDiscoveredDevice.bind(this),\n };\n\n #onExpiredDiscoveredDevice(\n event: ScannerEventMap[\"expiredDiscoveredDevice\"]\n ) {\n const { discoveredDevice } = event.message;\n const noblePeripheral =\n this.#noblePeripherals[discoveredDevice.bluetoothId];\n if (noblePeripheral) {\n // disconnect?\n delete this.#noblePeripherals[discoveredDevice.bluetoothId];\n }\n }\n\n // DISCOVERED DEVICES\n #noblePeripherals: { [bluetoothId: string]: NoblePeripheral } = {};\n #assertValidNoblePeripheralId(noblePeripheralId: string) {\n _console.assertTypeWithError(noblePeripheralId, \"string\");\n _console.assertWithError(\n this.#noblePeripherals[noblePeripheralId],\n `no noblePeripheral found with id \"${noblePeripheralId}\"`\n );\n }\n\n // DEVICES\n #devices: { [bluetoothId: string]: Device } = {};\n async connectToDevice(\n deviceId: string,\n connectionType?: ClientConnectionType\n ) {\n super.connectToDevice(deviceId, connectionType);\n this.#assertValidNoblePeripheralId(deviceId);\n const noblePeripheral = this.#noblePeripherals[deviceId];\n _console.log(\"connecting to discoveredDevice...\", deviceId);\n\n let device = DeviceManager.AvailableDevices.filter(\n (device) => device.connectionType == \"noble\"\n ).find((device) => device.bluetoothId == deviceId);\n device = device ?? this.#devices[deviceId];\n\n if (!device) {\n _console.log(\"creating device for discoveredDevice...\", deviceId);\n device = this.#createDevice(noblePeripheral);\n this.#devices[deviceId] = device;\n const { ipAddress, isWifiSecure } =\n this.discoveredDevices[device.bluetoothId!];\n if (connectionType && connectionType != \"noble\" && ipAddress) {\n await device.connect({ type: connectionType, ipAddress, isWifiSecure });\n } else {\n await device.connect();\n }\n } else {\n const { ipAddress, isWifiSecure } =\n this.discoveredDevices[device.bluetoothId!];\n if (\n connectionType &&\n connectionType != \"noble\" &&\n connectionType != device.connectionType &&\n ipAddress\n ) {\n await device.connect({ type: connectionType, ipAddress, isWifiSecure });\n } else {\n await device.reconnect();\n }\n }\n }\n\n #createDevice(noblePeripheral: NoblePeripheral) {\n const device = new Device();\n const nobleConnectionManager = new NobleConnectionManager();\n nobleConnectionManager.noblePeripheral = noblePeripheral;\n device.connectionManager = nobleConnectionManager;\n return device;\n }\n}\n\nexport default NobleScanner;\n","import { createConsole } from \"../utils/Console.ts\";\nimport NobleScanner from \"./NobleScanner.ts\";\nimport BaseScanner from \"./BaseScanner.ts\";\n\nconst _console = createConsole(\"Scanner\", { log: false });\n\nlet scanner: BaseScanner | undefined;\n\nif (NobleScanner.isSupported) {\n _console.log(\"using NobleScanner\");\n scanner = new NobleScanner() as BaseScanner;\n} else {\n _console.log(\"Scanner not available\");\n}\n\nexport default scanner;\n","import { createConsole } from \"../utils/Console.ts\";\nimport EventDispatcher, {\n BoundEventListeners,\n Event,\n EventMap,\n} from \"../utils/EventDispatcher.ts\";\nimport {\n createServerMessage,\n ServerMessageTypes,\n DeviceMessage,\n ServerMessage,\n ServerMessageType,\n createDeviceMessage,\n} from \"./ServerUtils.ts\";\nimport Device, {\n BoundDeviceEventListeners,\n DeviceEventMap,\n DeviceEventType,\n RequiredInformationConnectionMessages,\n} from \"../Device.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../utils/EventUtils.ts\";\nimport scanner from \"../scanner/Scanner.ts\";\nimport { parseMessage, parseStringFromDataView } from \"../utils/ParseUtils.ts\";\nimport {\n ConnectionMessageType,\n ConnectionMessageTypes,\n ConnectionType,\n ConnectionTypes,\n} from \"../connection/BaseConnectionManager.ts\";\nimport {\n BoundScannerEventListeners,\n DiscoveredDevice,\n ScannerEventMap,\n} from \"../scanner/BaseScanner.ts\";\nimport { concatenateArrayBuffers } from \"../utils/ArrayBufferUtils.ts\";\nimport DeviceManager, {\n DeviceManagerEventMap,\n BoundDeviceManagerEventListeners,\n} from \"../DeviceManager.ts\";\nimport { RequiredWifiMessageTypes } from \"../WifiManager.ts\";\nimport { DeviceInformationTypes } from \"../DeviceInformationManager.ts\";\n\nconst RequiredDeviceInformationMessageTypes: ConnectionMessageType[] = [\n ...DeviceInformationTypes,\n \"batteryLevel\",\n ...RequiredInformationConnectionMessages,\n];\n\nconst _console = createConsole(\"BaseServer\", { log: false });\n\nexport const ServerEventTypes = [\n \"clientConnected\",\n \"clientDisconnected\",\n] as const;\nexport type ServerEventType = (typeof ServerEventTypes)[number];\n\ninterface ServerEventMessages {\n clientConnected: { client: any };\n clientDisconnected: { client: any };\n}\n\nexport type ServerEventDispatcher = EventDispatcher<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type ServerEvent = Event<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type ServerEventMap = EventMap<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\nexport type BoundServerEventListeners = BoundEventListeners<\n BaseServer,\n ServerEventType,\n ServerEventMessages\n>;\n\nabstract class BaseServer {\n // EVENT DISPATCHER\n protected eventDispatcher: ServerEventDispatcher = new EventDispatcher(\n this as BaseServer,\n ServerEventTypes\n );\n get addEventListener() {\n return this.eventDispatcher.addEventListener;\n }\n protected get dispatchEvent() {\n return this.eventDispatcher.dispatchEvent;\n }\n get removeEventListener() {\n return this.eventDispatcher.removeEventListener;\n }\n get waitForEvent() {\n return this.eventDispatcher.waitForEvent;\n }\n\n // CONSTRUCTOR\n\n constructor() {\n _console.assertWithError(scanner, \"no scanner defined\");\n\n addEventListeners(scanner, this.#boundScannerListeners);\n addEventListeners(DeviceManager, this.#boundDeviceManagerListeners);\n addEventListeners(this, this.#boundServerListeners);\n }\n\n get numberOfClients() {\n return 0;\n }\n\n static #ClearSensorConfigurationsWhenNoClients = true;\n static get ClearSensorConfigurationsWhenNoClients() {\n return this.#ClearSensorConfigurationsWhenNoClients;\n }\n static set ClearSensorConfigurationsWhenNoClients(newValue) {\n _console.assertTypeWithError(newValue, \"boolean\");\n this.#ClearSensorConfigurationsWhenNoClients = newValue;\n }\n\n #clearSensorConfigurationsWhenNoClients =\n BaseServer.#ClearSensorConfigurationsWhenNoClients;\n get clearSensorConfigurationsWhenNoClients() {\n return this.#clearSensorConfigurationsWhenNoClients;\n }\n set clearSensorConfigurationsWhenNoClients(newValue) {\n _console.assertTypeWithError(newValue, \"boolean\");\n this.#clearSensorConfigurationsWhenNoClients = newValue;\n }\n\n // SERVER LISTENERS\n #boundServerListeners: BoundServerEventListeners = {\n clientConnected: this.#onClientConnected.bind(this),\n clientDisconnected: this.#onClientDisconnected.bind(this),\n };\n #onClientConnected(event: ServerEventMap[\"clientConnected\"]) {\n const client = event.message.client;\n _console.log(\"onClientConnected\");\n }\n #onClientDisconnected(event: ServerEventMap[\"clientDisconnected\"]) {\n const client = event.message.client;\n _console.log(\"onClientDisconnected\");\n if (\n this.numberOfClients == 0 &&\n this.clearSensorConfigurationsWhenNoClients\n ) {\n DeviceManager.ConnectedDevices.forEach((device) => {\n device.clearSensorConfiguration();\n device.setTfliteInferencingEnabled(false);\n });\n }\n }\n\n // CLIENT MESSAGING\n broadcastMessage(message: ArrayBuffer) {\n _console.log(\"broadcasting\", message);\n }\n\n // SCANNER\n #boundScannerListeners: BoundScannerEventListeners = {\n isScanningAvailable: this.#onScannerIsAvailable.bind(this),\n isScanning: this.#onScannerIsScanning.bind(this),\n discoveredDevice: this.#onScannerDiscoveredDevice.bind(this),\n expiredDiscoveredDevice: this.#onExpiredDiscoveredDevice.bind(this),\n };\n\n #onScannerIsAvailable(event: ScannerEventMap[\"isScanningAvailable\"]) {\n this.broadcastMessage(this.#isScanningAvailableMessage);\n }\n get #isScanningAvailableMessage() {\n return createServerMessage({\n type: \"isScanningAvailable\",\n data: scanner!.isScanningAvailable,\n });\n }\n\n #onScannerIsScanning(event: ScannerEventMap[\"isScanning\"]) {\n this.broadcastMessage(this.#isScanningMessage);\n }\n get #isScanningMessage() {\n return createServerMessage({\n type: \"isScanning\",\n data: scanner!.isScanning,\n });\n }\n\n #onScannerDiscoveredDevice(event: ScannerEventMap[\"discoveredDevice\"]) {\n const { discoveredDevice } = event.message;\n _console.log(discoveredDevice);\n\n this.broadcastMessage(\n this.#createDiscoveredDeviceMessage(discoveredDevice)\n );\n }\n #createDiscoveredDeviceMessage(discoveredDevice: DiscoveredDevice) {\n return createServerMessage({\n type: \"discoveredDevice\",\n data: discoveredDevice,\n });\n }\n\n #onExpiredDiscoveredDevice(\n event: ScannerEventMap[\"expiredDiscoveredDevice\"]\n ) {\n const { discoveredDevice } = event.message;\n _console.log(\"expired\", discoveredDevice);\n this.broadcastMessage(\n this.#createExpiredDiscoveredDeviceMessage(discoveredDevice)\n );\n }\n #createExpiredDiscoveredDeviceMessage(discoveredDevice: DiscoveredDevice) {\n return createServerMessage({\n type: \"expiredDiscoveredDevice\",\n data: discoveredDevice.bluetoothId,\n });\n }\n\n get #discoveredDevicesMessage() {\n const serverMessages: ServerMessage[] = scanner!.discoveredDevicesArray\n .filter((discoveredDevice) => {\n const existingConnectedDevice = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == discoveredDevice.bluetoothId\n );\n return !existingConnectedDevice;\n })\n .map((discoveredDevice) => {\n return { type: \"discoveredDevice\", data: discoveredDevice };\n });\n return createServerMessage(...serverMessages);\n }\n\n get #connectedDevicesMessage() {\n return createServerMessage({\n type: \"connectedDevices\",\n data: JSON.stringify({\n connectedDevices: DeviceManager.ConnectedDevices.map(\n (device) => device.bluetoothId\n ),\n }),\n });\n }\n\n // DEVICE LISTENERS\n\n #boundDeviceListeners: BoundDeviceEventListeners = {\n connectionMessage: this.#onDeviceConnectionMessage.bind(this),\n };\n\n #createDeviceMessage(\n device: Device,\n messageType: ConnectionMessageType,\n dataView?: DataView\n ): DeviceMessage {\n return {\n type: messageType as DeviceEventType,\n data: dataView || device.latestConnectionMessages.get(messageType),\n };\n }\n\n #onDeviceConnectionMessage(deviceEvent: DeviceEventMap[\"connectionMessage\"]) {\n const { target: device, message } = deviceEvent;\n _console.log(\"onDeviceConnectionMessage\", deviceEvent.message);\n\n if (!device.isConnected) {\n return;\n }\n\n const { messageType, dataView } = message;\n\n this.broadcastMessage(\n this.#createDeviceServerMessage(\n device,\n this.#createDeviceMessage(device, messageType, dataView)\n )\n );\n }\n\n // STATIC DEVICE LISTENERS\n #boundDeviceManagerListeners: BoundDeviceManagerEventListeners = {\n deviceConnected: this.#onDeviceConnected.bind(this),\n deviceDisconnected: this.#onDeviceDisconnected.bind(this),\n deviceIsConnected: this.#onDeviceIsConnected.bind(this),\n };\n\n #onDeviceConnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceConnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceConnected\", device.bluetoothId);\n addEventListeners(device, this.#boundDeviceListeners);\n device.isServerSide = true;\n }\n\n #onDeviceDisconnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceDisconnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceDisconnected\", device.bluetoothId);\n removeEventListeners(device, this.#boundDeviceListeners);\n }\n\n #onDeviceIsConnected(\n staticDeviceEvent: DeviceManagerEventMap[\"deviceIsConnected\"]\n ) {\n const { device } = staticDeviceEvent.message;\n _console.log(\"onDeviceIsConnected\", device.bluetoothId);\n this.broadcastMessage(this.#createDeviceIsConnectedMessage(device));\n }\n #createDeviceIsConnectedMessage(device: Device) {\n return this.#createDeviceServerMessage(device, {\n type: \"isConnected\",\n data: device.isConnected,\n });\n }\n\n #createDeviceServerMessage(device: Device, ...messages: DeviceMessage[]) {\n return createServerMessage({\n type: \"deviceMessage\",\n data: [device.bluetoothId!, createDeviceMessage(...messages)],\n });\n }\n\n // PARSING\n protected parseClientMessage(dataView: DataView) {\n let responseMessages: ArrayBuffer[] = [];\n\n parseMessage(\n dataView,\n ServerMessageTypes,\n this.#onClientMessage.bind(this),\n { responseMessages },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length > 0) {\n return concatenateArrayBuffers(responseMessages);\n }\n }\n\n #onClientMessage(\n messageType: ServerMessageType,\n dataView: DataView,\n context: { responseMessages: ArrayBuffer[] }\n ) {\n _console.log(\n `onClientMessage \"${messageType}\" (${dataView.byteLength} bytes)`\n );\n const { responseMessages } = context;\n switch (messageType) {\n case \"isScanningAvailable\":\n responseMessages.push(this.#isScanningAvailableMessage);\n break;\n case \"isScanning\":\n responseMessages.push(this.#isScanningMessage);\n break;\n case \"startScan\":\n scanner!.startScan();\n break;\n case \"stopScan\":\n scanner!.stopScan();\n break;\n case \"discoveredDevices\":\n responseMessages.push(this.#discoveredDevicesMessage);\n break;\n case \"connectToDevice\":\n {\n const { string: deviceId, byteOffset } =\n parseStringFromDataView(dataView);\n let connectionType = undefined;\n if (byteOffset < dataView.byteLength) {\n connectionType = ConnectionTypes[dataView.getUint8(byteOffset)];\n _console.log(`connectToDevice via ${connectionType}`);\n }\n scanner!.connectToDevice(deviceId, connectionType);\n }\n break;\n case \"disconnectFromDevice\":\n {\n const { string: deviceId } = parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n device.disconnect();\n }\n break;\n case \"connectedDevices\":\n responseMessages.push(this.#connectedDevicesMessage);\n break;\n case \"deviceMessage\":\n {\n const { string: deviceId, byteOffset } =\n parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n const _dataView = new DataView(\n dataView.buffer,\n dataView.byteOffset + byteOffset\n );\n const responseMessage = this.parseClientDeviceMessage(\n device,\n _dataView\n );\n if (responseMessage) {\n responseMessages.push(responseMessage);\n }\n }\n break;\n case \"requiredDeviceInformation\":\n {\n const { string: deviceId } = parseStringFromDataView(dataView);\n const device = DeviceManager.ConnectedDevices.find(\n (device) => device.bluetoothId == deviceId\n );\n if (!device) {\n _console.error(`no device found with id ${deviceId}`);\n break;\n }\n\n const messages = RequiredDeviceInformationMessageTypes.map(\n (messageType) => this.#createDeviceMessage(device, messageType)\n );\n if (device.isWifiAvailable) {\n RequiredWifiMessageTypes.forEach((messageType) => {\n messages.push(this.#createDeviceMessage(device, messageType));\n });\n }\n const responseMessage = this.#createDeviceServerMessage(\n device,\n ...messages\n );\n if (responseMessage) {\n responseMessages.push(responseMessage);\n }\n }\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n protected parseClientDeviceMessage(device: Device, dataView: DataView) {\n _console.log(\"onDeviceMessage\", device.bluetoothId, dataView);\n\n let responseMessages: DeviceMessage[] = [];\n\n parseMessage(\n dataView,\n ConnectionMessageTypes,\n this.#parseClientDeviceMessageCallback.bind(this),\n { responseMessages, device },\n true\n );\n\n if (responseMessages.length > 0) {\n return this.#createDeviceServerMessage(device, ...responseMessages);\n }\n }\n\n #parseClientDeviceMessageCallback(\n messageType: ConnectionMessageType,\n dataView: DataView,\n context: { responseMessages: DeviceMessage[]; device: Device }\n ) {\n _console.log(\n `clientDeviceMessage ${messageType} (${dataView.byteLength} bytes)`\n );\n switch (messageType) {\n case \"smp\":\n context.device.connectionManager!.sendSmpMessage(dataView.buffer);\n break;\n case \"tx\":\n context.device.connectionManager!.sendTxData(dataView.buffer);\n break;\n default:\n context.responseMessages.push(\n this.#createDeviceMessage(context.device, messageType)\n );\n break;\n }\n }\n}\n\nexport default BaseServer;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport {\n concatenateArrayBuffers,\n dataToArrayBuffer,\n} from \"../../utils/ArrayBufferUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\nimport BaseServer from \"../BaseServer.ts\";\nimport {\n webSocketPingMessage,\n webSocketPongMessage,\n WebSocketMessageType,\n WebSocketMessageTypes,\n webSocketPingTimeout,\n createWebSocketMessage,\n} from \"./WebSocketUtils.ts\";\n\nconst _console = createConsole(\"WebSocketServer\", { log: false });\n\n/** NODE_START */\nimport type * as ws from \"ws\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\n/** NODE_END */\n\ninterface WebSocketClient extends ws.WebSocket {\n isAlive: boolean;\n pingClientTimer?: Timer;\n}\ninterface WebSocketServer extends ws.WebSocketServer {}\n\nclass WebSocketServer extends BaseServer {\n get numberOfClients() {\n return this.#server?.clients.size || 0;\n }\n\n // WEBSOCKET SERVER\n\n #server?: WebSocketServer;\n get server() {\n return this.#server;\n }\n set server(newServer) {\n if (this.#server == newServer) {\n _console.log(\"redundant WebSocket server assignment\");\n return;\n }\n _console.log(\"assigning WebSocket server...\");\n\n if (this.#server) {\n _console.log(\"clearing existing WebSocket server...\");\n removeEventListeners(this.#server, this.#boundWebSocketServerListeners);\n }\n\n addEventListeners(newServer, this.#boundWebSocketServerListeners);\n this.#server = newServer;\n\n _console.log(\"assigned WebSocket server\");\n }\n\n // WEBSOCKET SERVER LISTENERS\n\n #boundWebSocketServerListeners = {\n close: this.#onWebSocketServerClose.bind(this),\n connection: this.#onWebSocketServerConnection.bind(this),\n error: this.#onWebSocketServerError.bind(this),\n headers: this.#onWebSocketServerHeaders.bind(this),\n listening: this.#onWebSocketServerListening.bind(this),\n };\n\n #onWebSocketServerClose() {\n _console.log(\"server.close\");\n }\n #onWebSocketServerConnection(client: WebSocketClient) {\n _console.log(\"server.connection\");\n client.isAlive = true;\n client.pingClientTimer = new Timer(\n () => this.#pingClient(client),\n webSocketPingTimeout\n );\n client.pingClientTimer.start();\n addEventListeners(client, this.#boundWebSocketClientListeners);\n this.dispatchEvent(\"clientConnected\", { client });\n }\n #onWebSocketServerError(error: Error) {\n _console.error(error);\n }\n #onWebSocketServerHeaders() {\n //_console.log(\"server.headers\");\n }\n #onWebSocketServerListening() {\n _console.log(\"server.listening\");\n }\n\n // WEBSOCKET CLIENT LISTENERS\n\n #boundWebSocketClientListeners: { [eventType: string]: Function } = {\n open: this.#onWebSocketClientOpen.bind(this),\n message: this.#onWebSocketClientMessage.bind(this),\n close: this.#onWebSocketClientClose.bind(this),\n error: this.#onWebSocketClientError.bind(this),\n };\n #onWebSocketClientOpen(event: ws.Event) {\n _console.log(\"client.open\");\n }\n #onWebSocketClientMessage(event: ws.MessageEvent) {\n _console.log(\"client.message\");\n const client = event.target as WebSocketClient;\n client.isAlive = true;\n client.pingClientTimer!.restart();\n const dataView = new DataView(dataToArrayBuffer(event.data as Buffer));\n _console.log(`received ${dataView.byteLength} bytes`, dataView.buffer);\n this.#parseWebSocketClientMessage(client, dataView);\n }\n #onWebSocketClientClose(event: ws.CloseEvent) {\n _console.log(\"client.close\");\n const client = event.target as WebSocketClient;\n client.pingClientTimer!.stop();\n removeEventListeners(client, this.#boundWebSocketClientListeners);\n this.dispatchEvent(\"clientDisconnected\", { client });\n }\n #onWebSocketClientError(event: ws.ErrorEvent) {\n _console.error(\"client.error\", event.message);\n }\n\n // PARSING\n #parseWebSocketClientMessage(client: WebSocketClient, dataView: DataView) {\n let responseMessages: ArrayBuffer[] = [];\n\n parseMessage(\n dataView,\n WebSocketMessageTypes,\n this.#onClientMessage.bind(this),\n { responseMessages },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length == 0) {\n _console.log(\"nothing to send back\");\n return;\n }\n\n const responseMessage = concatenateArrayBuffers(responseMessages);\n _console.log(`sending ${responseMessage.byteLength} bytes to client...`);\n try {\n client.send(responseMessage);\n } catch (error) {\n _console.log(\"error sending message\", error);\n }\n }\n\n #onClientMessage(\n messageType: WebSocketMessageType,\n dataView: DataView,\n context: { responseMessages: (ArrayBuffer | undefined)[] }\n ) {\n const { responseMessages } = context;\n switch (messageType) {\n case \"ping\":\n responseMessages.push(webSocketPongMessage);\n break;\n case \"pong\":\n break;\n case \"serverMessage\":\n const responseMessage = this.parseClientMessage(dataView);\n if (responseMessage) {\n responseMessages.push(\n createWebSocketMessage({\n type: \"serverMessage\",\n data: responseMessage,\n })\n );\n }\n break;\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n // CLIENT MESSAGING\n broadcastMessage(message: ArrayBuffer) {\n super.broadcastMessage(message);\n this.server!.clients.forEach((client) => {\n client.send(\n createWebSocketMessage({ type: \"serverMessage\", data: message })\n );\n });\n }\n\n // PING\n #pingClient(client: WebSocketClient) {\n if (!client.isAlive) {\n client.terminate();\n return;\n }\n client.isAlive = false;\n client.send(webSocketPingMessage);\n }\n}\n\nexport default WebSocketServer;\n","import { createConsole } from \"../../utils/Console.ts\";\nimport { createMessage, Message } from \"../ServerUtils.ts\";\n\nconst _console = createConsole(\"UDPUtils\", { log: false });\n\nexport const pongUDPClientTimeout = 2_000;\nexport const removeUDPClientTimeout = 4_000;\n\nexport const UDPServerMessageTypes = [\n \"ping\",\n \"pong\",\n \"setRemoteReceivePort\",\n \"serverMessage\",\n] as const;\nexport type UDPServerMessageType = (typeof UDPServerMessageTypes)[number];\n\nexport type UDPServerMessage =\n | UDPServerMessageType\n | Message<UDPServerMessageType>;\nexport function createUDPServerMessage(...messages: UDPServerMessage[]) {\n _console.log(\"createUDPServerMessage\", ...messages);\n return createMessage(UDPServerMessageTypes, ...messages);\n}\n\n// STATIC MESSAGES\nexport const udpPingMessage = createUDPServerMessage(\"ping\");\nexport const udpPongMessage = createUDPServerMessage(\"pong\");\n","import {\n concatenateArrayBuffers,\n dataToArrayBuffer,\n} from \"../../utils/ArrayBufferUtils.ts\";\nimport { createConsole } from \"../../utils/Console.ts\";\nimport {\n addEventListeners,\n removeEventListeners,\n} from \"../../utils/EventUtils.ts\";\nimport { parseMessage } from \"../../utils/ParseUtils.ts\";\nimport BaseServer from \"../BaseServer.ts\";\nimport {\n createUDPServerMessage,\n pongUDPClientTimeout,\n removeUDPClientTimeout,\n udpPongMessage,\n UDPServerMessageType,\n UDPServerMessageTypes,\n} from \"./UDPUtils.ts\";\nimport { Timer } from \"../../utils/Timer.ts\";\n\n/** NODE_START */\nimport type * as dgram from \"dgram\";\n/** NODE_END */\n\nconst _console = createConsole(\"UDPServer\", { log: false });\n\ninterface UDPClient extends dgram.RemoteInfo {\n receivePort?: number;\n isAlive?: boolean;\n removeSelfTimer: Timer;\n lastTimeSentData: number;\n}\n\ninterface UDPClientContext {\n client: UDPClient;\n responseMessages: (ArrayBuffer | undefined)[];\n}\n\nclass UDPServer extends BaseServer {\n // CLIENTS\n #clients: UDPClient[] = [];\n get numberOfClients() {\n return this.#clients.length;\n }\n\n #getClientByRemoteInfo(\n remoteInfo: dgram.RemoteInfo,\n createIfNotFound = false\n ) {\n const { address, port } = remoteInfo;\n let client = this.#clients.find(\n (client) => client.address == address && client.port == port\n );\n if (!client && createIfNotFound) {\n client = {\n ...remoteInfo,\n isAlive: true,\n removeSelfTimer: new Timer(() => {\n _console.log(\"removing client due to timeout...\");\n this.#removeClient(client!);\n }, removeUDPClientTimeout),\n lastTimeSentData: 0,\n };\n _console.log(\"created new client\", client);\n\n this.#clients.push(client);\n _console.log(`currently have ${this.numberOfClients} clients`);\n this.dispatchEvent(\"clientConnected\", { client });\n }\n return client;\n }\n\n #remoteInfoToString(client: dgram.RemoteInfo) {\n const { address, port } = client;\n return `${address}:${port}`;\n }\n #clientToString(client: UDPClient) {\n const { address, port, receivePort } = client;\n return `${address}:${port}=>${receivePort}`;\n }\n\n // UDP SOCKET\n\n #socket?: dgram.Socket;\n get socket() {\n return this.#socket;\n }\n set socket(newSocket) {\n if (this.#socket == newSocket) {\n _console.log(\"redundant udp socket assignment\");\n return;\n }\n _console.log(\"assigning udp socket...\");\n\n if (this.#socket) {\n _console.log(\"clearing existing udp socket...\");\n removeEventListeners(this.#socket, this.#boundSocketListeners);\n }\n\n addEventListeners(newSocket, this.#boundSocketListeners);\n this.#socket = newSocket;\n\n _console.log(\"assigned udp socket\");\n }\n\n // UDP SOCKET LISTENERS\n\n #boundSocketListeners = {\n close: this.#onSocketClose.bind(this),\n connect: this.#onSocketConnect.bind(this),\n error: this.#onSocketError.bind(this),\n listening: this.#onSocketListening.bind(this),\n message: this.#onSocketMessage.bind(this),\n };\n\n #onSocketClose() {\n _console.log(\"socket close\");\n }\n #onSocketConnect() {\n _console.log(\"socket connect\");\n }\n #onSocketError(error: Error) {\n _console.error(\"socket error\", error);\n }\n #onSocketListening() {\n const address = this.#socket!.address();\n _console.log(`socket listening on port ${address.address}:${address.port}`);\n }\n #onSocketMessage(message: Buffer, remoteInfo: dgram.RemoteInfo) {\n _console.log(\n `received ${message.length} bytes from ${this.#remoteInfoToString(\n remoteInfo\n )}`\n );\n const client = this.#getClientByRemoteInfo(remoteInfo, true);\n if (!client) {\n _console.error(\"no client found\");\n return;\n }\n client.removeSelfTimer.restart();\n const dataView = new DataView(dataToArrayBuffer(message));\n this.#onClientData(client, dataView);\n }\n\n // PARSING\n #onClientData(client: UDPClient, dataView: DataView) {\n _console.log(\n `parsing ${dataView.byteLength} bytes from ${this.#clientToString(\n client\n )}`,\n dataView.buffer\n );\n let responseMessages: ArrayBuffer[] = [];\n parseMessage(\n dataView,\n UDPServerMessageTypes,\n this.#onClientUDPMessage.bind(this),\n { responseMessages, client },\n true\n );\n\n responseMessages = responseMessages.filter(Boolean);\n\n if (responseMessages.length == 0) {\n _console.log(\"no response to send\");\n return;\n }\n\n if (client.receivePort == undefined) {\n _console.log(\"client has no defined receivePort\");\n return;\n }\n\n const response = concatenateArrayBuffers(responseMessages);\n _console.log(`responding with ${response.byteLength} bytes...`, response);\n this.#sendToClient(client, response);\n }\n #onClientUDPMessage(\n messageType: UDPServerMessageType,\n dataView: DataView,\n context: UDPClientContext\n ) {\n const { client, responseMessages } = context;\n _console.log(\n `received \"${messageType}\" message from ${client.address}:${client.port}`\n );\n switch (messageType) {\n case \"ping\":\n responseMessages.push(this.#createPongMessage(context));\n break;\n case \"pong\":\n break;\n case \"setRemoteReceivePort\":\n responseMessages.push(this.#parseRemoteReceivePort(dataView, client));\n break;\n case \"serverMessage\":\n const responseMessage = this.parseClientMessage(dataView);\n if (responseMessage) {\n responseMessages.push(\n createUDPServerMessage({\n type: \"serverMessage\",\n data: responseMessage,\n })\n );\n }\n break;\n\n default:\n _console.error(`uncaught messageType \"${messageType}\"`);\n break;\n }\n }\n\n #createPongMessage(context: UDPClientContext) {\n const { client } = context;\n // TODO: - no need to ping if streaming sensor data\n return udpPongMessage;\n }\n\n #parseRemoteReceivePort(dataView: DataView, client: UDPClient) {\n const receivePort = dataView.getUint16(0);\n client.receivePort = receivePort;\n _console.log(\n `updated ${client.address}:${client.port} receivePort to ${receivePort}`\n );\n const responseDataView = new DataView(new ArrayBuffer(2));\n responseDataView.setUint16(0, client.receivePort);\n return createUDPServerMessage({\n type: \"setRemoteReceivePort\",\n data: responseDataView,\n });\n }\n\n // CLIENT MESSAGING\n #sendToClient(client: UDPClient, message: ArrayBuffer) {\n _console.log(\n `sending ${message.byteLength} bytes to ${this.#clientToString(\n client\n )}...`\n );\n try {\n this.#socket!.send(\n new Uint8Array(message),\n client.receivePort,\n client.address,\n (error, bytes) => {\n if (error) {\n _console.error(\"error sending data\", error);\n return;\n }\n _console.log(`sent ${bytes} bytes`);\n client.lastTimeSentData = Date.now();\n }\n );\n } catch (error) {\n _console.error(\"serious error sending data\", error);\n }\n }\n broadcastMessage(message: ArrayBuffer) {\n super.broadcastMessage(message);\n this.#clients.forEach((client) => {\n this.#sendToClient(\n client,\n createUDPServerMessage({ type: \"serverMessage\", data: message })\n );\n });\n }\n\n // REMOVE CLIENT\n #removeClient(client: UDPClient) {\n _console.log(`removing client ${this.#clientToString(client)}...`);\n client.removeSelfTimer.stop();\n this.#clients = this.#clients.filter((_client) => _client != client);\n _console.log(`currently have ${this.numberOfClients} clients`);\n this.dispatchEvent(\"clientDisconnected\", { client });\n }\n}\n\nexport default UDPServer;\n","export {\n setAllConsoleLevelFlags,\n setConsoleLevelFlagsForType,\n} from \"./utils/Console.ts\";\nexport * as Environment from \"./utils/environment.ts\";\nexport { Vector2, Vector3, Quaternion, Euler } from \"./utils/MathUtils.ts\";\n\nexport {\n default as Device,\n DeviceEvent,\n DeviceEventMap,\n DeviceEventListenerMap,\n BoundDeviceEventListeners,\n} from \"./Device.ts\";\nexport {\n default as DeviceManager,\n DeviceManagerEvent,\n DeviceManagerEventMap,\n DeviceManagerEventListenerMap,\n BoundDeviceManagerEventListeners,\n} from \"./DeviceManager.ts\";\n\nexport { DeviceInformation } from \"./DeviceInformationManager.ts\";\nexport {\n DeviceType,\n DeviceTypes,\n MinNameLength,\n MaxNameLength,\n Sides,\n Side,\n} from \"./InformationManager.ts\";\nexport {\n MinWifiSSIDLength,\n MaxWifiSSIDLength,\n MinWifiPasswordLength,\n MaxWifiPasswordLength,\n} from \"./WifiManager.ts\";\nexport {\n SensorType,\n SensorTypes,\n ContinuousSensorType,\n ContinuousSensorTypes,\n} from \"./sensor/SensorDataManager.ts\";\nexport {\n MaxSensorRate,\n SensorRateStep,\n SensorConfiguration,\n} from \"./sensor/SensorConfigurationManager.ts\";\n\nexport {\n DefaultNumberOfPressureSensors,\n PressureData,\n} from \"./sensor/PressureSensorDataManager.ts\";\nexport { CenterOfPressure } from \"./utils/CenterOfPressureHelper.ts\";\nexport {\n VibrationConfiguration,\n VibrationLocation,\n VibrationLocations,\n VibrationType,\n VibrationTypes,\n MaxNumberOfVibrationWaveformEffectSegments,\n MaxVibrationWaveformSegmentDuration,\n MaxVibrationWaveformEffectSegmentDelay,\n MaxVibrationWaveformEffectSegmentLoopCount,\n MaxNumberOfVibrationWaveformSegments,\n MaxVibrationWaveformEffectSequenceLoopCount,\n} from \"./vibration/VibrationManager.ts\";\nexport {\n VibrationWaveformEffect,\n VibrationWaveformEffects,\n} from \"./vibration/VibrationWaveformEffects.ts\";\n\nexport {\n FileType,\n FileTypes,\n FileTransferDirection,\n FileTransferDirections,\n} from \"./FileTransferManager.ts\";\nexport {\n TfliteSensorType,\n TfliteSensorTypes,\n TfliteTask,\n TfliteTasks,\n TfliteFileConfiguration as TfliteFileConfiguration,\n} from \"./TfliteManager.ts\";\n\nexport {\n CameraConfiguration,\n CameraCommand,\n CameraCommands,\n CameraConfigurationType,\n CameraConfigurationTypes,\n} from \"./CameraManager.ts\";\n\nexport {\n MicrophoneConfiguration,\n MicrophoneCommand,\n MicrophoneCommands,\n MicrophoneConfigurationType,\n MicrophoneConfigurationTypes,\n MicrophoneConfigurationValues,\n} from \"./MicrophoneManager.ts\";\n\nexport {\n DisplayBrightness,\n DisplayBrightnesses,\n DisplaySize,\n DisplayBitmapColorPair,\n DisplayPixelDepths,\n DefaultNumberOfDisplayColors,\n MinSpriteSheetNameLength,\n MaxSpriteSheetNameLength,\n DisplayBitmap,\n DisplaySpriteColorPair,\n DisplayWireframeEdge,\n DisplayWireframe,\n DisplayBezierCurveType,\n DisplayBezierCurveTypes,\n} from \"./DisplayManager.ts\";\n\nexport { wait, Timer } from \"./utils/Timer.ts\";\n\nexport {\n DisplaySegmentCap,\n DisplaySegmentCaps,\n DisplayAlignment,\n DisplayAlignments,\n DisplayDirection,\n DisplayDirections,\n} from \"./utils/DisplayContextState.ts\";\n\nexport {\n maxDisplayScale,\n DisplayColorRGB,\n pixelDepthToNumberOfColors,\n displayCurveTypeToNumberOfControlPoints,\n mergeWireframes,\n intersectWireframes,\n isWireframePolygon,\n} from \"./utils/DisplayUtils.ts\";\n\n/** BROWSER_START */\nexport {\n svgToDisplayContextCommands,\n svgToSprite,\n svgToSpriteSheet,\n isValidSVG,\n getSvgStringFromDataUrl,\n} from \"./utils/SvgUtils.ts\";\n/** BROWSER_END */\n\nexport {\n DisplayContextCommand,\n DisplayContextCommandType,\n DisplayContextCommandTypes,\n DisplaySpriteContextCommandType,\n DisplaySpriteContextCommandTypes,\n} from \"./utils/DisplayContextCommand.ts\";\n\nexport {\n simplifyPoints,\n simplifyCurves,\n simplifyPointsAsCubicCurveControlPoints,\n} from \"./utils/PathUtils.ts\";\n\nexport {\n DisplaySprite,\n DisplaySpriteSheet,\n DisplaySpriteSheetPalette,\n DisplaySpritePaletteSwap,\n parseFont,\n getFontUnicodeRange,\n stringToSprites,\n fontToSpriteSheet,\n getFontMetrics,\n DisplaySpriteSubLine,\n DisplaySpriteLine,\n DisplaySpriteLines,\n getFontMaxHeight,\n getMaxSpriteSheetSize,\n englishRegex,\n FontToSpriteSheetOptions,\n} from \"./utils/DisplaySpriteSheetUtils.ts\";\n\n/** BROWSER_START */\nexport {\n default as DisplayCanvasHelper,\n DisplayCanvasHelperEvent,\n DisplayCanvasHelperEventMap,\n DisplayCanvasHelperEventListenerMap,\n} from \"./utils/DisplayCanvasHelper.ts\";\n/** BROWSER_END */\n\n/** BROWSER_START */\nexport { Font, Glyph } from \"opentype.js\";\n/** BROWSER_END */\n\n/** BROWSER_START */\nexport {\n resizeAndQuantizeImage,\n quantizeImage,\n imageToSprite,\n imageToSpriteSheet,\n canvasToSprite,\n canvasToSpriteSheet,\n resizeImage,\n imageToBitmaps,\n canvasToBitmaps,\n} from \"./utils/DisplayBitmapUtils.ts\";\n/** BROWSER_END */\n\nexport { rgbToHex, hexToRGB } from \"./utils/ColorUtils.ts\";\n\nexport {\n default as DevicePair,\n DevicePairEvent,\n DevicePairEventMap,\n DevicePairEventListenerMap,\n BoundDevicePairEventListeners,\n DevicePairType,\n DevicePairTypes,\n} from \"./devicePair/DevicePair.ts\";\n\nimport { addEventListeners, removeEventListeners } from \"./utils/EventUtils.ts\";\nexport const EventUtils = {\n addEventListeners,\n removeEventListeners,\n};\n\nimport { throttle, debounce } from \"./utils/ThrottleUtils.ts\";\nexport const ThrottleUtils = {\n throttle,\n debounce,\n};\n\nexport { DiscoveredDevice } from \"./scanner/BaseScanner.ts\";\n/** NODE_START */\nexport { default as Scanner } from \"./scanner/Scanner.ts\";\nexport { default as WebSocketServer } from \"./server/websocket/WebSocketServer.ts\";\nexport { default as UDPServer } from \"./server/udp/UDPServer.ts\";\n/** NODE_END */\n/** BROWSER_START */\nexport { default as WebSocketClient } from \"./server/websocket/WebSocketClient.ts\";\n/** BROWSER_END */\n/** LS_START */\nexport { default as WebSocketClient } from \"./server/websocket/WebSocketClient.ts\";\n/** LS_END */\n\nexport { default as RangeHelper, Range } from \"./utils/RangeHelper.ts\";\n"],"names":["_console"],"mappings":"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GF;AACE;;;;;;;;;;;;;;;;;;AA+BF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACA;;AAEF;AACE;;AAEF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACE;AACA;;AAGF;AACA;AACA;;AAEF;AACE;AACE;AACA;;AAEF;AACE;AACA;;AAIF;AACA;AACA;;;;AAKA;;;;AAKF;;;;;;;AAYI;;;AAIF;AACE;;;;AAIA;;;AAGF;AAEA;;AAGE;;;AAGA;;AAEF;AACA;AAEA;AACE;;AAMI;;;;;;AAMF;AACE;;AAIE;;;AAKF;AACA;AACF;;;AAIA;AACA;AACA;;;;AAGF;AACA;AACA;;AAGF;AAEA;;;AAKF;;AAGE;;AAGF;AACE;;;;;AAYA;;AAGF;;AAGE;AACE;AACA;;;;AAKF;AACA;;;;AAMA;AACA;AACA;;AAEH;;AC7ZD;AAqBM;AACJ;AACA;AACA;AACA;AACE;AACF;AACF;AAEM;AACJ;AACA;AACA;AACA;AACE;AACF;AACF;;ACjCA;AAIA;AAuCA;AACE;;;AAMF;AAEA;AAGE;AACF;AAEA;AACE;AACF;AA6BA;AACE;AACE;AACE;AACA;AACE;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACD;AACE;AACD;AACF;AACF;AACD;AACE;AACA;AACE;AACE;AACD;AACF;AACF;AACD;AACE;AACA;;;AAGC;AACF;AACD;AACE;AACA;AACE;AACD;AACF;AACF;AACF;AAEM;AACA;AACL;AACA;AACA;;AAEK;AAED;;;AAOJ;;;AAGE;;;;;;;AAOF;AACF;AAEO;AACA;AAGA;AAEP;AACE;;;;AAMA;;;AAGI;AACA;;AAEF;AACA;AACF;AACF;AAIM;;AAKJ;AACA;;;;;AAQI;;;;;;;AAOF;AACA;AACF;AACA;AACF;AAEM;AAGJ;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AAKA;AACA;AACA;AACE;;;;AAMF;AACA;AACA;AACE;;;;AAMF;AACE;;;;AAMF;AACE;;;AAIJ;AACF;AAEO;;ACpRP;AAIA;AACE;AAEE;;;;AASA;AACE;;;;;;AAMM;;;;AAQR;;;;AAKA;AACA;;;;;AAKH;;ACpBD;AAWA;AAGA;AACE;AACF;AASA;AACE;AACE;;AAGF;;;AAIA;;;AAKA;;;AAKA;AACE;;AAEF;AACE;;AAGF;AACA;;;;AAIE;AACE;;;AAGF;;;;AAOE;;AAEF;;AAGF;AACE;;AAEF;AACE;;AAGF;AACA;AAGA;AACE;;AAEE;;AAGF;AACE;AACE;;AAED;AAED;AACA;AAEA;;;AAIA;AAEA;AAEA;AACA;;;AAEA;AACA;AACA;AACA;AACA;;;AAGJ;;AAGE;;;AAKA;AACA;AACE;AACA;;;AAMA;AACA;;AAEA;AACA;AACA;AACA;AACE;AAGA;;AAIA;;AAOA;;AAEA;AAIA;;AAGA;AACE;AAGA;;AAEF;AACE;AACA;AACA;AACE;;;;;;AAMV;AACE;;;AAOA;;AAGI;AAIA;;AAGA;AACE;AAGA;;AAEJ;AAGF;;AAEF;AACE;;AAEE;;AAEF;AACA;AACA;AACA;;AAGF;AACE;AAEA;AACA;;AAGF;AACE;AAEA;AACA;AAKA;AAGA;;;AAUA;AACE;;;AAEA;;;AAIJ;AAIE;;;;AAQA;;AAGA;AACE;AACA;;;AAEA;AACA;;AAEF;;AAGE;AACA;AACA;AACE;;;;;AAMJ;AACA;;AAGF;AACE;;AAEF;AACE;;AAEE;;AAEF;AACE;;;AAEA;AACA;AACA;;AAGF;AACE;AACA;AACA;AACA;;;AAEA;AACA;AACA;;;;;AAMF;;AAEH;;ACtUD;AACA;AACA;AAEO;AACP;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO;AACP;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEO;AACP;AACA;AACA;;ACzYA;AAEO;AAEP;AACA;AACA;AACA;AAGA;AACA;AACA;AAOA;AAIA;AAEA;AACA;AAIA;AACA;AACA;AAMA;AACA;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAEA;AAGA;AACA;AACA;AAEA;AACA;AAGA;AACA;AACA;AAGA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;;ACnbA;AAEO;AAGA;AACL;;;;;;AAQK;AAyBP;AACE;AAEA;;;;AAKA;AACA;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;;AAIA;;AAGE;AACE;;;AAGF;AACE;;;;AAKJ;;AAIA;AAEA;;AAEA;;AAIA;AAEA;;;AAIF;;;AAGA;AACE;AACA;AACE;;;AAIF;;AAEA;;AAKF;AACA;;;;;;AAMA;AACE;AACA;;AAEF;;AAGE;AACA;AAEA;;AAGF;AACE;;;AAGE;;;;AAIA;;;;AAIA;;;;AAMF;;AAGA;;AAGF;;;AAIE;AACA;AAEA;AAEA;AACA;;AAGF;AACE;;;AAGE;;;;AAMF;;AAGA;;;AAIA;;AAIA;AACA;AAEA;;AAGF;;AAGE;AACA;AAEA;;AAIF;AACA;;;;AAIE;AACA;;AAIF;;AAGE;;AAGA;AAC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ACA;AACE;AACA;;;AAEA;AACA;;;AAIJ;AACE;;AAEF;AACE;;AAEE;;;AAGF;;AAIF;AACE;;;;;AAcE;;;AAGA;;AAEA;AACE;;AAEF;;;AAGA;;AAKM;AACF;;AAGJ;AACE;;AAEF;AACE;;;;AAMN;;AAEE;;AAEE;;;;AAGE;;AAED;;AAEH;AACE;;;;AAIF;AACA;;;AAIA;AACA;;;AAGF;;AAIE;;;;AAKA;AACA;AACA;;;;AAKA;;AAEH;;;AClOD;AAEO;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAkDK;;;;;;;;;;;;;;AAmBP;AACE;AACE;;AAGF;AACE;;AAGF;AACE;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AACH;AACG;AAEH;AACG;AACH;AACG;AAEH;AACE;;;;;AAKF;AACA;AACE;;;;AAIE;;AAEE;AACD;AACD;;;AAEA;;;AAIA;;AAEE;AACD;AACD;;;AAEA;;;AAIA;;AAEE;AACD;AACD;;;AAEA;;AAEJ;AACA;AACE;;;;AAIE;;;AAGA;;AAEJ;AACA;AACE;;;;;;AAMA;AACE;AACE;;;AAGN;AACA;AACE;;;AAGA;AACE;;AAEJ;;;;AAII;AACE;AACE;AACA;;AAED;;;AAKP;;;;AAII;AACE;AACE;AACE;AACA;AACD;;;;AAMT;AACA;;AAEE;;;;AAIA;;;AAIA;;;;AAIA;;;AAIJ;;;;AAQA;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAKF;AACA;;;;AAIE;AACE;;;AAIF;AACE;;;AAGA;AACE;AACF;AACE;AACF;AACE;;AAGJ;;;;AAKF;;;;;AAME;AACE;;;AAGF;AACE;;;AAIF;;AAEE;AACE;AACE;AACE;;;AAGJ;;;AAGI;;AAEK;AAC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F;AACE;;AAEF;AACE;;AAEF;AACE;;;;;AAKF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;;;AAKA;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAEF;;AAEE;;AAGF;;AAEE;;AAEF;;AAEE;;AAGF;AACE;;AAGF;AACE;;AAGF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAGF;A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","x_google_ignoreList":[20]}
|