mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(transit): keep the real silhouettes under sensor presets, make the selected trail visible, put Transit in Movement (#596)
## Changes Follow-up to #587 after the owner's first look at the merged transit layer on Google 3D tiles. - **Sensor presets keep the real silhouettes.** Under NVG, thermal (white-hot, black-hot, Ironbow) and noir, vehicles are the same silhouettes as in normal view, filled opaque white with a 2 px dark halo, at the CRT display sizes (26 px fleet, 39 px selected). The solid rounded "signature" bodies are gone. - **The selected vehicle's trail is visible.** The retained history is now draped onto 3D tiles and terrain (`GroundPolylinePrimitive`, `ClassificationType.BOTH`, the way the traffic heat-lines are drawn), 3 px core over a 5 px dark backing, per-segment age colour, a short draped head clipped to the sprite. Two defects kept it off screen before: Cesium's per-instance attribute getters return copies, so the renderer's show/colour edits were discarded, and a marker did not recover after the camera jumped to it, which hid the trail with it. Trail geometry is now prepared on selection independent of floor resolution and marker visibility; sparse long intervals (rail) keep a bounded marker corridor instead of dropping the interval; classification survives map-source changes. - **Transit sits in the Movement panel group** between Street Traffic and Bike Share (the panel orders by `PANEL_GROUPS`, not by catalog order; the catalog is moved too). ## How it was verified - `npm run format:check` (854 files), `npm run check:boundaries`, `npm test` (4,062 passing), `npm run build` clean; tracking regression 109/109 in a real browser on Google 3D tiles. - Browser harness (`scripts/qa-transit.mjs`), keyed on Google 3D tiles and keyless: keyed 194 pass, with the trail pixel check passing at 300, 600 and 900 m; the remaining keyed failures are the per-frame allocation gate already recorded as a follow-up from #587 (CPU and frame time within budget), four readability lines at threshold edges, and the card-anchor check now counting frames where the selected marker is legitimately off screen; keyless 193 pass with the remaining misses being threshold edges on the thin tram silhouette (centre 0.83–0.88 against 0.85) and one bus ring 0.28 against 0.25 on a pure-white roof. - New harness checks: `Google 3D selected trail visible at 6 of 8 projected samples` (repeated off/on framebuffer captures at 300/600/900 m, samples outside the sprite footprint, a background-change negative control; fails on main, passes here), a camera-arrival check (select a moving vehicle, `setView` onto it, marker shown and trail body drawn within 2 s), `PANEL_ORDER` pinned, raster tests comparing every sensor raster's mask and aspect ratio to the normal silhouette. - Direct look on a keyed build: a live CapMetro bus at 30 m/s selected through the layer API draws its draped trail along the highway lane (harness capture `keyed-600m-trail-visible.jpg`). - Adversarial review by Codex Astra before the last two fix passes (stack-switch classification, marker corridor under the vertex cap, pixel-gate loophole), all three fixed with regressions. Docs: `docs/CURRENT-STATE.md` and `CHANGELOG.md` updated in this PR. No data-source changes.
This commit is contained in:
1 parent
cd22ca3559
commit
0bb61f53c6
24 files changed
+1945
-205
No files matched your search
@@ -2178,7 +2178,7 @@ test('under FLIR and NVG every sprite is white-hot with a dark halo, and selecti
|
||||
assert.notEqual(entry.marker.image, plainImage, 'a haloed raster');
|
||||
assert.match(
|
||||
Buffer.from(entry.marker.image.split(',')[1], 'base64').toString(),
|
||||
/#000000/,
|
||||
/#05080C/,
|
||||
'with the dark ring',
|
||||
);
|
||||
assert.ok(entry.marker.width > plainWidth * 1.2, 'and larger');
|
||||
@@ -2547,7 +2547,7 @@ test('selected trail and marker share revised start floors, with unknown history
|
||||
assert.ok(
|
||||
Cesium.Cartographic.fromCartesian(entry.marker.position).height > 30,
|
||||
);
|
||||
assert.ok(entry.trailVertices <= 2048);
|
||||
assert.ok(entry.trailVertices <= 640);
|
||||
parts.selection.clearSelection();
|
||||
assert.equal(parts.trails.diagnostics(), null);
|
||||
});
|
||||
@@ -2659,7 +2659,7 @@ test('a long selected history shortens its oldest geometry before sacrificing th
|
||||
parts.rendering.sampleIdle(entry);
|
||||
parts.selection.selectVehicle(entry.key);
|
||||
assert.equal(entry.trailTruncated, true);
|
||||
assert.ok(entry.trailVertices <= 2048);
|
||||
assert.ok(entry.trailVertices <= 640);
|
||||
assert.ok(
|
||||
entry.displayPaths.has(entry.sample.fromSeq),
|
||||
'active path is prepared first',
|
||||
@@ -2671,6 +2671,63 @@ test('a long selected history shortens its oldest geometry before sacrificing th
|
||||
);
|
||||
});
|
||||
|
||||
test('sparse rail reports preserve a bounded marker corridor with and without selection', async (t) => {
|
||||
const app = harness(t);
|
||||
const parts = app.layer._transitPartsForTest();
|
||||
const { seek } = await import('../../data/contactPlayback.js');
|
||||
let latitude = 42.36 - 16000 / 111320;
|
||||
let timestamp = reported();
|
||||
app.serve('mbta', () => ({
|
||||
status: 200,
|
||||
body: snapshot(
|
||||
'mbta',
|
||||
'MBTA',
|
||||
[vehicle('sparse', latitude, -71.06, timestamp, { routeId: 'CR-1' })],
|
||||
{ fetchedAt: Date.now() },
|
||||
),
|
||||
}));
|
||||
app.layer.enable(app.viewer);
|
||||
await app.layer.update();
|
||||
app.settle();
|
||||
app.advance(180000);
|
||||
latitude = 42.36;
|
||||
timestamp += 180;
|
||||
await app.layer.update();
|
||||
app.settle();
|
||||
const entry = app.vehicles()[0];
|
||||
assert.equal(entry.mode, 'rail');
|
||||
assert.equal(entry.track.count, 2, 'both 88.9 m/s reports pass admission');
|
||||
assert.equal(entry.fixes[1].t - entry.fixes[0].t, 180000);
|
||||
for (let i = 0; i < 1500; i++)
|
||||
app.ground._warm.set(app.ground._key(42.2 + i * 0.0002, -71.06), 12);
|
||||
seek(entry.track, entry.fixes[0].t + 90000, {
|
||||
wallNowMs: Date.now(),
|
||||
monoNowMs: performance.now(),
|
||||
});
|
||||
parts.rendering.sampleIdle(entry);
|
||||
for (const selected of [false, true, false]) {
|
||||
if (selected) parts.selection.selectVehicle(entry.key);
|
||||
else parts.selection.clearSelection();
|
||||
parts.trails.prepareEntry(entry);
|
||||
const path = entry.displayPaths.get(entry.sample.fromSeq);
|
||||
assert.ok(path, 'active corridor survives the body cap');
|
||||
assert.ok(path.positions.length <= 640);
|
||||
const position = parts.trails.samplePosition(
|
||||
entry,
|
||||
entry.sample,
|
||||
new Cesium.Cartesian3(),
|
||||
true,
|
||||
);
|
||||
assert.ok(
|
||||
position && [position.x, position.y, position.z].every(Number.isFinite),
|
||||
);
|
||||
parts.rendering.placeSample(entry, true);
|
||||
assert.equal(entry.marker.show, true);
|
||||
assert.ok(Cesium.Cartesian3.equals(entry.marker.position, position));
|
||||
assert.ok(entry.trailVertices <= 640);
|
||||
}
|
||||
});
|
||||
|
||||
test('missing rectangle still rejects the far side, and frustum rejection wins', async (t) => {
|
||||
const app = harness(t, { altitude: 100_000 });
|
||||
app.viewer.camera.computeViewRectangle = () => undefined;
|
||||
@@ -2783,13 +2840,13 @@ test('every styling path retains the mode palette and exact unpadded size', asyn
|
||||
parts.rendering.paintMode(entry, 'subway');
|
||||
check('#FF4538', 30);
|
||||
app.style('thermal');
|
||||
check('#FFFFFF', 36);
|
||||
check('#FFFFFF', 39);
|
||||
parts.selection.clearSelection();
|
||||
check('#FFFFFF', 30);
|
||||
check('#FFFFFF', 26);
|
||||
app.vision('nvg', true);
|
||||
check('#FFFFFF', 30);
|
||||
check('#FFFFFF', 26);
|
||||
parts.selection.selectVehicle(entry.key);
|
||||
check('#FFFFFF', 36);
|
||||
check('#FFFFFF', 39);
|
||||
app.vision('normal', false);
|
||||
check('#FF4538', 30);
|
||||
parts.selection.clearSelection();
|
||||
@@ -3673,3 +3730,131 @@ test('transit polls the supplied source and cancels it when disabled', async (t)
|
||||
await app.layer.update();
|
||||
assert.equal(calls.length, before);
|
||||
});
|
||||
|
||||
test('selected trail QA fixture survives repeated live CapMetro snapshots', async (t) => {
|
||||
const { TRAIL_VISIBILITY_FEED } =
|
||||
await import('../../../scripts/qa-transit-scenes.mjs');
|
||||
const { getRegisteredTransitFeed } =
|
||||
await import('../../data/transitFeeds.js');
|
||||
const app = harness(t, { at: { lat: 30.267, lon: -97.7431 } });
|
||||
app.layer.enable(app.viewer);
|
||||
const { ingestion, selection } = app.layer._transitPartsForTest();
|
||||
const now = Date.now();
|
||||
ingestion.applySnapshot(
|
||||
TRAIL_VISIBILITY_FEED,
|
||||
{
|
||||
fetchedAt: now,
|
||||
vehicles: [vehicle('trail-visible', 30.267, -97.7431, now / 1000)],
|
||||
},
|
||||
{ stale: false },
|
||||
);
|
||||
const key = `${TRAIL_VISIBILITY_FEED.id}:trail-visible`;
|
||||
selection.selectVehicle(key);
|
||||
assert.equal(app.state()._selectedKey, key);
|
||||
for (let poll = 0; poll <= MISSED_POLLS_TO_DROP; poll++) {
|
||||
ingestion.applySnapshot(
|
||||
getRegisteredTransitFeed('capmetro-austin'),
|
||||
{
|
||||
fetchedAt: now + poll,
|
||||
vehicles: [vehicle('live-bus', 30.268, -97.7431, now / 1000)],
|
||||
},
|
||||
{ stale: false },
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
app.state()._vehicles.has(key),
|
||||
'live polls must not evict the selected fixture',
|
||||
);
|
||||
assert.equal(app.state()._selectedKey, key);
|
||||
assert.ok(
|
||||
app.state()._vehicles.has('capmetro-austin:live-bus'),
|
||||
'live ingestion still runs',
|
||||
);
|
||||
});
|
||||
|
||||
for (const jump of ['setView', 'flyTo']) {
|
||||
test(`${jump} recovers a cold hidden vehicle within one visibility interval`, async (t) => {
|
||||
const app = harness(t, { floorAt: () => undefined });
|
||||
let floor;
|
||||
app.viewer.scene.globe.show = true;
|
||||
app.viewer.scene.globe.getHeight = () => floor;
|
||||
app.serve('mbta', () => ({
|
||||
status: 200,
|
||||
body: snapshot('mbta', 'MBTA', [
|
||||
vehicle('cold', 42.36, -71.06, reported()),
|
||||
]),
|
||||
}));
|
||||
app.layer.enable(app.viewer);
|
||||
await app.layer.update();
|
||||
const entry = app.vehicles()[0];
|
||||
assert.equal(entry.marker.show, false);
|
||||
assert.equal(entry.heightPending, true);
|
||||
// Surface tiles are now loaded at the camera destination. No poll, floor
|
||||
// timer or preRender is allowed to bootstrap the visibility recovery.
|
||||
clearTimeout(app.state()._floorTimer);
|
||||
app.state()._floorTimer = null;
|
||||
app.viewer.camera[jump] = () => {
|
||||
floor = 12;
|
||||
app.viewer.camera.changed.raiseEvent();
|
||||
};
|
||||
app.viewer.camera[jump]();
|
||||
app.advance(250);
|
||||
assert.equal(entry.marker.show, true);
|
||||
assert.equal(entry.heightPending, false);
|
||||
assert.equal(entry.surfaceReady, true);
|
||||
assert.equal(entry.visibility.heightPending, false);
|
||||
assert.equal(entry.visibility.surfaceReady, true);
|
||||
});
|
||||
}
|
||||
|
||||
test('selected history is prepared while its head is hidden and remains shown outside the head frustum', async (t) => {
|
||||
const app = harness(t);
|
||||
app.layer.enable(app.viewer);
|
||||
app.layer._loadTransitFleetForTest(1, BOSTON);
|
||||
const entry = app.vehicles()[0],
|
||||
parts = app.layer._transitPartsForTest();
|
||||
entry.marker.show = false;
|
||||
app.state()._visible.delete(entry);
|
||||
app.state()._moving.delete(entry);
|
||||
parts.selection.selectVehicle(entry.key);
|
||||
const trail = parts.trails.diagnostics();
|
||||
assert.ok(trail.segments > 0);
|
||||
assert.ok(trail.body);
|
||||
assert.equal(trail.body.show, true);
|
||||
parts.trails.update();
|
||||
assert.equal(trail.body.show, true);
|
||||
app.state()._vehicles.delete(entry.key);
|
||||
parts.trails.update();
|
||||
assert.equal(trail.body.show, false);
|
||||
app.state()._vehicles.set(entry.key, entry);
|
||||
});
|
||||
|
||||
test('selected draped history prepares geometry before surface heights resolve', async (t) => {
|
||||
const app = harness(t, { floorAt: () => undefined });
|
||||
app.layer.enable(app.viewer);
|
||||
app.layer._loadTransitFleetForTest(1, BOSTON);
|
||||
const entry = app.vehicles()[0],
|
||||
parts = app.layer._transitPartsForTest();
|
||||
delete entry.qaFloorM;
|
||||
entry.marker.show = false;
|
||||
entry.heightPending = true;
|
||||
// Exercise the real GroundPolylinePrimitive branch, without a WebGL context.
|
||||
const width = Cesium.ContextLimits._maximumAliasedLineWidth;
|
||||
Cesium.ContextLimits._maximumAliasedLineWidth = 1;
|
||||
t.after(() => {
|
||||
Cesium.ContextLimits._maximumAliasedLineWidth = width;
|
||||
});
|
||||
app.viewer.scene.frameState = { context: { depthTexture: true } };
|
||||
app.viewer.scene.groundPrimitives = app.viewer.scene.primitives;
|
||||
parts.selection.selectVehicle(entry.key);
|
||||
const trail = parts.trails.diagnostics();
|
||||
assert.equal(trail.ground, true);
|
||||
assert.ok(trail.body instanceof Cesium.GroundPolylinePrimitive);
|
||||
assert.ok(trail.segments > 0);
|
||||
assert.equal(trail.body.show, true);
|
||||
assert.equal(
|
||||
entry.displayPaths.size,
|
||||
0,
|
||||
'unresolved marker corridors remain unavailable',
|
||||
);
|
||||
});
|
||||
@@ -329,3 +329,75 @@ export function reduceTrailHead(anchor, target) {
|
||||
selection: anchor?.selection ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Repeated off/on patches isolate stable, trail-correlated framebuffer changes. */
|
||||
export function reduceTrailPixels(on, off, evidence = {}) {
|
||||
const { onAgain, offAgain, samples = [], sprite, tilesReady } = evidence;
|
||||
const finitePoint = (p) =>
|
||||
p && [p.x, p.y, p.radius].every(Number.isFinite) && p.radius > 0;
|
||||
const stable = [],
|
||||
eligible = [];
|
||||
const hits = Array.from({ length: 8 }, (_, i) => {
|
||||
const point = samples[i];
|
||||
eligible[i] = !!(
|
||||
tilesReady === true &&
|
||||
finitePoint(point) &&
|
||||
point.inFront === true &&
|
||||
finitePoint(sprite) &&
|
||||
Math.hypot(point.x - sprite.x, point.y - sprite.y) >
|
||||
point.radius + sprite.radius &&
|
||||
samples
|
||||
.slice(0, i)
|
||||
.every(
|
||||
(prior) =>
|
||||
!finitePoint(prior) ||
|
||||
Math.hypot(point.x - prior.x, point.y - prior.y) >
|
||||
point.radius + prior.radius,
|
||||
)
|
||||
);
|
||||
const a = on?.[i],
|
||||
b = off?.[i],
|
||||
c = onAgain?.[i],
|
||||
d = offAgain?.[i];
|
||||
stable[i] = false;
|
||||
if (
|
||||
!eligible[i] ||
|
||||
!a?.length ||
|
||||
a.length % 4 ||
|
||||
[b, c, d].some((p) => !p || p.length !== a.length)
|
||||
)
|
||||
return false;
|
||||
let matches = 0;
|
||||
const contrast = (lit, baseline, k) => {
|
||||
const delta = Math.hypot(
|
||||
lit[k] - baseline[k],
|
||||
lit[k + 1] - baseline[k + 1],
|
||||
lit[k + 2] - baseline[k + 2],
|
||||
);
|
||||
const green = lit[k + 1] > lit[k] + 30 && lit[k + 1] > lit[k + 2] + 18;
|
||||
const dark =
|
||||
lit[k] + lit[k + 1] + lit[k + 2] <
|
||||
0.6 * (baseline[k] + baseline[k + 1] + baseline[k + 2]);
|
||||
return delta >= 40 && (green || dark);
|
||||
};
|
||||
for (let k = 0; k < a.length; k += 4) {
|
||||
for (let channel = 0; channel < 3; channel++) {
|
||||
const j = k + channel;
|
||||
if (
|
||||
![a[j], b[j], c[j], d[j]].every(
|
||||
(v) => Number.isFinite(v) && v >= 0 && v <= 255,
|
||||
) ||
|
||||
Math.abs(b[j] - d[j]) > 8 ||
|
||||
Math.abs(a[j] - c[j]) > 8 ||
|
||||
Math.abs(a[j] - b[j] - (c[j] - d[j])) > 8
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (contrast(a, b, k) && contrast(c, d, k)) matches++;
|
||||
}
|
||||
stable[i] = true;
|
||||
return matches >= 2;
|
||||
});
|
||||
const present = hits.filter(Boolean).length;
|
||||
return { hits, stable, eligible, present, total: 8, pass: present >= 6 };
|
||||
}
|
||||
@@ -328,3 +328,106 @@ test('trail-head acceptance names every missing and failed condition', async ()
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
const trailPatch = (rgb) =>
|
||||
Array.from({ length: 49 }, () => [...rgb, 255]).flat();
|
||||
const trailEvidence = (on, off) => ({
|
||||
onAgain: on,
|
||||
offAgain: off,
|
||||
tilesReady: true,
|
||||
samples: Array.from({ length: 8 }, (_, i) => ({
|
||||
x: i * 20 + 20,
|
||||
y: 20,
|
||||
radius: 3,
|
||||
inFront: true,
|
||||
})),
|
||||
sprite: { x: 300, y: 20, radius: 24 },
|
||||
});
|
||||
|
||||
test('trail pixels require repeatable contrast at six distinct samples outside the sprite', async () => {
|
||||
const { reduceTrailPixels } = await import('./qaMetrics.js');
|
||||
const off = Array.from({ length: 8 }, () => trailPatch([150, 150, 150]));
|
||||
const on = off.map((p, i) => (i < 6 ? trailPatch([94, 240, 138]) : p));
|
||||
const evidence = trailEvidence(on, off);
|
||||
assert.equal(reduceTrailPixels(on, off, evidence).pass, true);
|
||||
const five = on.map((p, i) => (i === 5 ? off[i] : p));
|
||||
assert.equal(
|
||||
reduceTrailPixels(five, off, trailEvidence(five, off)).pass,
|
||||
false,
|
||||
);
|
||||
for (const rgb of [
|
||||
[5, 8, 12],
|
||||
[126, 126, 126],
|
||||
]) {
|
||||
const patches = off.map(() => trailPatch(rgb));
|
||||
assert.equal(
|
||||
reduceTrailPixels(patches, off, trailEvidence(patches, off)).present,
|
||||
rgb[0] === 5 ? 8 : 0,
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(patches, patches, trailEvidence(patches, patches))
|
||||
.present,
|
||||
0,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off).pass,
|
||||
false,
|
||||
'two reads are insufficient',
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, { ...evidence, tilesReady: false }).pass,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, {
|
||||
...evidence,
|
||||
samples: Array(8).fill(evidence.samples[0]),
|
||||
}).pass,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, {
|
||||
...evidence,
|
||||
sprite: { x: 80, y: 20, radius: 200 },
|
||||
}).pass,
|
||||
false,
|
||||
);
|
||||
assert.equal(reduceTrailPixels([], [], trailEvidence([], [])).pass, false);
|
||||
});
|
||||
|
||||
test('background refinement without a trail cannot pass repeated off/on acceptance', async () => {
|
||||
const { reduceTrailPixels } = await import('./qaMetrics.js');
|
||||
const grey = (n) => Array.from({ length: 8 }, () => trailPatch([n, n, n]));
|
||||
const off = grey(100),
|
||||
on = grey(30);
|
||||
const evidence = trailEvidence(on, off);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, { ...evidence, offAgain: grey(30) }).pass,
|
||||
false,
|
||||
'one background transition is not a trail',
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, {
|
||||
...evidence,
|
||||
offAgain: grey(170),
|
||||
onAgain: grey(100),
|
||||
}).pass,
|
||||
false,
|
||||
'equal deltas on a drifting baseline are not a trail',
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(on, off, { ...evidence, onAgain: off }).pass,
|
||||
false,
|
||||
'contrast must repeat',
|
||||
);
|
||||
const first = off.map((p) => p.map((v, i) => (i < 8 ? on[0][i] : v)));
|
||||
const second = off.map((p) =>
|
||||
p.map((v, i) => (i >= 8 && i < 16 ? on[0][i] : v)),
|
||||
);
|
||||
assert.equal(
|
||||
reduceTrailPixels(first, off, { ...evidence, onAgain: second }).pass,
|
||||
false,
|
||||
'the same pixels must change twice',
|
||||
);
|
||||
});
|
||||
@@ -97,6 +97,7 @@ export function createRendering({ state, services, parts }) {
|
||||
nearGround,
|
||||
);
|
||||
entry.surfaceReady = !!known;
|
||||
entry.heightPending = !known;
|
||||
entry.marker.show = !!known && vehicleInView(entry);
|
||||
if (known) {
|
||||
entry.marker.position = state._scratchCartesian;
|
||||
@@ -523,6 +524,10 @@ export function createRendering({ state, services, parts }) {
|
||||
else state._visible.delete(entry);
|
||||
if (visible && entry.track?.count) {
|
||||
if (!wasVisible && !entry.qaFixture) syncPlayback(entry, now);
|
||||
// Re-entry can advance into an unprepared corridor; loaded tiles can
|
||||
// also resolve a cold path without a poll or a render-loop wakeup.
|
||||
if (!wasVisible || entry.surfaceReady === false || entry.heightPending)
|
||||
parts.trails.prepareEntry(entry);
|
||||
// Surface completion must recover while requestRenderMode is idle.
|
||||
// Otherwise surfaceReady=false prevents the very frame that clears it.
|
||||
if (!state._moving.has(entry)) {
|
||||
@@ -532,6 +537,8 @@ export function createRendering({ state, services, parts }) {
|
||||
}
|
||||
entry.marker.show =
|
||||
visible && !entry.heightPending && entry.surfaceReady !== false;
|
||||
visibility.heightPending = !!entry.heightPending;
|
||||
visibility.surfaceReady = entry.surfaceReady ?? null;
|
||||
schedulePlayback(entry);
|
||||
changed ||= visible !== wasVisible || wasShown !== entry.marker.show;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ const MODE_COLOR = {
|
||||
ferry: '#5FD6FF',
|
||||
unknown: '#D8DDE5',
|
||||
};
|
||||
export const TRAIL_VERTEX_LIMIT = 2048;
|
||||
// Final Cesium batching uses 3,152 bytes per two-lane edge.
|
||||
// The 1,278-instance body plus 64 bytes/instance reserve stays below 2 MiB.
|
||||
export const TRAIL_VERTEX_LIMIT = 640;
|
||||
const STEP_M = 25;
|
||||
|
||||
/** Shared prepared paths. Floors are aligned to work with Google 3D tiles. */
|
||||
@@ -63,6 +65,7 @@ export function createTrails({ state, services, parts, source }) {
|
||||
entry.trailSegments = [];
|
||||
entry.trailVertices = 0;
|
||||
}
|
||||
const draped = entry === selected && ensureRenderer()?.diagnostics().ground;
|
||||
const paths = new Map(),
|
||||
all = [],
|
||||
a = {},
|
||||
@@ -113,16 +116,28 @@ export function createTrails({ state, services, parts, source }) {
|
||||
if (entry === selected)
|
||||
for (let i = activeIndex - 1; i >= 0; i--) order.push(i);
|
||||
entry.trailTruncated = false;
|
||||
let markerVertices = 0;
|
||||
let markerIntervals = Math.max(0, entry.track.count - 1 - activeIndex);
|
||||
for (const i of order) {
|
||||
readFix(entry.track, i, a);
|
||||
readFix(entry.track, i + 1, b);
|
||||
const markerCorridor = i >= activeIndex;
|
||||
if (markerCorridor) markerIntervals--;
|
||||
if (a.epoch !== b.epoch || b.flags & FIX_FLAGS.BREAK) continue;
|
||||
if (entry !== selected && b.t < entry.sample.displayT) continue;
|
||||
const n = Math.max(1, Math.ceil(fixDistanceM(a, b) / STEP_M));
|
||||
if (vertices + n + 1 > TRAIL_VERTEX_LIMIT) {
|
||||
entry.trailTruncated = true;
|
||||
continue;
|
||||
}
|
||||
const desired = Math.max(1, Math.ceil(fixDistanceM(a, b) / STEP_M));
|
||||
// Reserve endpoints for every future marker interval. Sparse reports
|
||||
// coarsen the active path instead of losing the vehicle to the body cap.
|
||||
const n = markerCorridor
|
||||
? Math.min(
|
||||
desired,
|
||||
TRAIL_VERTEX_LIMIT - markerVertices - 2 * markerIntervals - 1,
|
||||
)
|
||||
: desired;
|
||||
const retainBody = vertices + n + 1 <= TRAIL_VERTEX_LIMIT;
|
||||
if (!retainBody || n < desired) entry.trailTruncated = true;
|
||||
if (!markerCorridor && !retainBody) continue;
|
||||
if (markerCorridor) markerVertices += n + 1;
|
||||
const positions = [],
|
||||
heights = [];
|
||||
let valid = true;
|
||||
@@ -136,17 +151,21 @@ export function createTrails({ state, services, parts, source }) {
|
||||
heights.push(h);
|
||||
if (h === null) {
|
||||
valid = false;
|
||||
positions.push(null);
|
||||
// Ground polylines consume longitude/latitude and drape themselves.
|
||||
// Unknown floors still disqualify the marker corridor.
|
||||
positions.push(
|
||||
draped ? Cesium.Cartesian3.fromDegrees(point.lon, point.lat) : null,
|
||||
);
|
||||
} else
|
||||
positions.push(
|
||||
Cesium.Cartesian3.fromDegrees(point.lon, point.lat, h),
|
||||
);
|
||||
}
|
||||
signature += `${a.seq}/${b.seq}:${heights.join(',')};`;
|
||||
vertices += n + 1;
|
||||
if (!valid) continue;
|
||||
paths.set(a.seq, { positions, toSeq: b.seq });
|
||||
if (entry === selected && fixDistanceM(a, b) > 0) {
|
||||
if (retainBody) vertices += n + 1;
|
||||
if (valid) paths.set(a.seq, { positions, toSeq: b.seq });
|
||||
if (!valid && !draped) continue;
|
||||
if (entry === selected && retainBody && fixDistanceM(a, b) > 0) {
|
||||
for (let k = 0; k < n; k++)
|
||||
all.push({
|
||||
fromSeq: a.seq,
|
||||
@@ -216,7 +235,9 @@ export function createTrails({ state, services, parts, source }) {
|
||||
}
|
||||
function update() {
|
||||
if (!selected || !renderer) return;
|
||||
renderer.setVisible(state._enabled && selected.marker?.show !== false);
|
||||
renderer.setVisible(
|
||||
state._enabled && state._vehicles.get(selected.key) === selected,
|
||||
);
|
||||
renderer.setDisplaySample(selected.sample, selected.marker?.position);
|
||||
}
|
||||
function select(entry) {
|
||||
|
||||
Reference in new issue
Block a user