Inspect the wind field through the engine contract

The inspection marker and the centre probe use the engine's host,
projection, frame events, ellipsoid pick and render requests instead of
Cesium's scene, so they behave the same on Navara.
This commit is contained in:
German Khokhlov committed 2026-11-10 16:14:14 +04:00
1 parent e0d3e93a28
commit 4f654df3c0
7 files changed
+47 -75

No files matched your search

+5 -5
View File
@@ -23,13 +23,13 @@ const options = {
validTime: '2026-09-14 18:00 UTC',
status: 'Model forecast',
};
/** The Cesium path picks the centre itself; Navara hands in the position. */
const viaViewer = (snapshot, point) =>
/** Inspect either the current map centre or a captured position. */
const viaEngine = (snapshot, point) =>
inspectWindAtCenter(
snapshot,
{
camera: { pickEllipsoid: () => point },
scene: { canvas: { clientWidth: 800, clientHeight: 600 } },
host: { canvas: { clientWidth: 800, clientHeight: 600 } },
},
options,
);
@@ -42,7 +42,7 @@ test('edge:wind: a fixed grid gives the same direction, speed and legend colour
// 5 m/s blowing toward the west: the wind comes FROM the east.
const snapshot = field(-5, 0);
for (const point of [at(10, 20), at(180, 0), at(-180, 0), at(-45.5, -60)]) {
const cesiumReading = viaViewer(snapshot, point);
const cesiumReading = viaEngine(snapshot, point);
const navaraReading = viaPosition(snapshot, point);
for (const reading of [cesiumReading, navaraReading]) {
assert.equal(reading.from, 'E');
@@ -76,7 +76,7 @@ test('edge:wind: an empty or unavailable field is not reported as calm, a real 0
];
for (const snapshot of empty) {
for (const reading of [
viaViewer(snapshot, point),
viaEngine(snapshot, point),
viaPosition(snapshot, point),
]) {
assert.equal(reading.speed, undefined, 'no speed is invented');
+2 -10
View File
@@ -82,14 +82,7 @@ export function createWindLayer({
inspectionMarker?.clear();
};
const sample = (position) => {
if (!position && engine && !viewer) {
const canvas = engine.host.canvas;
position = engine.camera.pickEllipsoid({
x: canvas.clientWidth / 2,
y: canvas.clientHeight / 2,
});
}
reading = inspectWindAtCenter(manifest, viewer, {
reading = inspectWindAtCenter(manifest, engine, {
position,
units,
overlay,
@@ -130,7 +123,6 @@ export function createWindLayer({
rendering?.setOptions?.({ overlay, paused });
inspectionMarker = createWindInspectionMarker({
container: engine.host.container,
viewer,
engine,
});
if (enabled && manifest && !manifest.unavailable) {
@@ -254,7 +246,7 @@ export function createWindLayer({
loading = enabled && needsField;
if (enabled && needsField)
queueMicrotask(() => {
if (enabled && generation === current) void layer.update(viewer);
if (enabled && generation === current) void layer.update();
});
else if (enabled && manifest && !manifest.unavailable)
rendering?.start();
+4 -2
View File
@@ -365,8 +365,10 @@ test('sample stays fixed across model, field and unit changes; dismissal and dis
feed: { getSnapshot: async ({ model }) => complete(model) },
createNativeRendering: () => rendering,
});
// A Cesium page: the viewer samples, the engine projects the marker.
layer.init(viewer, { engine: createFakeEngine({ container }) });
const engine = createFakeEngine({ container, canvas: viewer.scene.canvas });
engine.camera.pickEllipsoid = (point) => viewer.camera.pickEllipsoid(point);
engine.frames.postRender = viewer.scene.postRender;
layer.init(viewer, { engine });
layer.enable();
await layer.update();
layer.setParams({ inspect: true });
+7 -10
View File
@@ -45,20 +45,17 @@ export function formatWindSpeed(speed, units = 'km/h') {
/** A deliberate snapshot at the map center; does not own pointer input. */
export function inspectWindAtCenter(
snapshot,
viewer,
engine,
{ units, overlay, model, validTime, status, position } = {},
) {
const scene = viewer?.scene;
const canvas = scene?.canvas;
// The globe's own ellipsoid, else WGS84 (geo's default).
const ellipsoid = scene?.globe?.ellipsoid;
const canvas = engine?.host.canvas;
const point =
position ||
(canvas &&
viewer.camera?.pickEllipsoid?.(
geo.cartesian2.create(canvas.clientWidth / 2, canvas.clientHeight / 2),
ellipsoid,
));
engine.camera.pickEllipsoid({
x: canvas.clientWidth / 2,
y: canvas.clientHeight / 2,
}));
const noReading = () => ({
coordinates: msg('No surface reading'),
wind: msg('Aim the map center at Earth'),
@@ -70,7 +67,7 @@ export function inspectWindAtCenter(
),
});
if (!point || !snapshot?.u || !snapshot?.v) return noReading();
const location = geo.cartographic.fromCartesian(point, ellipsoid);
const location = geo.cartographic.fromCartesian(point);
const lon = geo.math.toDegrees(location.longitude);
const lat = geo.math.toDegrees(location.latitude);
const vector = sampleWind(
+5 -5
View File
@@ -27,11 +27,11 @@ test('center inspection uses a fixed forecast reading without double-converting
values: Float32Array.from([20, 20, 20, 20]),
},
};
const viewer = {
const engine = {
camera: { pickEllipsoid: () => geo.cartesian3.fromDegrees(0, 0) },
scene: { canvas: { clientWidth: 800, clientHeight: 600 } },
host: { canvas: { clientWidth: 800, clientHeight: 600 } },
};
const result = inspectWindAtCenter(snapshot, viewer, {
const result = inspectWindAtCenter(snapshot, engine, {
overlay: 'temperature',
units: 'km/h',
model: 'GFS',
@@ -42,9 +42,9 @@ test('center inspection uses a fixed forecast reading without double-converting
assert.equal(result.scalarValue, '20.0 °C');
assert.equal(result.validTime, 'fixed');
assert.equal(result.coordinates, '0.00°N · 0.00°E');
viewer.camera.pickEllipsoid = () => null;
engine.camera.pickEllipsoid = () => null;
assert.equal(
inspectWindAtCenter(snapshot, viewer).coordinates,
inspectWindAtCenter(snapshot, engine).coordinates,
'No surface reading',
);
});
+7 -24
View File
@@ -9,9 +9,7 @@ import { applicationPinia } from '../../ui/vueRuntime.js';
*/
export function createWindInspectionMarker({
container,
viewer,
engine,
cesium,
store = useWindInspectionStore(applicationPinia()),
}) {
const owner = {};
@@ -28,45 +26,31 @@ export function createWindInspectionMarker({
removeRender = null;
if (shown()) store.publish(null);
marker = null;
if (engine) engine.render.request('wind-inspection');
else viewer?.scene?.requestRender?.();
engine.render.request('wind-inspection');
};
return {
show(position) {
clear();
const scene = viewer?.scene;
const canvas = scene?.canvas ?? engine?.host.canvas;
const frames = scene?.postRender ?? engine?.frames.postRender;
const canvas = engine.host.canvas;
const frames = engine.frames.postRender;
if (
!position ||
!container?.ownerDocument ||
!frames?.addEventListener ||
(!engine?.projector && !scene?.cartesianToCanvasCoordinates)
!engine.projector
)
return;
present({ owner, container, visible: false, left: 0, top: 0 });
const occluder =
!engine &&
new cesium.EllipsoidalOccluder(
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
viewer.camera.positionWC,
);
const render = () => {
if (!shown()) return;
if (occluder) occluder.cameraPosition = viewer.camera.positionWC;
const point = engine
? engine.projector.worldToWindow(position, {})
: scene.cartesianToCanvasCoordinates(position);
const point = engine.projector.worldToWindow(position, {});
const visible =
point &&
point.x >= 0 &&
point.y >= 0 &&
point.x <= canvas.clientWidth &&
point.y <= canvas.clientHeight &&
(engine
? engine.projector.horizonVisible(position)
: scene.mode !== cesium.SceneMode.SCENE3D ||
occluder.isPointVisible(position));
engine.projector.horizonVisible(position);
if (!visible) {
// A hidden marker keeps its last place, as the element did.
if (marker.visible) present({ visible: false });
@@ -82,8 +66,7 @@ export function createWindInspectionMarker({
};
removeRender = frames.addEventListener(render);
render();
if (engine) engine.render.request('wind-inspection');
else scene.requestRender?.();
engine.render.request('wind-inspection');
},
clear,
destroy: clear,
+17 -19
View File
@@ -10,16 +10,7 @@ import { useWindInspectionStore } from '../../ui/stores/windInspection.js';
import WindInspectionMarker from '../../ui/views/panels/WindInspectionMarker.vue';
import { createWindInspectionMarker } from './inspectionMarker.js';
const cesium = {
SceneMode: { SCENE3D: 3 },
Ellipsoid: { WGS84: {} },
EllipsoidalOccluder: class {
isPointVisible() {
return cesium.visible;
}
},
visible: true,
};
const visibility = { visible: true };
function fixture(t) {
document.body.replaceChildren();
@@ -56,18 +47,26 @@ function fixture(t) {
requestRender() {},
},
};
const engine = {
host: { canvas: viewer.scene.canvas },
frames: { postRender: viewer.scene.postRender },
projector: {
worldToWindow: viewer.scene.cartesianToCanvasCoordinates,
horizonVisible: () => visibility.visible,
},
render: { request() {} },
};
const store = useWindInspectionStore(pinia);
cesium.visible = true;
visibility.visible = true;
const owner = createWindInspectionMarker({
container,
viewer,
cesium,
engine,
store,
});
const nodes = () => [
...container.querySelectorAll('.gev-wind-inspection-marker'),
];
return { owner, viewer, store, frames, projected, nodes, container, cesium };
return { owner, viewer, store, frames, projected, nodes, container, engine };
}
test('the marker follows only its captured point, hides beyond the limb, and releases its listener', async (t) => {
@@ -85,13 +84,13 @@ test('the marker follows only its captured point, hides beyond the limb, and rel
assert.equal(marker.style.top, '220px');
assert.equal(marker.hidden, false);
viewer.camera.positionWC = { moved: true };
cesium.visible = false;
visibility.visible = false;
frames.listener();
await nextTick();
assert.equal(marker.hidden, true);
assert.equal(marker.style.left, '110px', 'a hidden marker keeps its place');
assert.ok(projected.every((value) => value === point));
cesium.visible = true;
visibility.visible = true;
frames.listener();
await nextTick();
assert.equal(marker.hidden, false);
@@ -111,7 +110,7 @@ test('the marker follows only its captured point, hides beyond the limb, and rel
});
test('an unchanged frame publishes nothing, and an older owner leaves a newer marker', (t) => {
const { owner, viewer, store, frames, container } = fixture(t);
const { owner, engine, store, frames, container } = fixture(t);
owner.show({ x: 1 });
let publishes = 0;
const publish = store.publish;
@@ -124,8 +123,7 @@ test('an unchanged frame publishes nothing, and an older owner leaves a newer ma
frames.listener = null;
const newer = createWindInspectionMarker({
container,
viewer,
cesium,
engine,
store,
});
newer.show({ x: 2 });