mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
A mover set whose glTF or atlas fails to load retries by itself after retryAfterMs (1,500 ms by default), at most three attempts, on Cesium, Navara and the fake engine alike: failed() is true while the latest attempt failed and stays true after the last, ready() turns true when a retry loads, and an instance given another pickId loads again. Healthy models of the same class no longer load again when one fails. The layers drop what they did instead: moverBank's rebuilds, the loops that dropped failed handles in flights, military and local ADS-B, and the tracked-model backoff in the aircraft display. The contract suite checks the one behaviour on both engines, with a mutant that never retries.
663 lines
21 KiB
JavaScript
663 lines
21 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import * as Cesium from 'cesium';
|
||
import * as geo from '../../geo/index.js';
|
||
import { createCesiumMovers } from './movers.js';
|
||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||
|
||
// Cesium's own collections, billboards and points (they draw nothing
|
||
// without a renderer, but hold what the engine writes). What needs the
|
||
// network or a DOM is faked: the glTF load and the atlas crop.
|
||
function setup({ loadModel, cropCells } = {}) {
|
||
const viewer = createFakeViewer();
|
||
viewer.scene.frameState.mode = Cesium.SceneMode.SCENE3D;
|
||
const requests = [];
|
||
const crops = [];
|
||
const loads = [];
|
||
const factories = {
|
||
cropCells:
|
||
cropCells ??
|
||
(async (atlas, keys) => {
|
||
crops.push({ atlas, keys });
|
||
return new Map(keys.map((key) => [key, { width: 8, height: 8, key }]));
|
||
}),
|
||
loadModel:
|
||
loadModel ??
|
||
(async (options) => {
|
||
loads.push(options);
|
||
return fakeModel(options);
|
||
}),
|
||
};
|
||
const part = createCesiumMovers(viewer, {
|
||
factories,
|
||
requestRender: (reason) => requests.push(reason),
|
||
});
|
||
const root = () => viewer.primitiveList[0];
|
||
return { viewer, requests, crops, loads, root, ...part };
|
||
}
|
||
|
||
// What the engine reads and writes of a Cesium Model: its own model matrix
|
||
// (kept as given), a colour it copies.
|
||
function fakeModel(options) {
|
||
let color;
|
||
return {
|
||
options,
|
||
ready: false,
|
||
show: true,
|
||
modelMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||
get color() {
|
||
return color;
|
||
},
|
||
set color(value) {
|
||
color = Cesium.Color.clone(value);
|
||
},
|
||
isDestroyed: () => false,
|
||
destroy() {
|
||
this.destroyed = true;
|
||
},
|
||
};
|
||
}
|
||
|
||
const at = (lon, lat, height = 0) =>
|
||
geo.cartesian3.fromDegrees(lon, lat, height);
|
||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||
const atlas = {
|
||
canvas: { width: 64, height: 32 },
|
||
cells: { jet: [0, 0, 0.5, 1], heli: [0.5, 0, 1, 1] },
|
||
};
|
||
const items = (collection) =>
|
||
Array.from({ length: collection.length }, (_, index) =>
|
||
collection.get(index),
|
||
);
|
||
|
||
test('creating the movers changes nothing; the first set adds one root collection', () => {
|
||
const { viewer, movers, root, destroy } = setup();
|
||
assert.equal(viewer.scene.primitives.length, 0);
|
||
const set = movers.createSet({
|
||
id: 'a',
|
||
kind: 'point',
|
||
capacity: 2,
|
||
point: { sizePx: 4 },
|
||
});
|
||
assert.equal(viewer.scene.primitives.length, 1);
|
||
assert.ok(root() instanceof Cesium.PrimitiveCollection);
|
||
const [collection] = items(root());
|
||
assert.ok(collection instanceof Cesium.PointPrimitiveCollection);
|
||
set.destroy();
|
||
assert.equal(root().length, 0);
|
||
assert.equal(collection.isDestroyed(), true, 'a destroyed set is released');
|
||
destroy();
|
||
assert.equal(viewer.scene.primitives.length, 0);
|
||
});
|
||
|
||
test('sprites are billboards with the set style once the atlas cells are ready', async () => {
|
||
const { movers, root, requests, crops } = setup();
|
||
const set = movers.createSet({
|
||
id: 'flights',
|
||
kind: 'sprite',
|
||
capacity: 3,
|
||
depthTest: 'always',
|
||
sprite: { atlas, sizePx: 20, scaleByDistance: [1e3, 1, 1e6, 0.5] },
|
||
});
|
||
const [collection] = items(root());
|
||
assert.ok(collection instanceof Cesium.BillboardCollection);
|
||
assert.deepEqual(crops[0].keys, ['jet', 'heli']);
|
||
assert.equal(crops[0].atlas, atlas);
|
||
assert.equal(set.ready(), false, 'not ready before the atlas');
|
||
set.set(0, {
|
||
position: at(1, 2),
|
||
rotation: 0.25,
|
||
cell: 'heli',
|
||
pickId: 'x1',
|
||
});
|
||
set.set(1, {
|
||
position: at(3, 4),
|
||
color: [1, 0, 0, 0.5],
|
||
scale: 2,
|
||
show: false,
|
||
});
|
||
set.setCount(2);
|
||
set.commit();
|
||
assert.equal(collection.length, 0, 'waits for the atlas');
|
||
await settle();
|
||
assert.equal(set.ready(), true);
|
||
assert.equal(collection.length, 2);
|
||
const [first, second] = items(collection);
|
||
assert.ok(first instanceof Cesium.Billboard);
|
||
assert.ok(Cesium.Cartesian3.equalsEpsilon(first.position, at(1, 2), 0, 1e-6));
|
||
assert.equal(first.width, 20);
|
||
assert.equal(first.height, 20);
|
||
assert.equal(first.rotation, 0.25);
|
||
assert.equal(first.id, 'x1');
|
||
assert.equal(first.disableDepthTestDistance, Number.POSITIVE_INFINITY);
|
||
assert.ok(
|
||
Cesium.NearFarScalar.equals(
|
||
first.scaleByDistance,
|
||
new Cesium.NearFarScalar(1e3, 1, 1e6, 0.5),
|
||
),
|
||
);
|
||
assert.equal(first.image, 'flights:heli', 'the cell image, by set and key');
|
||
assert.equal(first.show, true);
|
||
assert.equal(second.image, 'flights:jet', 'the first cell by default');
|
||
assert.equal(second.show, false);
|
||
assert.equal(second.scale, 2);
|
||
assert.ok(Cesium.Color.equals(second.color, new Cesium.Color(1, 0, 0, 0.5)));
|
||
assert.ok(requests.includes('movers'));
|
||
// The same cell is not sent again; a new one is.
|
||
const sent = [];
|
||
const setImage = first.setImage;
|
||
first.setImage = (id, image) => {
|
||
sent.push(id);
|
||
setImage.call(first, id, image);
|
||
};
|
||
set.set(0, { cell: 'heli' });
|
||
set.commit();
|
||
assert.deepEqual(sent, []);
|
||
set.set(0, { cell: 'jet' });
|
||
set.commit();
|
||
assert.deepEqual(sent, ['flights:jet']);
|
||
// Lowering the count hides the instances past it.
|
||
set.set(1, { show: true });
|
||
set.commit();
|
||
assert.equal(second.show, true);
|
||
set.setCount(1);
|
||
set.commit();
|
||
assert.equal(second.show, false);
|
||
});
|
||
|
||
test('a sprite set destroyed before its atlas arrives draws nothing', async () => {
|
||
const { movers, root } = setup();
|
||
const set = movers.createSet({
|
||
id: 'early',
|
||
kind: 'sprite',
|
||
capacity: 1,
|
||
sprite: { atlas, sizePx: 8 },
|
||
});
|
||
const [collection] = items(root());
|
||
set.set(0, { position: at(0, 0) });
|
||
set.setCount(1);
|
||
set.commit();
|
||
set.destroy();
|
||
await settle();
|
||
assert.equal(collection.isDestroyed(), true);
|
||
assert.equal(root().length, 0);
|
||
});
|
||
|
||
test('points take the size, scale, outline, translucency and depth test of the set', () => {
|
||
const { movers, root } = setup();
|
||
const set = movers.createSet({
|
||
id: 'sats',
|
||
kind: 'point',
|
||
capacity: 2,
|
||
depthTest: { disableWithinM: 5000 },
|
||
point: {
|
||
sizePx: 4,
|
||
outlinePx: 1,
|
||
outlineColor: [1, 1, 1, 0.5],
|
||
scaleByDistance: [1e3, 2, 1e7, 0.5],
|
||
translucencyByDistance: [1e6, 1, 1e8, 0],
|
||
},
|
||
});
|
||
set.set(0, {
|
||
position: at(5, 6),
|
||
scale: 1.5,
|
||
color: [0, 1, 0, 1],
|
||
pickId: 25544,
|
||
});
|
||
set.setCount(1);
|
||
set.commit();
|
||
const [collection] = items(root());
|
||
const [point] = items(collection);
|
||
assert.ok(point instanceof Cesium.PointPrimitive);
|
||
assert.equal(point.pixelSize, 6, 'sizePx × scale');
|
||
assert.equal(point.outlineWidth, 1);
|
||
assert.ok(
|
||
Cesium.Color.equals(point.outlineColor, new Cesium.Color(1, 1, 1, 0.5)),
|
||
);
|
||
assert.ok(
|
||
Cesium.NearFarScalar.equals(
|
||
point.scaleByDistance,
|
||
new Cesium.NearFarScalar(1e3, 2, 1e7, 0.5),
|
||
),
|
||
);
|
||
assert.ok(
|
||
Cesium.NearFarScalar.equals(
|
||
point.translucencyByDistance,
|
||
new Cesium.NearFarScalar(1e6, 1, 1e8, 0),
|
||
),
|
||
);
|
||
assert.equal(point.disableDepthTestDistance, 5000);
|
||
assert.equal(point.id, 25544);
|
||
assert.ok(Cesium.Color.equals(point.color, new Cesium.Color(0, 1, 0, 1)));
|
||
set.set(0, { outlineColor: [0.5, 0, 1, 0.25] });
|
||
set.commit();
|
||
assert.deepEqual(
|
||
Cesium.Color.pack(point.outlineColor, []),
|
||
[0.5, 0, 1, 0.25],
|
||
);
|
||
assert.ok(Cesium.Color.equals(point.color, new Cesium.Color(0, 1, 0, 1)));
|
||
assert.equal(point.outlineWidth, 1);
|
||
assert.throws(
|
||
() => set.set(0, { scale: 9, outlineColor: [1, 0, 0, 2] }),
|
||
TypeError,
|
||
);
|
||
set.commit();
|
||
assert.equal(
|
||
point.pixelSize,
|
||
6,
|
||
'an invalid outline update leaves the point unchanged',
|
||
);
|
||
// 'normal' keeps Cesium's depth test.
|
||
const normal = movers.createSet({
|
||
id: 'normal',
|
||
kind: 'point',
|
||
capacity: 1,
|
||
point: { sizePx: 2 },
|
||
});
|
||
normal.set(0, { position: at(0, 0) });
|
||
normal.setCount(1);
|
||
normal.commit();
|
||
const [, normalCollection] = items(root());
|
||
assert.equal(items(normalCollection)[0].disableDepthTestDistance, 0);
|
||
assert.equal(items(normalCollection)[0].outlineWidth, 0);
|
||
});
|
||
|
||
test('sets are ordered in the root by their order', () => {
|
||
const { movers, root } = setup();
|
||
const make = (id, order) =>
|
||
movers.createSet({
|
||
id,
|
||
kind: 'point',
|
||
capacity: 1,
|
||
order,
|
||
point: { sizePx: 2 },
|
||
});
|
||
make('top', 5);
|
||
make('bottom', -1);
|
||
const middle = make('middle', 0);
|
||
make('middle-later', 0);
|
||
assert.deepEqual(
|
||
items(root()).map((collection) => collection.gevMoverSet),
|
||
['bottom', 'middle', 'middle-later', 'top'],
|
||
);
|
||
middle.destroy();
|
||
make('again', 0);
|
||
assert.deepEqual(
|
||
items(root()).map((collection) => collection.gevMoverSet),
|
||
['bottom', 'middle-later', 'again', 'top'],
|
||
);
|
||
});
|
||
|
||
test('models load per instance with the set options and follow position, orientation and colour', async () => {
|
||
const { movers, root, loads } = setup();
|
||
const make = (id, model, pickable = false) =>
|
||
movers.createSet({ id, kind: 'model', capacity: 2, pickable, model });
|
||
const set = make(
|
||
'models',
|
||
{
|
||
url: '/models/airplane.glb',
|
||
minimumPixelSize: 24,
|
||
colorBlend: { mode: 'mix', amount: 0.4 },
|
||
},
|
||
true,
|
||
);
|
||
const position = at(10, 20, 3000);
|
||
// A quarter turn about z.
|
||
const orientation = { x: 0, y: 0, z: Math.SQRT1_2, w: Math.SQRT1_2 };
|
||
set.set(0, { position, orientation, color: [1, 0.5, 0, 0.8], scale: 3 });
|
||
set.set(1, { position: at(11, 20), pickId: 'second' });
|
||
set.setCount(2);
|
||
set.commit();
|
||
assert.equal(set.ready(), false);
|
||
await settle();
|
||
const [collection] = items(root());
|
||
assert.ok(collection instanceof Cesium.PrimitiveCollection);
|
||
assert.equal(collection.length, 2, 'one model per instance');
|
||
assert.equal(loads.length, 2);
|
||
const [options] = loads;
|
||
assert.equal(options.url, '/models/airplane.glb');
|
||
assert.equal(options.minimumPixelSize, 24);
|
||
assert.equal(options.colorBlendMode, Cesium.ColorBlendMode.MIX);
|
||
assert.equal(options.colorBlendAmount, 0.4);
|
||
assert.equal(options.allowPicking, true);
|
||
assert.equal(options.customShader, undefined, 'lit by default');
|
||
assert.equal(options.asynchronous, false, 'built at once');
|
||
// The glTF's own axes: no Cesium axis correction (Y_UP_TO_Z_UP ×
|
||
// Z_UP_TO_X_UP), so an orientation draws the same on every engine.
|
||
assert.equal(options.upAxis, Cesium.Axis.Z);
|
||
assert.equal(options.forwardAxis, Cesium.Axis.X);
|
||
assert.ok(
|
||
Cesium.Matrix4.equals(
|
||
Cesium.ModelUtility.getAxisCorrectionMatrix(
|
||
options.upAxis,
|
||
options.forwardAxis,
|
||
new Cesium.Matrix4(),
|
||
),
|
||
Cesium.Matrix4.IDENTITY,
|
||
),
|
||
);
|
||
assert.deepEqual(options.environmentMapOptions, { enabled: false });
|
||
const [first, second] = items(collection);
|
||
assert.notEqual(first.modelMatrix, second.modelMatrix);
|
||
const translation = Cesium.Matrix4.getTranslation(
|
||
first.modelMatrix,
|
||
new Cesium.Cartesian3(),
|
||
);
|
||
assert.ok(Cesium.Cartesian3.equalsEpsilon(translation, position, 0, 1e-6));
|
||
// The model's x axis turned onto Earth-fixed y.
|
||
assert.ok(Math.abs(first.modelMatrix[1] - 1) < 1e-12);
|
||
assert.ok(Math.abs(first.modelMatrix[0]) < 1e-12);
|
||
assert.equal(first.scale, 3);
|
||
assert.ok(first.color.equalsEpsilon(new Cesium.Color(1, 0.5, 0, 0.8), 1e-6));
|
||
assert.equal(second.id, 'second');
|
||
assert.equal(set.ready(), false, 'loaded models not yet ready');
|
||
first.ready = true;
|
||
assert.equal(set.ready(0), true, 'an instance is ready with its model');
|
||
assert.equal(set.ready(1), false);
|
||
assert.equal(set.ready(), false);
|
||
second.ready = true;
|
||
assert.equal(set.ready(), true);
|
||
assert.equal(set.ready(2), false, 'past the count');
|
||
// Shrinking hides the instance past the count; a later write moves it.
|
||
set.setCount(1);
|
||
set.commit();
|
||
assert.equal(second.show, false);
|
||
set.set(0, { show: false });
|
||
set.commit();
|
||
assert.equal(first.show, false);
|
||
set.destroy();
|
||
assert.equal(collection.isDestroyed(), true);
|
||
assert.equal(first.destroyed, true, 'the models go with the set');
|
||
});
|
||
|
||
test('model colour blends and unlit become Cesium model options', async () => {
|
||
const { movers, loads } = setup();
|
||
const make = (id, model) => {
|
||
const set = movers.createSet({ id, kind: 'model', capacity: 1, model });
|
||
set.set(0, { position: at(0, 0) });
|
||
set.setCount(1);
|
||
set.commit();
|
||
};
|
||
make('plain', { url: '/a.glb' });
|
||
make('replace', { url: '/a.glb', colorBlend: { mode: 'replace' } });
|
||
make('unlit', { url: '/a.glb', unlit: true });
|
||
await settle();
|
||
const [plain, replace, unlit] = loads;
|
||
// Without colorBlend the colour sets only the alpha.
|
||
assert.equal(plain.colorBlendMode, Cesium.ColorBlendMode.MIX);
|
||
assert.equal(plain.colorBlendAmount, 0);
|
||
assert.equal(plain.allowPicking, false);
|
||
assert.equal(plain.minimumPixelSize, 0);
|
||
assert.equal(replace.colorBlendMode, Cesium.ColorBlendMode.REPLACE);
|
||
assert.ok(unlit.customShader instanceof Cesium.CustomShader);
|
||
assert.equal(unlit.customShader.lightingModel, Cesium.LightingModel.UNLIT);
|
||
});
|
||
|
||
test('a model that loads after its set is destroyed is released', async () => {
|
||
let finish;
|
||
const pending = new Promise((resolve) => (finish = resolve));
|
||
const { movers, root } = setup({ loadModel: () => pending });
|
||
const set = movers.createSet({
|
||
id: 'late',
|
||
kind: 'model',
|
||
capacity: 1,
|
||
model: { url: '/a.glb' },
|
||
});
|
||
set.set(0, { position: at(0, 0) });
|
||
set.setCount(1);
|
||
set.commit();
|
||
set.destroy();
|
||
const model = fakeModel({});
|
||
finish(model);
|
||
await settle();
|
||
assert.equal(model.destroyed, true);
|
||
assert.equal(root().length, 0);
|
||
});
|
||
|
||
test('failed model attempts stop at three and a replacement gets a fresh budget', async (t) => {
|
||
t.mock.method(console, 'warn', () => {});
|
||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||
let loads = 0;
|
||
const { movers } = setup({
|
||
loadModel: async () => {
|
||
loads++;
|
||
throw new Error('503');
|
||
},
|
||
});
|
||
const set = movers.createSet({
|
||
id: 'broken',
|
||
kind: 'model',
|
||
capacity: 1,
|
||
model: { url: '/a.glb' },
|
||
});
|
||
set.set(0, { position: at(0, 0), pickId: 'a' });
|
||
set.setCount(1);
|
||
set.commit();
|
||
assert.equal(set.failed(), false);
|
||
await new Promise(setImmediate);
|
||
assert.equal(set.failed(0), true);
|
||
t.mock.timers.tick(1499);
|
||
assert.equal(loads, 1);
|
||
for (const delay of [1, 1500]) {
|
||
t.mock.timers.tick(delay);
|
||
await new Promise(setImmediate);
|
||
}
|
||
t.mock.timers.tick(10000);
|
||
assert.equal(loads, 3);
|
||
assert.equal(set.failed(), true);
|
||
assert.equal(set.ready(), false);
|
||
set.set(0, { pickId: 'b' });
|
||
set.commit();
|
||
await new Promise(setImmediate);
|
||
assert.equal(loads, 4);
|
||
set.destroy();
|
||
t.mock.timers.tick(10000);
|
||
assert.equal(loads, 4);
|
||
assert.equal(set.failed(), false);
|
||
});
|
||
|
||
test('a failed instance retries without reloading its healthy neighbour', async (t) => {
|
||
t.mock.method(console, 'warn', () => {});
|
||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||
let loads = 0;
|
||
let finish;
|
||
const { movers, root } = setup({
|
||
loadModel: async (options) => {
|
||
loads++;
|
||
if (loads === 1) throw new Error('503');
|
||
if (loads === 3) await new Promise((resolve) => (finish = resolve));
|
||
return { ...fakeModel(options), ready: true };
|
||
},
|
||
});
|
||
const set = movers.createSet({
|
||
id: 'fleet',
|
||
kind: 'model',
|
||
capacity: 2,
|
||
retryAfterMs: 20,
|
||
model: { url: '/a.glb' },
|
||
});
|
||
t.after(() => set.destroy());
|
||
set.set(0, { position: at(0, 0), pickId: 'a' });
|
||
set.set(1, { position: at(1, 0), pickId: 'b' });
|
||
set.setCount(2);
|
||
set.commit();
|
||
await new Promise(setImmediate);
|
||
const [collection] = items(root());
|
||
const [healthy] = items(collection);
|
||
assert.equal(set.failed(0), true);
|
||
assert.equal(set.ready(1), true);
|
||
t.mock.timers.tick(20);
|
||
assert.equal(
|
||
set.failed(0),
|
||
true,
|
||
'failure stays visible while retry is pending',
|
||
);
|
||
set.set(0, { position: at(2, 0), show: false });
|
||
set.commit();
|
||
finish();
|
||
await new Promise(setImmediate);
|
||
assert.equal(loads, 3);
|
||
assert.equal(set.ready(), true);
|
||
assert.equal(set.failed(), false);
|
||
assert.equal(collection.get(0), healthy);
|
||
assert.equal(collection.get(1).show, false);
|
||
assert.equal(collection.get(1).modelMatrix[12], at(2, 0).x);
|
||
set.set(0, { pickId: 'c' });
|
||
set.commit();
|
||
await new Promise(setImmediate);
|
||
assert.equal(loads, 4, 'a healthy index reloads for its new identity');
|
||
});
|
||
|
||
test('atlas retries apply the latest instances and cancel on destruction', async (t) => {
|
||
t.mock.method(console, 'warn', () => {});
|
||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||
let loads = 0;
|
||
const { movers, root } = setup({
|
||
cropCells: async (atlas, keys) => {
|
||
if (++loads < 3) throw new Error('503');
|
||
return new Map(keys.map((key) => [key, { width: 8, height: 8 }]));
|
||
},
|
||
});
|
||
const set = movers.createSet({
|
||
id: 'sprites',
|
||
kind: 'sprite',
|
||
capacity: 1,
|
||
retryAfterMs: 20,
|
||
sprite: { atlas, sizePx: 8 },
|
||
});
|
||
set.setCount(1);
|
||
set.commit();
|
||
await new Promise(setImmediate);
|
||
assert.equal(set.failed(), true);
|
||
for (let i = 0; i < 2; i++) {
|
||
t.mock.timers.tick(20);
|
||
await new Promise(setImmediate);
|
||
}
|
||
assert.equal(set.ready(), true);
|
||
assert.equal(set.failed(), false);
|
||
assert.equal(root().get(0).length, 1);
|
||
set.destroy();
|
||
const cancelled = movers.createSet({
|
||
id: 'cancelled',
|
||
kind: 'sprite',
|
||
capacity: 1,
|
||
sprite: { atlas, sizePx: 8 },
|
||
});
|
||
cancelled.destroy();
|
||
await new Promise(setImmediate);
|
||
t.mock.timers.tick(10000);
|
||
assert.equal(loads, 4);
|
||
});
|
||
|
||
// A layer removes an instance by moving its last one into the freed index
|
||
// (src/layers/shared/render/moverBank.js).
|
||
test('an instance moved into a failed index loads its own model', async (t) => {
|
||
t.mock.method(console, 'warn', () => {});
|
||
let loads = 0;
|
||
const { movers } = setup({
|
||
loadModel: async (options) => {
|
||
if (loads++ === 0) throw new Error('503');
|
||
return fakeModel(options);
|
||
},
|
||
});
|
||
const set = movers.createSet({
|
||
id: 'fleet',
|
||
kind: 'model',
|
||
capacity: 2,
|
||
model: { url: '/a.glb' },
|
||
});
|
||
set.set(0, { position: at(0, 0), pickId: 'a' });
|
||
set.set(1, { position: at(1, 0), pickId: 'b' });
|
||
set.setCount(2);
|
||
set.commit();
|
||
await settle();
|
||
assert.equal(set.failed(0), true);
|
||
assert.equal(set.failed(1), false);
|
||
// 'a' is removed: 'b' moves into index 0.
|
||
set.set(0, { position: at(1, 0), pickId: 'b' });
|
||
set.setCount(1);
|
||
assert.equal(set.failed(0), false, "'b' does not take the failure of 'a'");
|
||
assert.equal(set.failed(), false);
|
||
set.commit();
|
||
await settle();
|
||
assert.equal(loads, 3, 'index 0 loads again for its new instance');
|
||
assert.equal(set.failed(0), false);
|
||
set.destroy();
|
||
});
|
||
|
||
test('identify names the set, instance and pickId of a picked billboard or point', async () => {
|
||
const { movers, root, identify, viewer } = setup();
|
||
// The fake camera is over (0°, ~1°), 7000 km from the centre.
|
||
viewer.camera.positionWC = Cesium.Cartesian3.fromDegrees(0, 0, 7e5);
|
||
const set = movers.createSet({
|
||
id: 'sats',
|
||
kind: 'point',
|
||
capacity: 3,
|
||
point: { sizePx: 4 },
|
||
});
|
||
set.set(0, { position: at(0, 1) });
|
||
set.set(1, { position: at(1, 0), pickId: 25544 });
|
||
set.set(2, { position: at(180, 0), pickId: 'far' });
|
||
set.setCount(3);
|
||
set.commit();
|
||
const [, second, third] = items(items(root())[0]);
|
||
assert.deepEqual(identify({ primitive: second, id: 'stale' }), {
|
||
owner: 'sats',
|
||
instance: 1,
|
||
id: 25544,
|
||
pickable: false,
|
||
visible: true,
|
||
});
|
||
assert.equal(
|
||
identify({ primitive: third }).visible,
|
||
false,
|
||
'behind the horizon',
|
||
);
|
||
// The pickId the layer writes now, not the one Cesium holds.
|
||
set.set(1, { pickId: 'renamed' });
|
||
assert.equal(identify({ primitive: second }).id, 'renamed');
|
||
// An instance past the count is not identified.
|
||
set.setCount(1);
|
||
set.commit();
|
||
assert.equal(identify({ primitive: second }), null);
|
||
assert.equal(identify({ primitive: {}, id: 'x' }), null);
|
||
assert.equal(identify(undefined), null);
|
||
const sprites = movers.createSet({
|
||
id: 'flights',
|
||
kind: 'sprite',
|
||
capacity: 1,
|
||
pickable: true,
|
||
sprite: { atlas, sizePx: 8 },
|
||
});
|
||
sprites.set(0, { position: at(0, 0.5), pickId: 'ae01ce' });
|
||
sprites.setCount(1);
|
||
sprites.commit();
|
||
await settle();
|
||
const [billboard] = items(items(root())[1]);
|
||
assert.deepEqual(identify({ primitive: billboard }), {
|
||
owner: 'flights',
|
||
instance: 0,
|
||
id: 'ae01ce',
|
||
pickable: true,
|
||
visible: true,
|
||
});
|
||
set.destroy();
|
||
sprites.destroy();
|
||
assert.equal(identify({ primitive: billboard }), null);
|
||
});
|
||
|
||
test('primitiveOf maps a live set of this engine to its collection', () => {
|
||
const { movers, root, primitiveOf } = setup();
|
||
const set = movers.createSet({
|
||
id: 'a',
|
||
kind: 'point',
|
||
capacity: 1,
|
||
point: { sizePx: 2 },
|
||
});
|
||
assert.equal(primitiveOf(set), items(root())[0]);
|
||
assert.equal(primitiveOf({ id: 'a' }), null, 'a look-alike is not the set');
|
||
assert.equal(primitiveOf({ primitive: 1 }), null);
|
||
set.destroy();
|
||
assert.equal(primitiveOf(set), null);
|
||
});
|