Restore Wildlife privacy fills through engine.ground

Draw distinct privacy cells in one ground set with taxon-coloured fills
at 25 percent opacity and two-pixel outlines at 80 percent opacity.
Restore the original Cesium ground appearance on both engines, replacing
the shared display's raised, depth-free outline. Ground classification
handles terrain, tiles and horizon occlusion; picks pass through.

Retain bounds-and-accent deduplication, unchanged-geometry reuse, selection,
wide-view removal and detach cleanup. Check the fill and outline on fake
and headless Cesium engines, and keep engine-switch ownership coverage.
Update the Wildlife switch check to read ground cells.

No coupling count fell or grew: all eleven metrics are unchanged, including
891 viewer lines and three external Cesium import files.

Checked in the browser, keyless: the Wildlife engine switch draws the cell
set on both engines; its keyboard step still meets the known Tab issue.
This commit is contained in:
German Khokhlov committed 2026-11-07 22:47:24 +04:00
1 parent fbadf792d8
commit db4bdba64f
4 files changed
+155 -150

No files matched your search

+12 -10
View File
@@ -16,7 +16,7 @@ import {
// Wildlife Sightings over Cesium → Navara → Cesium → Navara: on each engine
// the four fixture sightings are badges in engine.movers at their positions
// (each one picks as itself), the density glow is its own sprite set, and
// the obscured record's privacy cell is an outline on engine.lines; a
// the obscured record's privacy cell is a fill and outline on engine.ground; a
// pointer click on a badge selects that sighting and opens its card.
//
// The first two switches are made from the keyboard with the card open (Tab
@@ -30,7 +30,7 @@ import {
const LAYER = 'wildlife-sightings';
const BADGES = 'wildlife-sightings';
const GLOW = 'wildlife-sightings-density';
const CELL_LINE = 'wildlife-sightings-cell:';
const CELL_SET = 'wildlife-sightings-cells';
// One density cell per iNaturalist observation (Acartia has no grid).
const GLOW_CELLS = WILDLIFE_SIGHTINGS.filter((s) =>
s.id.startsWith('inat:'),
@@ -113,10 +113,10 @@ await strict.run({}, async (b) => {
.filter((set) => set.id.startsWith('wildlife-sightings'))
.map((set) => [set.id, { kind: set.kind, count: set.count }]),
);
const cells = engine.lines
const cells = engine.ground
.list()
.filter((line) => line.id.startsWith(prefix))
.map(({ points, shown }) => ({ points, shown }));
.filter((set) => set.id === prefix)
.map(({ kind, count, shown }) => ({ kind, count, shown }));
const badges = ids.map((id) => {
const position = window.qaWild.getSightingPosition(id);
const screen =
@@ -149,7 +149,7 @@ await strict.run({}, async (b) => {
return { sets, cells, badges };
},
[...byId.keys()],
CELL_LINE,
CELL_SET,
);
// Every badge in engine.movers and picked as itself, the glow drawn and
// the privacy cell outlined (the engines draw a few frames after the data).
@@ -164,7 +164,8 @@ await strict.run({}, async (b) => {
const ok =
drawn.sets[BADGES]?.count === byId.size &&
drawn.sets[GLOW]?.count === GLOW_CELLS &&
drawn.cells.length === CELLS.length &&
drawn.cells.length === 1 &&
drawn.cells[0].count === CELLS.length &&
drawn.badges.every(
(badge) => badge.picked?.id === `wildlife:${badge.id}`,
);
@@ -417,9 +418,10 @@ await strict.run({}, async (b) => {
await strict.check(
p,
LAYER,
'the obscured record’s privacy cell is a shown outline on engine.lines',
drawn.cells.length === CELLS.length &&
drawn.cells.every((line) => line.shown && line.points === 33),
'the obscured record’s privacy cell is a shown fill and outline on engine.ground',
drawn.cells.length === 1 &&
drawn.cells[0].count === CELLS.length &&
drawn.cells.every((set) => set.shown && set.kind === 'cell'),
drawn.cells,
);
console.log(
+36 -18
View File
@@ -836,7 +836,22 @@ test('selection subscriptions retain omitted rows and clear on disable and destr
unsubscribe();
});
test('privacy cells draw outlines on the engine, replace changed batches and release them', async () => {
function watchCells(engine) {
let current;
const createSet = engine.ground.createSet.bind(engine.ground);
engine.ground.createSet = (options) => {
const set = createSet(options);
const { replace } = set;
set.replace = (items) => {
replace(items);
current = { set, items };
};
return set;
};
return () => current;
}
test('privacy cells draw on ground, replace changed batches and release them', async () => {
let rows = [
{
...base,
@@ -845,29 +860,27 @@ test('privacy cells draw outlines on the engine, replace changed batches and rel
},
];
const h = harness({ getSnapshot: async () => rows });
const cells = watchCells(h.engine);
await h.update();
const first = h.engine.lines.list();
assert.equal(first.length, 1);
assert.equal(h.frameWork(), 0, 'outlines need no frame work');
const first = cells();
assert.equal(h.engine.ground.list().length, 1);
assert.equal(h.frameWork(), 0, 'static cells need no frame work');
rows = [{ ...rows[0], iconic: 'Fungi' }];
await h.update();
const second = h.engine.lines.list();
assert.equal(second.length, 1);
assert.notEqual(second[0].id, first[0].id, 'a new accent replaces the line');
assert.equal(h.engine.fake.line(first[0].id), undefined);
const second = cells();
assert.equal(h.engine.ground.list().length, 1);
assert.notEqual(second.set, first.set, 'a new accent replaces the geometry');
assert.notDeepEqual(second.items[0].fill, first.items[0].fill);
assert.throws(() => first.set.commit(), /destroyed/);
h.layer.selectSighting(base.stableId);
assert.equal(h.focus.at(-1).kind, 'sighting-area');
const selectedRing = h.badges()[0];
assert.deepEqual(selectedRing.color, [1, 1, 1, 1]);
assert.equal(selectedRing.scale, 1);
assert.match(h.badgeSet().options.sprite.atlas.url, /^data:image\/svg/);
assert.deepEqual(
h.engine.lines.list(),
second,
'selection reuses unchanged privacy geometry',
);
assert.equal(cells(), second, 'selection reuses unchanged privacy geometry');
h.layer.disable();
assert.deepEqual(h.engine.lines.list(), []);
assert.deepEqual(h.engine.ground.list(), []);
h.layer.destroy();
});
@@ -2185,6 +2198,7 @@ test('an engine switch keeps the data and selection and redraws the same badges
fetchedAt: NOW,
}),
});
const firstCells = watchCells(h.engine);
await h.update();
assert.equal(h.layer.selectSighting('inat:1'), true);
const drawn = (engine) =>
@@ -2221,12 +2235,13 @@ test('an engine switch keeps the data and selection and redraws the same badges
const beforeStyle = style(first);
const beforeGlow = glow(first);
assert.equal(beforeGlow.count, 1);
const outline = first.fake.line(first.lines.list()[0].id);
const cell = firstCells();
const stats = h.layer.getStats();
h.layer.detachRenderer();
assert.deepEqual(first.movers.list(), [], 'nothing stays drawn');
assert.deepEqual(first.lines.list(), []);
assert.deepEqual(first.ground.list(), []);
assert.equal(first.camera.moveEnd.numberOfListeners, 0);
assert.equal(first.frames.preRender.numberOfListeners, 0);
assert.equal(first.frames.postRender.numberOfListeners, 0);
@@ -2236,14 +2251,17 @@ test('an engine switch keeps the data and selection and redraws the same badges
const other = ENGINE_KINDS.find((kind) => kind !== h.engine.kind);
const next = h.globe(other);
const nextCells = watchCells(next);
assert.equal(h.layer.attachRenderer(null, { engine: next }), true);
h.flushIdle();
assert.deepEqual(drawn(next), before, 'the same badges, drawn the same');
assert.deepEqual(style(next), beforeStyle);
assert.deepEqual(glow(next), beforeGlow);
const redrawn = next.fake.line(next.lines.list()[0].id);
assert.deepEqual(redrawn.options.color, outline.options.color);
assert.deepEqual([...redrawn.positions], [...outline.positions]);
assert.deepEqual(
nextCells().items,
cell.items,
'same draped cell colours and bounds',
);
assert.equal(requests, 1, 'the retained view needs no new request');
assert.equal(h.layer.getStats().count, stats.count);
assert.equal(
+20 -68
View File
@@ -28,44 +28,9 @@ import {
export const WILDLIFE_BADGE_SET_ID = 'wildlife-sightings';
export const WILDLIFE_BADGE_ORDER =
SPRITE_LAYER_ORDER.indexOf('wildlife-sightings');
const CELL_LINE_PREFIX = 'wildlife-sightings-cell:';
const CELL_LINE_WIDTH_PX = 2;
// Points per privacy-cell edge, so long edges follow the surface.
const CELL_EDGE_STEPS = 8;
export const WILDLIFE_CELL_SET_ID = 'wildlife-sightings-cells';
/** The closed outline of a privacy cell at `height` metres, as flat ECEF. */
export function wildlifeCellOutline({ south, west, north, east }, height = 0) {
const corners = [
[west, south],
[east, south],
[east, north],
[west, north],
[west, south],
];
const flat = new Float64Array((4 * CELL_EDGE_STEPS + 1) * 3);
let at = 0;
const scratch = geo.cartesian3.create();
for (let edge = 0; edge < 4; edge++) {
const [lon0, lat0] = corners[edge];
const [lon1, lat1] = corners[edge + 1];
for (let step = 0; step < CELL_EDGE_STEPS + (edge === 3 ? 1 : 0); step++) {
const t = step / CELL_EDGE_STEPS;
geo.cartesian3.fromDegrees(
lon0 + (lon1 - lon0) * t,
lat0 + (lat1 - lat0) * t,
height,
undefined,
scratch,
);
flat[at++] = scratch.x;
flat[at++] = scratch.y;
flat[at++] = scratch.z;
}
}
return flat;
}
/** Own sighting badges, density glow and privacy-cell outlines on the engine. */
/** Own sighting badges, density glow and draped privacy cells on the engine. */
export function createRendering({ state, services, parts }) {
const badgeSprites = services.badgeSprites ?? wildlifeSpriteCache();
let batchDepth = 0;
@@ -73,7 +38,6 @@ export function createRendering({ state, services, parts }) {
let billboards = null;
let density = null;
let cellsSignature = null;
let cellLineSerial = 0;
let ambientAnchorIds = new Set();
const badgeScreens = new Map();
const crowdedBadgeIds = new Set();
@@ -179,14 +143,6 @@ export function createRendering({ state, services, parts }) {
point.show = show;
changed = true;
}
// A depth-free outline shows through the globe; keep only front ones.
for (const cell of state.cells) {
const shown = !horizon || horizon.isPointVisible(cell.center) === true;
if (cell.shown === shown) continue;
cell.shown = shown;
cell.line.show(shown);
changed = true;
}
if (changed) requestRender('cull');
return changed;
}
@@ -326,13 +282,12 @@ export function createRendering({ state, services, parts }) {
}
function clearCells() {
for (const cell of state.cells) cell.line.destroy();
for (const cell of state.cells) cell.destroy();
state.cells = [];
cellsSignature = null;
}
// Privacy cells as outlines in their taxon accent. Outlines sit at the
// cell's sampled ground height and draw through the scene, as the badges do.
// Privacy cells drape their taxon fill and outline on terrain and tiles.
function stageCells(rows) {
const cellsVisible = state.band === 'cells' || state.band === 'cards';
const unique = new Map();
@@ -349,25 +304,22 @@ export function createRendering({ state, services, parts }) {
clearCells();
cellsSignature = signature;
if (!unique.size) return true;
const horizon = occluder();
for (const row of unique.values()) {
const { south, west, north, east } = row.cell;
const lat = (south + north) / 2;
const lon = (west + east) / 2;
const floor = services.groundFloor?.cachedGroundFloor?.(lat, lon);
const height = Number.isFinite(floor) ? floor : 0;
const line = state.engine.lines.createLine({
id: `${CELL_LINE_PREFIX}${++cellLineSerial}`,
widthPx: CELL_LINE_WIDTH_PX,
color: rgbaFromCss(wildlifeAccent(row.iconic)).withAlpha(0.8).rgba(),
depthFailAlpha: 1,
});
line.setPositions(wildlifeCellOutline(row.cell, height));
const center = geo.cartesian3.fromDegrees(lon, lat, height);
const shown = !horizon || horizon.isPointVisible(center) === true;
if (!shown) line.show(false);
state.cells.push({ line, center, shown });
}
const set = state.engine.ground.createSet({
id: WILDLIFE_CELL_SET_ID,
kind: 'cell',
});
state.cells.push(set);
set.replace(
[...unique.values()].map((row) => {
const tint = rgbaFromCss(wildlifeAccent(row.iconic));
return {
...row.cell,
fill: tint.withAlpha(0.25).rgba(),
outline: { color: tint.withAlpha(0.8).rgba(), widthPx: 2 },
};
}),
);
set.commit();
return true;
}
+87 -54
View File
@@ -1,11 +1,16 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as geo from '../../geo/index.js';
import {
createFakeGroundHarness,
createCesiumGroundHarness,
} from '../../testSupport/groundDisplayEngines.mjs';
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import {
createRendering,
WILDLIFE_BADGE_ORDER,
WILDLIFE_BADGE_SET_ID,
WILDLIFE_CELL_SET_ID,
} from './rendering.js';
import { WILDLIFE_DENSITY_SET_ID } from './density.js';
import { wildlifeNearFarValue } from './display.js';
@@ -33,9 +38,12 @@ const latOf = (position) =>
(geo.cartographic.fromCartesian(position).latitude * 180) / Math.PI;
/** The real rendering on a fake engine above the fixture, 100 px a degree. */
function harness(rows = [base], { project, ...extra } = {}) {
const engine = createFakeEngine();
engine.fake.setPose({
function harness(rows = [base], { project, groundHarness, ...extra } = {}) {
const engine = groundHarness?.engine ?? createFakeEngine();
engine.camera.setView({
destination: geo.cartesian3.fromDegrees(base.lon, base.lat, 30_000),
});
engine.fake?.setPose({
longitude: degrees(base.lon),
latitude: degrees(base.lat),
height: 30_000,
@@ -44,7 +52,7 @@ function harness(rows = [base], { project, ...extra } = {}) {
roll: 0,
});
let projections = 0;
engine.fake.script('worldToWindow', (position) => {
engine.projector.worldToWindow = (position) => {
projections++;
return project
? project(position)
@@ -52,7 +60,7 @@ function harness(rows = [base], { project, ...extra } = {}) {
x: 100 + (lonOf(position) - base.lon) * 100,
y: 100 - (latOf(position) - base.lat) * 100,
};
});
};
let visible = true;
engine.projector.horizonVisible = () => visible;
const counts = { created: 0, removed: 0, cleared: 0, writes: [] };
@@ -446,56 +454,81 @@ test('a selected observation remains rendered outside the viewport and observati
h.rendering.destroy();
});
test('privacy cells draw accent outlines through the scene and release them', () => {
const cell = { south: 47.6, west: -122.4, north: 47.8, east: -122.2 };
const h = harness([{ ...base, obscured: true, cell }], {
groundFloor: { cachedGroundFloor: () => 250 },
for (const make of [createFakeGroundHarness, createCesiumGroundHarness]) {
test(`privacy cells drape accent fills and outlines and release them (${make.name})`, () => {
const cell = { south: 47.6, west: -122.4, north: 47.8, east: -122.2 };
const groundHarness = make();
const h = harness(
[
{ ...base, obscured: true, cell },
{ ...base, stableId: 'duplicate', obscured: true, cell },
],
{ groundHarness, groundFloor: { cachedGroundFloor: () => 250 } },
);
h.rendering.rebuild();
const set = groundHarness.set(WILDLIFE_CELL_SET_ID);
assert.equal(set.options.kind, 'cell');
assert.equal(set.options.drape, 'both');
assert.equal(set.options.pickable, false);
assert.equal(set.items.length, 1, 'same bounds and taxon share a cell');
const item = set.items[0];
assert.deepEqual(
{
south: item.south,
west: item.west,
north: item.north,
east: item.east,
},
cell,
);
const tint = ICONIC_TINTS.Aves;
const rgb = [1, 3, 5].map((offset) =>
parseInt(tint.slice(offset, offset + 2), 16),
);
assert.deepEqual(
item.fill.map((v) => Math.round(v * 255)),
[...rgb, Math.round(0.25 * 255)],
);
assert.deepEqual(
item.outline.color.map((v) => Math.round(v * 255)),
[...rgb, Math.round(0.8 * 255)],
);
assert.equal(item.outline.widthPx, 2);
assert.equal(item.pickId, undefined, 'picks pass through the cell');
assert.equal(set.shown, true);
assert.deepEqual(h.engine.lines.list(), [], 'ground owns the outline');
if (groundHarness.cesium) {
const { fills, lines } = groundHarness.primitives(WILDLIFE_CELL_SET_ID);
assert.equal(fills.length, 1);
assert.equal(lines.length, 1);
assert.equal(fills[0].options.allowPicking, false);
assert.equal(lines[0].options.allowPicking, false);
}
h.rendering.rebuild();
assert.equal(
groundHarness.set(WILDLIFE_CELL_SET_ID).commits,
1,
'unchanged cells keep their geometry',
);
h.visible(false);
h.rendering.cull();
assert.equal(
groundHarness.set(WILDLIFE_CELL_SET_ID).shown,
true,
'ground handles horizon occlusion',
);
h.visible(true);
h.state.band = 'regional';
h.rendering.rebuild();
assert.deepEqual(h.engine.ground.list(), [], 'wide views draw no cells');
h.state.band = 'cards';
h.rendering.rebuild();
assert.equal(h.engine.ground.list().length, 1);
h.rendering.destroy();
assert.deepEqual(h.engine.ground.list(), []);
groundHarness.destroy();
});
h.rendering.rebuild();
const lines = h.engine.lines.list();
assert.equal(lines.length, 1);
const line = h.engine.fake.line(lines[0].id);
assert.equal(line.options.widthPx, 2);
assert.equal(line.options.depthFailAlpha, 1);
const tint = ICONIC_TINTS.Aves;
assert.deepEqual(
line.color.map((value) => Math.round(value * 255)),
[
parseInt(tint.slice(1, 3), 16),
parseInt(tint.slice(3, 5), 16),
parseInt(tint.slice(5, 7), 16),
Math.round(0.8 * 255),
],
);
const flat = line.positions;
const first = geo.cartesian3.create(flat[0], flat[1], flat[2]);
const last = geo.cartesian3.create(...flat.slice(-3));
assert.ok(geo.cartesian3.distance(first, last) < 1e-6, 'a closed ring');
assert.ok(Math.abs(lonOf(first) - cell.west) < 1e-9);
assert.ok(Math.abs(latOf(first) - cell.south) < 1e-9);
assert.ok(
Math.abs(geo.cartographic.fromCartesian(first).height - 250) < 1e-3,
'at the sampled ground height',
);
assert.equal(line.shown, true);
// An unchanged cell set keeps its lines.
h.rendering.rebuild();
assert.deepEqual(h.engine.lines.list(), lines);
h.visible(false);
h.rendering.cull();
assert.equal(line.shown, false, 'outlines behind the globe hide');
h.visible(true);
h.rendering.cull();
assert.equal(line.shown, true);
h.state.band = 'regional';
h.rendering.rebuild();
assert.deepEqual(h.engine.lines.list(), [], 'wide views draw no cells');
h.state.band = 'cards';
h.rendering.rebuild();
assert.equal(h.engine.lines.list().length, 1);
h.rendering.destroy();
assert.deepEqual(h.engine.lines.list(), []);
});
}
test('font readiness swaps the atlas in place without badge recreation or position work', () => {
const listeners = new Set();