feat(globe): port to CeliumJS
This commit is contained in:
parent
ec03425067
commit
eb7698e034
51 changed files with 3521 additions and 1992 deletions
144
tests/e2e/globe.spec.ts
Normal file
144
tests/e2e/globe.spec.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { test, expect, login, sceneObjectCount } from './fixtures';
|
||||
|
||||
/**
|
||||
* Regression guard for the Mercator -> globe migration.
|
||||
*
|
||||
* The MapLibre implementation tiled every GeoJSON source through
|
||||
* @maplibre/geojson-vt, whose projectY() clamps its result to [0,1] — so any
|
||||
* vertex above 85.051129° collapsed onto that parallel and a polar trajectory
|
||||
* rendered as a straight line along it. Cesium converts degrees straight to
|
||||
* Cartesian3, so latitude must survive intact. If a Mercator-tiled render path
|
||||
* is ever reintroduced, this test fails.
|
||||
*/
|
||||
|
||||
const MERCATOR_LIMIT = 85.051129;
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await login(context);
|
||||
});
|
||||
|
||||
test('globe preserves latitude above the Mercator limit', async ({ page }) => {
|
||||
// Cesium ships ~7 MB of workers/assets; first paint is slower than MapLibre.
|
||||
test.setTimeout(90_000);
|
||||
|
||||
await page.goto('/predict');
|
||||
await page
|
||||
.locator('.map-container canvas')
|
||||
.first()
|
||||
.waitFor({ state: 'attached', timeout: 60_000 });
|
||||
|
||||
// Map.svelte exposes the raw Cesium Viewer on window._lsvMap in dev builds.
|
||||
await page.waitForFunction(
|
||||
() => (window as unknown as { _lsvMap?: { scene?: unknown } })._lsvMap?.scene !== undefined,
|
||||
undefined,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
const latitudes: number[] = await page.evaluate((limit) => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const viewer = (window as any)._lsvMap;
|
||||
// Cesium is an ES module, not a global. Reach its constructors through
|
||||
// live objects the Viewer already holds: camera.position is a Cartesian3
|
||||
// and positionCartographic is a Cartographic, both carrying the statics.
|
||||
const Cartesian3: any = viewer.camera.position.constructor;
|
||||
const Cartographic: any = viewer.camera.positionCartographic.constructor;
|
||||
|
||||
const wanted = [89.0, limit + 0.001, 89.99, 90.0];
|
||||
const positions = Cartesian3.fromDegreesArray(wanted.flatMap((lat: number) => [10, lat]));
|
||||
viewer.entities.add({ polyline: { positions, width: 3 } });
|
||||
|
||||
return positions.map(
|
||||
(p: unknown) => (Cartographic.fromCartesian(p).latitude * 180) / Math.PI,
|
||||
);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}, MERCATOR_LIMIT);
|
||||
|
||||
expect(latitudes).toHaveLength(4);
|
||||
// Every vertex round-trips to the latitude it was given.
|
||||
expect(latitudes[0]).toBeCloseTo(89.0, 6);
|
||||
expect(latitudes[1]).toBeCloseTo(MERCATOR_LIMIT + 0.001, 6);
|
||||
expect(latitudes[2]).toBeCloseTo(89.99, 6);
|
||||
expect(latitudes[3]).toBeCloseTo(90.0, 6);
|
||||
// And none got pinned to the old Mercator ceiling.
|
||||
for (const lat of latitudes.slice(1)) {
|
||||
expect(lat).toBeGreaterThan(MERCATOR_LIMIT);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The flight_path tuples carry altitude as an optional third element
|
||||
* (domain/geo.ts: LatLngTuple), filled in by parsePrediction. MapLibre could
|
||||
* not use it, so it sat unread; on a globe it must become real geometry —
|
||||
* otherwise a 30 km balloon flight renders as a flat line on the ground.
|
||||
*/
|
||||
test.describe('altitude', () => {
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await login(context);
|
||||
});
|
||||
|
||||
/** Heights (metres) of every vertex of the rendered workspace path. */
|
||||
function pathHeights(page: import('@playwright/test').Page): Promise<number[]> {
|
||||
return page.evaluate(() => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const v: any = (window as any)._lsvMap;
|
||||
const C: any = v.camera.positionCartographic.constructor;
|
||||
const e = v.entities.values.find((x: any) => String(x.id).endsWith('__path'));
|
||||
if (!e) return [];
|
||||
return e.polyline.positions
|
||||
.getValue(v.clock.currentTime)
|
||||
.map((p: unknown) => C.fromCartesian(p).height);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
}
|
||||
|
||||
async function runPrediction(page: import('@playwright/test').Page) {
|
||||
await page.goto('/');
|
||||
await page.evaluate(() => localStorage.removeItem('workspaces'));
|
||||
await page.goto('/predict');
|
||||
await page
|
||||
.locator('.map-container canvas')
|
||||
.first()
|
||||
.waitFor({ state: 'attached', timeout: 60_000 });
|
||||
await page
|
||||
.locator('.panel-container-right')
|
||||
.locator('.workspace-row')
|
||||
.first()
|
||||
.getByRole('button', { name: /Рассчитать|Run/ })
|
||||
.click();
|
||||
await expect
|
||||
.poll(() => sceneObjectCount(page, 'ws/'), {
|
||||
timeout: 90_000,
|
||||
intervals: [1000, 2000, 3000],
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
test('trajectory vertices carry real altitude, not ground level', async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
await runPrediction(page);
|
||||
|
||||
const heights = await pathHeights(page);
|
||||
expect(heights.length).toBeGreaterThan(10);
|
||||
// Burst is at 30 km; the apex must be somewhere near it, definitely not 0.
|
||||
expect(Math.max(...heights)).toBeGreaterThan(10_000);
|
||||
// Launch/landing sit on the ground, so the minimum stays low.
|
||||
expect(Math.min(...heights)).toBeLessThan(1_000);
|
||||
});
|
||||
|
||||
test('burst marker sits at its altitude, not on the ground', async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
await runPrediction(page);
|
||||
|
||||
const burstHeight = await page.evaluate(() => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const v: any = (window as any)._lsvMap;
|
||||
const C: any = v.camera.positionCartographic.constructor;
|
||||
const e = v.entities.values.find((x: any) => String(x.id).endsWith('__burst'));
|
||||
if (!e) return null;
|
||||
return C.fromCartesian(e.position.getValue(v.clock.currentTime)).height;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
expect(burstHeight).not.toBeNull();
|
||||
expect(burstHeight as number).toBeGreaterThan(10_000);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue