Add local glTF viewer and visual coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a02d1c-27fc-7761-9f51-ed97b2dd4e3e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
type RowFormat = "f32" | "u32" | "i32";
|
||||
type RowDescriptor = {
|
||||
name: string;
|
||||
rows: number;
|
||||
stride: number;
|
||||
format: RowFormat;
|
||||
};
|
||||
|
||||
const views = {
|
||||
f32: Float32Array,
|
||||
u32: Uint32Array,
|
||||
i32: Int32Array,
|
||||
};
|
||||
|
||||
export class SharedRows {
|
||||
readonly name: string;
|
||||
rows: number;
|
||||
readonly stride: number;
|
||||
readonly format: RowFormat;
|
||||
buffer: SharedArrayBuffer;
|
||||
|
||||
constructor({ name, rows, stride, format }: RowDescriptor) {
|
||||
this.name = name;
|
||||
this.rows = rows;
|
||||
this.stride = stride;
|
||||
this.format = format;
|
||||
this.buffer = new SharedArrayBuffer(rows * stride);
|
||||
}
|
||||
|
||||
resize(rows: number) {
|
||||
if (rows <= this.rows) return this;
|
||||
const previous = this.view;
|
||||
this.rows = rows;
|
||||
this.buffer = new SharedArrayBuffer(rows * this.stride);
|
||||
this.view.set(previous);
|
||||
return this;
|
||||
}
|
||||
|
||||
get view() {
|
||||
const View = views[this.format];
|
||||
return new View(this.buffer);
|
||||
}
|
||||
|
||||
row(index: number) {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= this.rows)
|
||||
throw new RangeError(`ROW_RANGE: ${this.name}[${index}]`);
|
||||
const width = this.stride / views[this.format].BYTES_PER_ELEMENT;
|
||||
return this.view.subarray(index * width, (index + 1) * width);
|
||||
}
|
||||
|
||||
write(index: number, values: ArrayLike<number>) {
|
||||
const row = this.row(index);
|
||||
if (values.length !== row.length) throw new RangeError("ROW_WIDTH");
|
||||
row.set(values);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(serialized: string) {
|
||||
let offset = 0;
|
||||
const skip = () => {
|
||||
while (/\s/u.test(serialized[offset] ?? "")) offset++;
|
||||
};
|
||||
const take = (character: string) => {
|
||||
skip();
|
||||
if (serialized[offset] !== character)
|
||||
throw new SyntaxError(`Expected ${character} at ${offset}`);
|
||||
offset++;
|
||||
};
|
||||
const word = () => {
|
||||
skip();
|
||||
const start = offset;
|
||||
while (offset < serialized.length && !/[\s()]/u.test(serialized[offset]))
|
||||
offset++;
|
||||
return serialized.slice(start, offset);
|
||||
};
|
||||
const value = (): any => {
|
||||
skip();
|
||||
if (serialized[offset] === '"') {
|
||||
const start = offset++;
|
||||
while (offset < serialized.length) {
|
||||
if (serialized[offset] === "\\") offset += 2;
|
||||
else if (serialized[offset++] === '"') break;
|
||||
}
|
||||
return JSON.parse(serialized.slice(start, offset));
|
||||
}
|
||||
if (serialized[offset] !== "(") {
|
||||
const token = word();
|
||||
if (token === "true") return true;
|
||||
if (token === "false") return false;
|
||||
if (token === "null") return null;
|
||||
const number = Number(token);
|
||||
if (!Number.isNaN(number)) return number;
|
||||
throw new SyntaxError(`Unknown value ${token}`);
|
||||
}
|
||||
take("(");
|
||||
const tag = word();
|
||||
if (tag === "array") {
|
||||
const result = [];
|
||||
skip();
|
||||
while (serialized[offset] !== ")") result.push(value());
|
||||
take(")");
|
||||
return result;
|
||||
}
|
||||
if (tag === "object") {
|
||||
const result: Record<string, unknown> = {};
|
||||
skip();
|
||||
while (serialized[offset] !== ")") {
|
||||
take("(");
|
||||
if (word() !== "field") throw new SyntaxError("Expected field");
|
||||
const key = value();
|
||||
result[key] = value();
|
||||
take(")");
|
||||
skip();
|
||||
}
|
||||
take(")");
|
||||
return result;
|
||||
}
|
||||
if (tag === "yawn-graph") {
|
||||
if (word() !== "1") throw new SyntaxError("Graph version");
|
||||
const result = value();
|
||||
take(")");
|
||||
return result;
|
||||
}
|
||||
throw new SyntaxError(`Unknown list ${tag}`);
|
||||
};
|
||||
return value();
|
||||
}
|
||||
|
||||
export class YawnCore {
|
||||
readonly ready = Promise.resolve();
|
||||
readonly graphs: any[] = [];
|
||||
readonly serializedGraphs: string[] = [];
|
||||
readonly loadouts: string[] = [];
|
||||
readonly uploadedTextures: { name: string; mipLevel: number }[] = [];
|
||||
fps?: number;
|
||||
disposed = false;
|
||||
#arrays = new Map<string, SharedRows>();
|
||||
#nextObjects = new Map<string, number>();
|
||||
|
||||
constructor() {
|
||||
this.#arrays.set(
|
||||
"signals",
|
||||
new SharedRows({ name: "signals", rows: 1, stride: 32, format: "f32" }),
|
||||
);
|
||||
}
|
||||
|
||||
createRows(descriptor: RowDescriptor) {
|
||||
const current = this.#arrays.get(descriptor.name);
|
||||
if (current) {
|
||||
if (
|
||||
current.stride !== descriptor.stride ||
|
||||
current.format !== descriptor.format
|
||||
)
|
||||
throw new Error(`ROW_LAYOUT: ${descriptor.name}`);
|
||||
return Promise.resolve(current.resize(descriptor.rows));
|
||||
}
|
||||
const rows = new SharedRows(descriptor);
|
||||
this.#arrays.set(descriptor.name, rows);
|
||||
return Promise.resolve(rows);
|
||||
}
|
||||
|
||||
createRowsBatch(descriptors: RowDescriptor[]) {
|
||||
return Promise.all(
|
||||
descriptors.map((descriptor) => this.createRows(descriptor)),
|
||||
);
|
||||
}
|
||||
|
||||
deleteRows(name: string) {
|
||||
this.#arrays.delete(name);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
allocateObject(name: string) {
|
||||
const id = this.#nextObjects.get(name) ?? 0;
|
||||
this.#nextObjects.set(name, id + 1);
|
||||
const rows = this.array(name);
|
||||
rows.resize(id + 1);
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
deleteObject() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
array(name: string) {
|
||||
const rows = this.#arrays.get(name);
|
||||
if (!rows) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
compileGraph(serialized: string) {
|
||||
this.serializedGraphs.push(serialized);
|
||||
this.graphs.push(deserialize(serialized));
|
||||
return Promise.resolve(`loadout-${this.graphs.length}`);
|
||||
}
|
||||
|
||||
switchLoadout(id: string) {
|
||||
this.loadouts.push(id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setFps(fps: number) {
|
||||
this.fps = fps;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
uploadTexture(name: string, _image: unknown, mipLevel = 0) {
|
||||
this.uploadedTextures.push({ name, mipLevel });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
deleteTexture() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user