mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Review of Tasks 2–3: - the mover store's instance checks no longer build arrays or closures per set() (orientation and colour checked field by field), and dirty() returns one reused range object; - Cesium models get their matrix and colour through scratch objects (Model.modelMatrix and Model.color copy what they are given) instead of a new Matrix4 and Color per placement; - a destroyed MoverSet releases its store (count 0, empty instance arrays, no pick ids) so a set object a layer still holds keeps no memory, and the Cesium parts drop their billboard, point and model lists; - (with the overlay commit) the Navara line viewport is updated in place. Node micro-measure, 10 000 instances set + packed per frame: 0.27-0.29 ms, about 1.7 KB per frame (0.17 B per instance), as before. Contract: Cesium 70/70, Navara 76/76.
210 lines
6.4 KiB
JavaScript
210 lines
6.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
MOVER_KINDS,
|
|
createMoverSet,
|
|
createMoverStore,
|
|
normalizeMoverSetOptions,
|
|
} from './movers.js';
|
|
|
|
const atlas = {
|
|
canvas: { width: 64, height: 32 },
|
|
cells: { jet: [0, 0, 0.5, 1], heli: [0.5, 0, 1, 1] },
|
|
};
|
|
|
|
test('set options are normalised with the contract defaults', () => {
|
|
assert.deepEqual(MOVER_KINDS, ['sprite', 'point', 'model']);
|
|
const sprite = normalizeMoverSetOptions({
|
|
id: 'flights',
|
|
kind: 'sprite',
|
|
capacity: 10,
|
|
sprite: { atlas, sizePx: 24 },
|
|
});
|
|
assert.equal(sprite.id, 'flights');
|
|
assert.equal(sprite.order, 0);
|
|
assert.equal(sprite.depthTest, 'normal');
|
|
assert.equal(sprite.pickable, false);
|
|
assert.deepEqual(sprite.cellKeys, ['jet', 'heli']);
|
|
assert.equal(sprite.sprite.scaleByDistance, null);
|
|
const point = normalizeMoverSetOptions({
|
|
id: 'sats',
|
|
kind: 'point',
|
|
capacity: 3,
|
|
depthTest: { disableWithinM: 5000 },
|
|
point: { sizePx: 4 },
|
|
});
|
|
assert.equal(point.point.outlinePx, 0);
|
|
assert.deepEqual(point.depthTest, { disableWithinM: 5000 });
|
|
const model = normalizeMoverSetOptions({
|
|
id: 'models',
|
|
kind: 'model',
|
|
capacity: 2,
|
|
model: { url: '/models/airplane.glb' },
|
|
});
|
|
assert.equal(model.model.minimumPixelSize, 0);
|
|
assert.equal(model.model.maximumScale, Infinity);
|
|
assert.equal(model.model.colorBlend, null);
|
|
});
|
|
|
|
test('bad set options are refused with a TypeError', () => {
|
|
for (const options of [
|
|
{},
|
|
{ id: 'a', kind: 'cube', capacity: 1 },
|
|
{ id: 'a', kind: 'sprite', capacity: 0, sprite: { atlas, sizePx: 2 } },
|
|
{ id: 'a', kind: 'sprite', capacity: 1.5, sprite: { atlas, sizePx: 2 } },
|
|
{ id: 'a', kind: 'sprite', capacity: 1 },
|
|
{ id: 'a', kind: 'sprite', capacity: 1, sprite: { atlas, sizePx: -1 } },
|
|
{ id: 'a', kind: 'point', capacity: 1, point: {} },
|
|
{ id: 'a', kind: 'model', capacity: 1, model: {} },
|
|
{
|
|
id: 'a',
|
|
kind: 'point',
|
|
capacity: 1,
|
|
point: { sizePx: 2 },
|
|
depthTest: 'sometimes',
|
|
},
|
|
{
|
|
id: 'a',
|
|
kind: 'sprite',
|
|
capacity: 1,
|
|
sprite: { atlas, sizePx: 2, scaleByDistance: [1, 2] },
|
|
},
|
|
])
|
|
assert.throws(() => normalizeMoverSetOptions(options), TypeError);
|
|
});
|
|
|
|
test('the store keeps each instance in flat arrays with defaults', () => {
|
|
const store = createMoverStore(
|
|
normalizeMoverSetOptions({
|
|
id: 'f',
|
|
kind: 'sprite',
|
|
capacity: 3,
|
|
sprite: { atlas, sizePx: 16 },
|
|
}),
|
|
);
|
|
assert.equal(store.capacity, 3);
|
|
assert.equal(store.count, 0);
|
|
store.set(1, {
|
|
position: { x: 1, y: 2, z: 3 },
|
|
rotation: 0.5,
|
|
color: [1, 0, 0, 0.5],
|
|
scale: 2,
|
|
cell: 'heli',
|
|
show: false,
|
|
pickId: 'abc',
|
|
});
|
|
store.set(0, { position: { x: 4, y: 5, z: 6 } });
|
|
assert.deepEqual([...store.positions.subarray(0, 6)], [4, 5, 6, 1, 2, 3]);
|
|
assert.deepEqual(
|
|
[...store.colors.subarray(0, 8)],
|
|
[1, 1, 1, 1, 1, 0, 0, 0.5],
|
|
);
|
|
assert.equal(store.rotations[1], 0.5);
|
|
assert.equal(store.scales[0], 1);
|
|
assert.equal(store.scales[1], 2);
|
|
assert.equal(store.cells[0], 0);
|
|
assert.equal(store.cells[1], 1);
|
|
assert.equal(store.shows[0], 1);
|
|
assert.equal(store.shows[1], 0);
|
|
assert.equal(store.pickIds[1], 'abc');
|
|
assert.equal(store.pickIds[0], undefined);
|
|
// set() without a field keeps the stored value.
|
|
store.set(1, { position: { x: 7, y: 8, z: 9 } });
|
|
assert.equal(store.scales[1], 2);
|
|
assert.equal(store.shows[1], 0);
|
|
store.setCount(2);
|
|
assert.equal(store.count, 2);
|
|
assert.deepEqual([...store.positionsView()], [4, 5, 6, 7, 8, 9]);
|
|
assert.deepEqual(store.dirty(), { from: 0, to: 2 });
|
|
store.clean();
|
|
assert.equal(store.dirty(), null);
|
|
store.set(2, { position: { x: 0, y: 0, z: 1 } });
|
|
assert.deepEqual(store.dirty(), { from: 2, to: 3 });
|
|
});
|
|
|
|
test('bad instances are refused and leave the store unchanged', () => {
|
|
const store = createMoverStore(
|
|
normalizeMoverSetOptions({
|
|
id: 'f',
|
|
kind: 'point',
|
|
capacity: 2,
|
|
point: { sizePx: 4 },
|
|
}),
|
|
);
|
|
for (const [index, fields] of [
|
|
[2, { position: { x: 0, y: 0, z: 0 } }],
|
|
[-1, { position: { x: 0, y: 0, z: 0 } }],
|
|
[0, { position: { x: Number.NaN, y: 0, z: 0 } }],
|
|
[0, { color: [1, 1, 1] }],
|
|
[0, { cell: 'jet' }],
|
|
[0, { scale: -1 }],
|
|
])
|
|
assert.throws(() => store.set(index, fields), TypeError);
|
|
assert.throws(() => store.setCount(3), RangeError);
|
|
assert.equal(store.dirty(), null);
|
|
});
|
|
|
|
test('models take an orientation quaternion', () => {
|
|
const store = createMoverStore(
|
|
normalizeMoverSetOptions({
|
|
id: 'm',
|
|
kind: 'model',
|
|
capacity: 1,
|
|
model: { url: '/m.glb' },
|
|
}),
|
|
);
|
|
assert.deepEqual([...store.orientations], [0, 0, 0, 1]);
|
|
store.set(0, { orientation: { x: 0, y: 0, z: 1, w: 0 } });
|
|
assert.deepEqual([...store.orientations], [0, 0, 1, 0]);
|
|
assert.throws(() => store.set(0, { orientation: { x: 0, y: 0 } }), TypeError);
|
|
});
|
|
|
|
test('the set facade forwards to the store and an engine commit, once destroyed it refuses', () => {
|
|
const store = createMoverStore(
|
|
normalizeMoverSetOptions({
|
|
id: 'f',
|
|
kind: 'point',
|
|
capacity: 2,
|
|
point: { sizePx: 4 },
|
|
}),
|
|
);
|
|
const calls = [];
|
|
const set = createMoverSet(store, {
|
|
commit: () => calls.push(['commit', { ...store.dirty() }]),
|
|
destroy: () => calls.push(['destroy']),
|
|
});
|
|
set.set(0, { position: { x: 1, y: 2, z: 3 } });
|
|
set.setCount(1);
|
|
assert.equal(set.count(), 1);
|
|
assert.deepEqual([...set.positionsView()], [1, 2, 3]);
|
|
set.commit();
|
|
assert.deepEqual(calls, [['commit', { from: 0, to: 1 }]]);
|
|
assert.equal(store.dirty(), null, 'commit cleans the store');
|
|
set.destroy();
|
|
set.destroy();
|
|
assert.deepEqual(calls.at(-1), ['destroy']);
|
|
assert.equal(calls.length, 2);
|
|
assert.throws(() => set.set(0, {}), /destroyed/);
|
|
assert.throws(() => set.commit(), /destroyed/);
|
|
});
|
|
|
|
test('a destroyed set releases its instance arrays', () => {
|
|
const store = createMoverStore(
|
|
normalizeMoverSetOptions({
|
|
id: 'r',
|
|
kind: 'point',
|
|
capacity: 1000,
|
|
point: { sizePx: 2 },
|
|
}),
|
|
);
|
|
const set = createMoverSet(store, { commit() {}, destroy() {} });
|
|
set.set(0, { position: { x: 1, y: 2, z: 3 }, pickId: 'p' });
|
|
set.setCount(1);
|
|
set.destroy();
|
|
assert.equal(store.count, 0);
|
|
assert.equal(store.positions.length, 0);
|
|
assert.equal(store.pickIds.length, 0);
|
|
assert.equal(set.positionsView().length, 0);
|
|
assert.equal(store.dirty(), null);
|
|
});
|