fix(vessels): upload Navara chevrons and restore click focus

Fix delayed and growing mover bank commits, reject stale hidden model picks, and route world-focus through the engine. Strengthen vessel parity and performance checks without changing thresholds.
This commit is contained in:
German Khokhlov committed 2026-10-22 12:31:59 +04:00
1 parent 69a78710cc
commit c8a4f3ed98
10 files changed
+96 -7

No files matched your search

+2 -2
View File
@@ -215,7 +215,7 @@ async function main() {
console.log(` Tag : ${TAG}`);
console.log(` Engine : ${ENGINE ?? 'viewer (Cesium)'}`);
console.log(` Data : ${DATA_MODE}`);
console.log(` Browser : ${HEADFUL ? 'headful (hardware renderer requested)' : 'headless (SwiftShader)'}`);
console.log(` Browser : ${HEADFUL ? 'headful (hardware renderer requested)' : 'headless (Metal)'}`);
console.log(` Ports : ${PORT_KEYS.join(', ')}\n`);
for (const line of vesselCardProvenanceLines(DATA_MODE)) console.log(` ${line}`);
console.log('');
@@ -238,7 +238,7 @@ async function main() {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
...(!HEADFUL ? ['--use-gl=angle', '--use-angle=swiftshader'] : []),
'--use-angle=metal',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
+1 -1
View File
@@ -223,7 +223,7 @@ async function main() {
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--use-angle=metal',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
+1 -1
View File
@@ -378,7 +378,7 @@ test('refused canned destinations commit no location or POI state', () => {
test('world-focus listener lifecycle is symmetric and idempotent', () => {
assert.match(ui, /registerWorldFocusRequestListener\(\s*window,/);
assert.match(ui, /routeWorldFocusRequest\(/);
assert.match(ui, /flyToWorldTarget\(this\.viewer, detail\)/);
assert.match(ui, /flyToWorldTarget\(this\.engine \?\? this\.viewer, detail\)/);
assert.match(ui, /this\._removeWorldRequestFocusListener\?\.\(\);/);
assert.match(ui, /this\._removeWorldRequestFocusListener = null;/);
});
+2 -1
View File
@@ -196,10 +196,11 @@ export function createCpuPicker({ projector, camera, movers, lines }) {
const found = movers().find(({ options }) => options.id === owner);
if (!found) return true;
const { options, store, pickRadiusPx } = found;
if (instance < 0 || instance >= store.count || store.shows[instance] !== 1)
return false;
const modelRadius =
options.kind === 'model' ? (pickRadiusPx?.(instance) ?? null) : null;
if (options.kind === 'model' && modelRadius == null) return true;
if (instance >= store.count) return false;
geo.cartesian3.fromElements(
store.positions[instance * 3],
store.positions[instance * 3 + 1],
+23
View File
@@ -411,3 +411,26 @@ test('a model hit is checked against its projected position within its drawn rad
assert.equal(picking.drillPick({ x: 121, y: 100 })[0]?.id, 'jet');
assert.equal(picking.drillPick({ x: 125, y: 100 }).length, 0);
});
test('stale GPU model hits cannot select hidden or removed instances', async () => {
const view = fakeView();
const model = modelSet('models', [[100, 100, 50, 'jet']], null);
const picking = createNavaraPicking({
view,
projector,
camera,
movers: () => [model],
lines: () => [],
resolveBatch: () => ({ set: 'models', instance: 0, pickId: 'jet' }),
...timers(),
});
model.store.set(0, { show: false });
const hidden = picking.clickPick({ x: 100, y: 100 });
view.emit('featureClick', { batchId: 1 });
assert.equal(await hidden, undefined);
model.store.setCount(0);
const removed = picking.clickPick({ x: 100, y: 100 });
view.emit('featureClick', { batchId: 1 });
assert.equal(await removed, undefined);
picking.destroy();
});
-1
View File
@@ -83,7 +83,6 @@ export function createMoverBank(engine, { options, write, onCommit }) {
capacity = wanted;
handles.forEach((handle, index) => write(handle, set, index));
set.setCount(visible ? handles.length : 0);
dirty = true;
};
const bank = {
handles,
+45
View File
@@ -286,3 +286,48 @@ test('vessels on the Navara renderer: one always-on-top chevron bank, MMSI picks
globalThis.document = previousDocument;
}
});
test('a vessel bank commits rows arriving after its empty visibility commit and after growth', async () => {
const { createMoverBank } = await import('../aircraft/render/navara.js');
let commits = 0;
let pending = null;
const bank = createMoverBank(
{
movers: {
createSet: () => ({
set() {},
setCount() {},
destroy() {},
commit() {
commits++;
},
}),
},
},
{
options: (capacity) => ({ capacity }),
write() {},
onCommit: (next) => {
pending = next;
},
},
);
const flush = () => {
const next = pending;
pending = null;
next?.commit();
};
bank.setVisible(true);
flush();
bank.add({});
assert.equal(pending, bank, 'first rows schedule their GPU upload');
flush();
assert.equal(commits, 1);
for (let i = 1; i < 1024; i++) bank.add({});
flush();
bank.add({});
assert.equal(pending, bank, 'capacity growth schedules its GPU upload');
flush();
assert.equal(commits, 3);
bank.destroy();
});
+1
View File
@@ -264,6 +264,7 @@ export class StyleManager extends ShellFacade {
this._layerBindings = new LayerBindings({
viewer,
engine,
services: {
imageryHost: () =>
resolveImageryHost({
+3 -1
View File
@@ -13,6 +13,7 @@ import { registerNavigationAuthorityListener } from '../navigationPolicy.js';
export class LayerBindings {
constructor({
viewer,
engine,
services,
readControls,
operations,
@@ -23,6 +24,7 @@ export class LayerBindings {
this,
{
viewer,
engine,
services,
readControls,
_feedback: feedback,
@@ -71,7 +73,7 @@ export class LayerBindings {
routeWorldFocusRequest(
event,
(detail, fly) => this._runExplicitWorldFocus(detail, fly),
(detail) => flyToWorldTarget(this.viewer, detail),
(detail) => flyToWorldTarget(this.engine ?? this.viewer, detail),
);
this._removeWorldRequestFocusListener = registerWorldFocusRequestListener(
window,
+18
View File
@@ -152,3 +152,21 @@ test('weather shell supplies live imagery hosts to wind and observations and rel
owner.disconnect();
for (const id of ids) assert.equal(attached.get(id), null);
});
test('world focus uses the engine when the shell has no Cesium viewer', async (t) => {
const { requestWorldFocus } = await import('../worldFocus.js');
replaceWindow(t, new EventTarget());
const engine = createFakeEngine();
let target;
engine.camera.flyToSphere = (sphere) => { target = sphere.center; };
const owner = new LayerBindings({
viewer: null, engine, services: {}, readControls: () => ({}),
operations: { _runExplicitWorldFocus: (_detail, fly) => fly() },
});
owner.observeCamera();
const position = { x: 6378137, y: 0, z: 0 };
requestWorldFocus({ kind: 'vessel', id: '111', position });
assert.deepEqual(target, position);
owner.stop();
});