Fix the follow-up review findings in import, layers, startup and engines

- KML/KMZ import: a per-file canvas budget and cache for ground overlays,
  limits on rings and vertices of raised polygons, overlays across 360° and
  near the poles, degenerate holes, polar and single-latitude fills,
  absolute overlays at their altitude, overlay order by drawOrder as on
  Cesium, and pins on tiles keep their measured height.
- Points on the surface: OSM Places and Street Level cones share the
  surface sampler and sit on the terrain before they are measured, within
  a sampling budget per pass; Cockpit's contact-dot bank keeps the dot
  atlas, and a failed fleet model loads again after a pause.
- Startup: the loading screen lifts after the share-link view is restored,
  the retry screen names the failed engine and takes focus, and the
  fallback toast stays for 6 s.
- Engines: an engine switch lands a running camera flight at its
  destination (camera.completeFlight on both engines), the sighting card
  leaves focus on the engine picker after a switch, and a Cesium model
  error stays with the instance that failed.
- The MCP panel builds again: its wind-mesh alias resolves to the stub.
- QA: the wildlife switch waits for the camera to rest, GBFS --w2 says it
  needs the dev server, and test fixtures are shared instead of copied.
This commit is contained in:
German Khokhlov committed 2026-11-10 01:57:11 +04:00
1 parent 0ef29781e5
commit 60fb8fee07
82 files changed
+2363 -1574

No files matched your search

+5 -6
View File
@@ -18,11 +18,11 @@ const browserGlobals = new Set([
/**
* Inspect literal module dependencies and browser-platform references without
* executing code. With `dynamic`, also list the import() specifiers apart.
* executing code. With `dynamic`, also list the static (not import())
* specifiers apart.
*/
export function analyzeModule(source, { dynamic = false, filename = '' } = {}) {
const imports = [];
const dynamicImports = [];
const staticImports = [];
const browser = new Set();
function walk(node, parent, field) {
@@ -41,9 +41,8 @@ export function analyzeModule(source, { dynamic = false, filename = '' } = {}) {
'Computed module imports are not allowed in runtime code',
);
imports.push(node.source.value);
if (node.type === 'ImportExpression')
dynamicImports.push(node.source.value);
else staticImports.push(node.source.value);
if (node.type !== 'ImportExpression')
staticImports.push(node.source.value);
}
if (node.type === 'CallExpression' && node.callee?.name === 'require')
throw new Error('Runtime modules must use ES imports');
@@ -86,7 +85,7 @@ export function analyzeModule(source, { dynamic = false, filename = '' } = {}) {
return {
imports,
browser: [...browser],
...(dynamic && { dynamicImports, staticImports }),
...(dynamic && { staticImports }),
};
}
+2
View File
@@ -47,6 +47,7 @@
"src/ui/views/index.js",
"src/ui/views/CockpitView.vue",
"src/ui/views/CommandDockView.vue",
"src/app/engineChoice.js",
"src/ui/views/ContextView.vue",
"src/ui/views/panels/RadioPanel.vue",
"src/ui/views/panels/SdrCard.vue",
@@ -2141,6 +2142,7 @@
"src/layers/shared/color.js",
"src/globe/cameraMath.js",
"src/layers/streetLevel/render/display.js",
"src/layers/shared/render/surfacePoints.js",
"src/globe/contract.js"
],
"external": ["pbf", "@mapbox/vector-tile", "mapillary-js"]
+8
View File
@@ -2,6 +2,7 @@
/**
* Keyless station-name regression against the real globe and bike-share tool.
* Run with a dev or QA preview server and an explicitly assigned --url.
* --w2 adds raster checks that import source modules: dev server only.
* GBFS documents are synthetic; no operator feed or provider key is required.
*/
import assert from 'node:assert/strict';
@@ -138,6 +139,13 @@ async function open(mode = 'normal') {
{ information, status, mode },
);
await page.goto(appUrl, { waitUntil: 'domcontentloaded' });
if (
extended &&
!(await page.evaluate(() =>
Boolean(document.querySelector('script[src="/@vite/client"]')),
))
)
throw new Error('--w2 requires a Vite dev server; omit --w2 for preview');
await page.waitForFunction(
() =>
window.__godsEyeView?.dataManager && window.__godsEyeView?.voiceCommands,
+50
View File
@@ -0,0 +1,50 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const source = readFileSync(
new URL('./qa-gbfs-names.mjs', import.meta.url),
'utf8',
);
const openSource = source.slice(
source.indexOf('async function open('),
source.indexOf('async function nextFrame('),
);
function fixture({ extended, dev }) {
const page = {
bringToFront: async () => {},
on() {},
setViewport: async () => {},
evaluateOnNewDocument: async () => {},
goto: async () => {},
evaluate: async () => dev,
waitForFunction: async () => {},
};
const dependencies = {
browser: { newPage: async () => page },
extended,
watchBrowserPage() {},
report: {},
appUrl: 'http://localhost:4000',
information: {},
status: {},
fromDegrees: () => ({}),
};
return new Function(
...Object.keys(dependencies),
`${openSource}\nreturn open;`,
)(...Object.values(dependencies));
}
test('preview rejects the optional dev-only raster checks before importing source modules', async () => {
await assert.rejects(
fixture({ extended: true, dev: false })(),
/--w2 requires a Vite dev server/,
);
});
test('normal preview checks and extended dev checks remain available', async () => {
await fixture({ extended: false, dev: false })();
await fixture({ extended: true, dev: true })();
});
+53 -16
View File
@@ -1,5 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { quantile, slope } from './qa-strict-session.mjs';
import { strictArguments, waitForCameraAtRest } from './qa-strict-harness.mjs';
@@ -46,31 +47,67 @@ test('the runner hands each scenario its layers and its manifest path', () => {
});
});
test('the camera is at rest once a flight has landed and it stays still', async () => {
// A selection's flight holds the inputs off; once they are on, the camera
// still moves one more sample before it stays put.
const poses = [0, 400, 900, 900, 1250, 1250, 1250];
const inputs = [false, false, false, false, true, true, true];
// A page whose camera answers one sampled position (x, metres) and input
// state per waitForFunction poll; `polls()` counts the polls taken.
function cameraPage(t, positions, inputs, globals = {}) {
let frame = 0;
const engine = {
camera: { pose: () => ({ position: { x: poses[frame], y: 0, z: 6.4e6 } }) },
camera: {
pose: () => ({ position: { x: positions[frame], y: 0, z: 6.4e6 } }),
},
input: { cameraInputsEnabled: () => inputs[frame] },
};
const previous = globalThis.window;
globalThis.window = { __godsEyeView: { engine } };
globalThis.window = { __godsEyeView: { engine }, ...globals };
t.after(() => {
globalThis.window = previous;
});
const page = {
evaluate: async (fn) => fn(),
async waitForFunction(fn) {
for (; frame < poses.length; frame++) if (fn()) return;
for (; frame < positions.length; frame++) if (fn()) return;
throw new Error('timed out');
},
};
try {
await waitForCameraAtRest(page);
// Not while the flight holds the inputs (sample 3 repeats a pose), nor
// while the camera moves (sample 4).
assert.equal(frame, 5);
} finally {
globalThis.window = previous;
}
return { page, polls: () => frame };
}
test('the camera is at rest once a flight has landed and it stays still', async (t) => {
// A selection's flight holds the inputs off; once they are on, the camera
// still moves one more sample before it stays put.
const { page, polls } = cameraPage(
t,
[0, 400, 900, 900, 1250, 1250, 1250],
[false, false, false, false, true, true, true],
);
await waitForCameraAtRest(page);
// Not while the flight holds the inputs (sample 3 repeats a pose), nor
// while the camera moves (sample 4).
assert.equal(polls(), 5);
});
test('qa-wildlife-switch waits for the focus flight even when the camera was already still', async (t) => {
// A rest the scenario recorded before the click is no rest of the flight.
const { page, polls } = cameraPage(
t,
[0, 0, 100, 500, 500],
[false, false, false, true, true],
{
__qaWildPose: { key: '0.0,0.0,6400000.0', at: performance.now() - 5000 },
},
);
const source = readFileSync(
new URL('./qa-wildlife-switch.mjs', import.meta.url),
'utf8',
);
const still = new Function(
'p',
'waitForCameraAtRest',
`${source.slice(
source.indexOf(' const still ='),
source.indexOf(' // What the engine draws'),
)}\nreturn still;`,
)(page, waitForCameraAtRest);
await still();
assert.equal(polls(), 4);
});
+2 -12
View File
@@ -10,6 +10,7 @@ import {
createStrict,
localPreview,
openPage,
waitForCameraAtRest,
waitForEngine,
} from './qa-strict-harness.mjs';
@@ -86,18 +87,7 @@ await strict.run({}, async (b) => {
},
fromDegrees(WILDLIFE_VIEW.lon, WILDLIFE_VIEW.lat, WILDLIFE_VIEW.alt),
);
// Until the camera has held still for a second (a focus flight is over).
const still = () =>
p.waitForFunction(
() => {
const pose = window.__godsEyeView.engine.camera.pose().position;
const key = [pose.x, pose.y, pose.z].map((v) => v.toFixed(1)).join();
if (window.__qaWildPose?.key !== key)
window.__qaWildPose = { key, at: performance.now() };
return performance.now() - window.__qaWildPose.at > 1000;
},
{ timeout: 30000, polling: 100 },
);
const still = () => waitForCameraAtRest(p, { timeout: 30000 });
// What the engine draws for the layer, and what its picker answers at each
// sighting's anchor.
const drawing = () =>