mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
refactor(voice): W1 actions on the engine
The voice runner drives the globe engine (an `engine` option, by default the one registered for the viewer) instead of the Cesium viewer: track_entity, stop_tracking (camera.following().stop()), frame_overhead (camera.flyToSphere with the pose heading), fly_to_location, zoom_to_globe (follow released through camera.following()), adjust_camera_zoom, next_iss_pass / next_satellite_pass, select_nearest_aircraft and the analyst view context, get_current_view_state, get_entity_context (projector.worldToWindow), the view target (picker.pickPosition, then camera.pickEllipsoid, then picker.pickGlobe) and its moveEnd prewarm, and the viewport samples. getBasemapLabelContext takes the engine or a viewer with a registered engine (the HUD). Without a viewer the tools that still need one (VIEWER_ONLY_ACTIONS: control_scene, annotate_map, clear_annotations, fly_route, move_camera, control_cockpit, set_context_mode, control_radio, control_cctv) answer a structured refusal (code UNAVAILABLE_ON_ENGINE) instead of throwing. Point-and-ask picks with engine.picker and keys the camera pose from engine.camera.pose(); the viewport capture reads engine.host.canvas and waits for the engine's postRender (both engines keep their drawing buffer: Cesium's contextOptions, Navara's own WebGL2 canvas, S4). Tests: gevActionsEngine.test.mjs runs each moved action on the fake engine. gevActions.test.mjs keeps its expectations; its runner helper now registers an engine for the test viewer (the Cesium engine over it, or a fake engine at its positionCartographic) and the two harness cameras gain roll and frustum for camera.pose().
This commit is contained in:
1 parent
ec7dcc944e
commit
bc281ab246
9 files changed
+701
-190
No files matched your search
@@ -1010,6 +1010,7 @@
|
|||||||
"scripts/qa/navaraContractPage.js",
|
"scripts/qa/navaraContractPage.js",
|
||||||
"scripts/qa/tileCache.mjs",
|
"scripts/qa/tileCache.mjs",
|
||||||
"src/annotations/resolverEngine.test.mjs",
|
"src/annotations/resolverEngine.test.mjs",
|
||||||
|
"src/voice/gevActionsEngine.test.mjs",
|
||||||
"src/app/controls.test.mjs",
|
"src/app/controls.test.mjs",
|
||||||
"src/app/engineChoice.js",
|
"src/app/engineChoice.js",
|
||||||
"src/app/engineChoice.test.mjs",
|
"src/app/engineChoice.test.mjs",
|
||||||
|
|||||||
@@ -1705,7 +1705,8 @@
|
|||||||
"src/voice/narration.js",
|
"src/voice/narration.js",
|
||||||
"src/voice/earcon.js",
|
"src/voice/earcon.js",
|
||||||
"src/voice/pointerContext.js",
|
"src/voice/pointerContext.js",
|
||||||
"src/voice/pointerReticle.js"
|
"src/voice/pointerReticle.js",
|
||||||
|
"src/globe/contract.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
+178
-152
@@ -43,6 +43,7 @@ import {
|
|||||||
} from '../cameraVerbs.js';
|
} from '../cameraVerbs.js';
|
||||||
import * as defaultFloorServices from '../data/groundFloor.js';
|
import * as defaultFloorServices from '../data/groundFloor.js';
|
||||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||||
|
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||||
import { unavailablePlaceSearch } from '../search/placeSearch.js';
|
import { unavailablePlaceSearch } from '../search/placeSearch.js';
|
||||||
import * as defaultAnnotationResolver from '../annotations/annotationResolver.js';
|
import * as defaultAnnotationResolver from '../annotations/annotationResolver.js';
|
||||||
import { normalizeRadioCountryInput } from '../data/radioCountry.js';
|
import { normalizeRadioCountryInput } from '../data/radioCountry.js';
|
||||||
@@ -264,6 +265,38 @@ const FRAME_TARGETS = new Map([
|
|||||||
['ships', 'ais-live-vessels'],
|
['ships', 'ais-live-vessels'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tools that still need the Cesium viewer or a subsystem built on it:
|
||||||
|
* the scene director, annotations and routes over them, the camera verbs
|
||||||
|
* (orbit, pan, tilt), the cockpit, Contacts (the context mode), Radio and
|
||||||
|
* CCTV. Without a viewer (the Navara engine) each answers a structured
|
||||||
|
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
||||||
|
*/
|
||||||
|
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||||
|
'control_scene',
|
||||||
|
'annotate_map',
|
||||||
|
'clear_annotations',
|
||||||
|
'fly_route',
|
||||||
|
'move_camera',
|
||||||
|
'control_cockpit',
|
||||||
|
'set_context_mode',
|
||||||
|
'control_radio',
|
||||||
|
'control_cctv',
|
||||||
|
]);
|
||||||
|
/** The refusal code of a viewer-only tool on an engine without a viewer. */
|
||||||
|
export const UNAVAILABLE_ON_ENGINE = 'UNAVAILABLE_ON_ENGINE';
|
||||||
|
|
||||||
|
function unavailableOnEngine(name, engine) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
action: name,
|
||||||
|
unavailable: true,
|
||||||
|
code: UNAVAILABLE_ON_ENGINE,
|
||||||
|
engine: engine?.kind ?? null,
|
||||||
|
error: 'Unavailable on this engine',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const serviceCaches = new WeakMap();
|
const serviceCaches = new WeakMap();
|
||||||
function cachesFor(service) {
|
function cachesFor(service) {
|
||||||
service.signal?.throwIfAborted();
|
service.signal?.throwIfAborted();
|
||||||
@@ -289,10 +322,21 @@ function cachesFor(service) {
|
|||||||
const VISIBLE_ENTITY_SHORTLIST = 64;
|
const VISIBLE_ENTITY_SHORTLIST = 64;
|
||||||
const BASEMAP_CONTEXT_WAIT_MS = 1500;
|
const BASEMAP_CONTEXT_WAIT_MS = 1500;
|
||||||
const viewTargetCache = new WeakMap();
|
const viewTargetCache = new WeakMap();
|
||||||
|
const prewarmedEngines = new WeakSet();
|
||||||
|
|
||||||
/** Create application actions over the supplied scene and services. */
|
/** The camera's place (radians, metres) from its live position. */
|
||||||
|
const cameraCartographic = (engine) =>
|
||||||
|
geo.cartographic.fromCartesian(engine.camera.livePosition);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create application actions over the supplied scene and services. The
|
||||||
|
* actions drive the globe `engine` (by default the one registered for the
|
||||||
|
* viewer); `viewer` is null on an engine without one (Navara), where the
|
||||||
|
* VIEWER_ONLY_ACTIONS refuse.
|
||||||
|
*/
|
||||||
export function createGevActionRunner({
|
export function createGevActionRunner({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine: givenEngine = null,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
sceneDirector = null,
|
sceneDirector = null,
|
||||||
@@ -310,20 +354,22 @@ export function createGevActionRunner({
|
|||||||
const builders = speechBuilders
|
const builders = speechBuilders
|
||||||
? { ...SPEECH_BUILDERS, ...speechBuilders }
|
? { ...SPEECH_BUILDERS, ...speechBuilders }
|
||||||
: SPEECH_BUILDERS;
|
: SPEECH_BUILDERS;
|
||||||
|
const engine = givenEngine ?? engineFromViewer(viewer);
|
||||||
const _layerEnabledAt = new Map();
|
const _layerEnabledAt = new Map();
|
||||||
let analystEngine;
|
let analystEngine;
|
||||||
const resolveRegionRing = (name, signal) =>
|
const resolveRegionRing = (name, signal) =>
|
||||||
annotationResolver.resolveRegionRingForQuery(name, signal, placeSearch);
|
annotationResolver.resolveRegionRingForQuery(name, signal, placeSearch);
|
||||||
const ensureAnalystEngine = () =>
|
const ensureAnalystEngine = () =>
|
||||||
(analystEngine ||= createAnalystEngine(
|
(analystEngine ||= createAnalystEngine(
|
||||||
analystProviders(viewer, dataManager, {
|
analystProviders(engine, dataManager, {
|
||||||
placeSearch,
|
placeSearch,
|
||||||
resolveRegionRing,
|
resolveRegionRing,
|
||||||
isWarming: (layerKey) => layerIsWarming(_layerEnabledAt, layerKey),
|
isWarming: (layerKey) => layerIsWarming(_layerEnabledAt, layerKey),
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
installViewTargetPrewarm(viewer);
|
installViewTargetPrewarm(engine);
|
||||||
initCameraVerbs(viewer, getViewTargetCartesian);
|
// The camera verbs (move_camera, fly_route) still run on the viewer.
|
||||||
|
if (viewer) initCameraVerbs(viewer, () => getViewTargetCartesian(engine));
|
||||||
// Nested calls (select_nearest_aircraft composing layer + flight) use the
|
// Nested calls (select_nearest_aircraft composing layer + flight) use the
|
||||||
// raw runner; only the outer call gets a voice envelope.
|
// raw runner; only the outer call gets a voice envelope.
|
||||||
const runGevAction = async function runGevAction(
|
const runGevAction = async function runGevAction(
|
||||||
@@ -343,6 +389,8 @@ export function createGevActionRunner({
|
|||||||
/* Progress is advisory. */
|
/* Progress is advisory. */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (!viewer && VIEWER_ONLY_ACTIONS.includes(name))
|
||||||
|
return unavailableOnEngine(name, engine);
|
||||||
|
|
||||||
// Navigation tools interrupt any continuous camera motion (spec §1.1) —
|
// Navigation tools interrupt any continuous camera motion (spec §1.1) —
|
||||||
// checked FIRST because each handler returns.
|
// checked FIRST because each handler returns.
|
||||||
@@ -352,8 +400,8 @@ export function createGevActionRunner({
|
|||||||
// Explicit navigation while TRACKING supersedes the follow camera —
|
// Explicit navigation while TRACKING supersedes the follow camera —
|
||||||
// otherwise the tracker drags the view back and "I flew there but can't
|
// otherwise the tracker drags the view back and "I flew there but can't
|
||||||
// do anything" (field finding). track_entity manages its own handoff.
|
// do anything" (field finding). track_entity manages its own handoff.
|
||||||
if (name === 'zoom_to_globe' && viewer.trackedEntity) {
|
if (name === 'zoom_to_globe' && engine?.camera.following()) {
|
||||||
stopAllTracking(viewer, dataManager);
|
stopAllTracking(engine, dataManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zoom during an active orbit adjusts the orbit RADIUS (spiral in/out) —
|
// Zoom during an active orbit adjusts the orbit RADIUS (spiral in/out) —
|
||||||
@@ -587,7 +635,7 @@ export function createGevActionRunner({
|
|||||||
});
|
});
|
||||||
} else if (typeof layerModule?.update === 'function') {
|
} else if (typeof layerModule?.update === 'function') {
|
||||||
refreshed =
|
refreshed =
|
||||||
(await layerModule.update(viewer, {
|
(await layerModule.update(viewer ?? engine, {
|
||||||
signal: runOptions.signal,
|
signal: runOptions.signal,
|
||||||
})) !== false;
|
})) !== false;
|
||||||
}
|
}
|
||||||
@@ -620,7 +668,7 @@ export function createGevActionRunner({
|
|||||||
|
|
||||||
reportProgress('nearest', location.label);
|
reportProgress('nearest', location.label);
|
||||||
const nearest = await createAnalystEngine(
|
const nearest = await createAnalystEngine(
|
||||||
analystProviders(viewer, dataManager, {
|
analystProviders(engine, dataManager, {
|
||||||
recordLimitByLayer: { [layerId]: Number.MAX_SAFE_INTEGER },
|
recordLimitByLayer: { [layerId]: Number.MAX_SAFE_INTEGER },
|
||||||
placeSearch,
|
placeSearch,
|
||||||
resolveRegionRing,
|
resolveRegionRing,
|
||||||
@@ -653,7 +701,7 @@ export function createGevActionRunner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stableAircraftId = aircraft.icao24 || aircraft.id;
|
const stableAircraftId = aircraft.icao24 || aircraft.id;
|
||||||
const selection = await trackEntity(viewer, dataManager, styleManager, {
|
const selection = await trackEntity(engine, dataManager, styleManager, {
|
||||||
query: stableAircraftId,
|
query: stableAircraftId,
|
||||||
layerId,
|
layerId,
|
||||||
});
|
});
|
||||||
@@ -950,7 +998,7 @@ export function createGevActionRunner({
|
|||||||
runOptions,
|
runOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return flyToRequestedLocation(viewer, args, {
|
return flyToRequestedLocation(engine, args, {
|
||||||
onProgress: reportProgress,
|
onProgress: reportProgress,
|
||||||
placeSearch,
|
placeSearch,
|
||||||
searchNavigation,
|
searchNavigation,
|
||||||
@@ -975,20 +1023,20 @@ export function createGevActionRunner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
interruptCameraMotion('nav:fly_to_location');
|
interruptCameraMotion('nav:fly_to_location');
|
||||||
if (viewer.trackedEntity) stopAllTracking(viewer, dataManager);
|
if (engine?.camera.following()) stopAllTracking(engine, dataManager);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'adjust_camera_zoom') {
|
if (name === 'adjust_camera_zoom') {
|
||||||
return adjustCameraZoom(viewer, args);
|
return adjustCameraZoom(engine, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'zoom_to_globe') {
|
if (name === 'zoom_to_globe') {
|
||||||
if (typeof styleManager?.resetToGlobeView === 'function') {
|
if (typeof styleManager?.resetToGlobeView === 'function') {
|
||||||
return styleManager.resetToGlobeView();
|
return styleManager.resetToGlobeView();
|
||||||
}
|
}
|
||||||
const result = flyToGlobeView(viewer);
|
const result = flyToGlobeView(engine);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'zoom_to_globe',
|
action: 'zoom_to_globe',
|
||||||
@@ -1001,11 +1049,11 @@ export function createGevActionRunner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'next_satellite_pass') {
|
if (name === 'next_satellite_pass') {
|
||||||
return nextSatellitePass(viewer, dataManager, args);
|
return nextSatellitePass(engine, dataManager, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'next_iss_pass') {
|
if (name === 'next_iss_pass') {
|
||||||
return nextIssPass(viewer, dataManager, args);
|
return nextIssPass(engine, dataManager, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'analyst_query') {
|
if (name === 'analyst_query') {
|
||||||
@@ -1049,7 +1097,7 @@ export function createGevActionRunner({
|
|||||||
|
|
||||||
if (name === 'get_entity_context') {
|
if (name === 'get_entity_context') {
|
||||||
return getEntityContext(
|
return getEntityContext(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
styleManager,
|
styleManager,
|
||||||
args,
|
args,
|
||||||
@@ -1059,7 +1107,7 @@ export function createGevActionRunner({
|
|||||||
|
|
||||||
if (name === 'get_current_view_state') {
|
if (name === 'get_current_view_state') {
|
||||||
return getCurrentViewState(
|
return getCurrentViewState(
|
||||||
viewer,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
sceneDirector,
|
sceneDirector,
|
||||||
@@ -1164,17 +1212,17 @@ export function createGevActionRunner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'track_entity') {
|
if (name === 'track_entity') {
|
||||||
return trackEntity(viewer, dataManager, styleManager, args, {
|
return trackEntity(engine, dataManager, styleManager, args, {
|
||||||
isCurrent: current,
|
isCurrent: current,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'stop_tracking') {
|
if (name === 'stop_tracking') {
|
||||||
return stopAllTracking(viewer, dataManager);
|
return stopAllTracking(engine, dataManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'frame_overhead') {
|
if (name === 'frame_overhead') {
|
||||||
return frameOverhead(viewer, dataManager, styleManager, args);
|
return frameOverhead(engine, dataManager, styleManager, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'annotate_map') {
|
if (name === 'annotate_map') {
|
||||||
@@ -2187,7 +2235,7 @@ export function formatTrackedEntityLabel(found, query = '') {
|
|||||||
|
|
||||||
/** Finds and tracks/selects an entity by spoken query across layer families. */
|
/** Finds and tracks/selects an entity by spoken query across layer families. */
|
||||||
async function trackEntity(
|
async function trackEntity(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
styleManager,
|
styleManager,
|
||||||
args = {},
|
args = {},
|
||||||
@@ -2240,7 +2288,7 @@ async function trackEntity(
|
|||||||
'fire',
|
'fire',
|
||||||
'track_entity',
|
'track_entity',
|
||||||
() => {
|
() => {
|
||||||
flyToLandmark(viewer, strongest.latitude, strongest.longitude, {
|
flyToLandmark(engine, strongest.latitude, strongest.longitude, {
|
||||||
range: 14000,
|
range: 14000,
|
||||||
pitch: -50,
|
pitch: -50,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
@@ -2300,7 +2348,7 @@ async function trackEntity(
|
|||||||
let trackedOk = false;
|
let trackedOk = false;
|
||||||
if (family.kind === 'vessel') {
|
if (family.kind === 'vessel') {
|
||||||
trackedOk = !!module.selectById?.(found.mmsi);
|
trackedOk = !!module.selectById?.(found.mmsi);
|
||||||
flyToLandmark(viewer, found.latitude, found.longitude, {
|
flyToLandmark(engine, found.latitude, found.longitude, {
|
||||||
range: 6000,
|
range: 6000,
|
||||||
pitch: -45,
|
pitch: -45,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
@@ -2346,7 +2394,7 @@ async function trackEntity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Releases tracking/selection on every entity layer family. */
|
/** Releases tracking/selection on every entity layer family. */
|
||||||
function stopAllTracking(viewer, dataManager) {
|
function stopAllTracking(engine, dataManager) {
|
||||||
const released = [];
|
const released = [];
|
||||||
const failed = new Set();
|
const failed = new Set();
|
||||||
for (const family of TRACKABLE_FAMILIES) {
|
for (const family of TRACKABLE_FAMILIES) {
|
||||||
@@ -2397,7 +2445,7 @@ function stopAllTracking(viewer, dataManager) {
|
|||||||
failed.add(layerId);
|
failed.add(layerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (viewer) viewer.trackedEntity = undefined;
|
engine?.camera.following()?.stop();
|
||||||
if (failed.size) {
|
if (failed.size) {
|
||||||
const failedLayerIds = [...failed];
|
const failedLayerIds = [...failed];
|
||||||
return {
|
return {
|
||||||
@@ -2418,7 +2466,7 @@ function stopAllTracking(viewer, dataManager) {
|
|||||||
* detection so the framed entities are labeled, and reports
|
* detection so the framed entities are labeled, and reports
|
||||||
* detectionEnabled so the voice agent can mention labels are on.
|
* detectionEnabled so the voice agent can mention labels are on.
|
||||||
*/
|
*/
|
||||||
async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
|
async function frameOverhead(engine, dataManager, styleManager, args = {}) {
|
||||||
const targetRaw = String(args.target || 'flights').toLowerCase();
|
const targetRaw = String(args.target || 'flights').toLowerCase();
|
||||||
const layerId =
|
const layerId =
|
||||||
FRAME_TARGETS.get(targetRaw) || normalizeLayerId(targetRaw) || 'flights';
|
FRAME_TARGETS.get(targetRaw) || normalizeLayerId(targetRaw) || 'flights';
|
||||||
@@ -2445,7 +2493,9 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
|
|||||||
? 120
|
? 120
|
||||||
: 150;
|
: 150;
|
||||||
const radiusKm = clampNumber(args.radiusKm, 10, 20000, defaultRadiusKm);
|
const radiusKm = clampNumber(args.radiusKm, 10, 20000, defaultRadiusKm);
|
||||||
const center = getViewTargetCartesian(viewer) || viewer.camera.positionWC;
|
const center =
|
||||||
|
getViewTargetCartesian(engine) ||
|
||||||
|
geo.cartesian3.clone(engine.camera.livePosition);
|
||||||
|
|
||||||
let entries = [];
|
let entries = [];
|
||||||
if (typeof module.getNearby === 'function') {
|
if (typeof module.getNearby === 'function') {
|
||||||
@@ -2472,23 +2522,25 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const sphere = Cesium.BoundingSphere.fromPoints(
|
// Cesium's bounding sphere (pure math, no viewer), flown by the engine.
|
||||||
|
const bounds = Cesium.BoundingSphere.fromPoints(
|
||||||
entries.map((entry) => entry.position),
|
entries.map((entry) => entry.position),
|
||||||
);
|
);
|
||||||
sphere.radius = Math.max(sphere.radius * 1.25, 8000);
|
const sphere = {
|
||||||
|
center: geo.cartesian3.clone(bounds.center),
|
||||||
|
radius: Math.max(bounds.radius * 1.25, 8000),
|
||||||
|
};
|
||||||
const pitch = geo.math.toRadians(isSatellites ? -35 : -62);
|
const pitch = geo.math.toRadians(isSatellites ? -35 : -62);
|
||||||
return runManagedVoiceNavigation(
|
return runManagedVoiceNavigation(
|
||||||
styleManager,
|
styleManager,
|
||||||
'frame',
|
'frame',
|
||||||
'frame_overhead',
|
'frame_overhead',
|
||||||
() => {
|
() => {
|
||||||
viewer.camera.flyToBoundingSphere(sphere, {
|
engine.camera.flyToSphere(sphere, {
|
||||||
duration: 2.0,
|
duration: 2.0,
|
||||||
offset: new Cesium.HeadingPitchRange(
|
heading: engine.camera.pose().heading,
|
||||||
viewer.camera.heading,
|
pitch,
|
||||||
pitch,
|
range: sphere.radius * 2.4,
|
||||||
sphere.radius * 2.4,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let detectionEnabled = false;
|
let detectionEnabled = false;
|
||||||
@@ -2567,25 +2619,30 @@ function collectTrackedEntities(dataManager) {
|
|||||||
return tracked;
|
return tracked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} target The globe engine, or a Cesium viewer with a
|
||||||
|
* registered engine (the HUD).
|
||||||
|
*/
|
||||||
export async function getBasemapLabelContext(
|
export async function getBasemapLabelContext(
|
||||||
viewer,
|
target,
|
||||||
service = defaultGeospatial,
|
service = defaultGeospatial,
|
||||||
{ cachedOnly = false } = {},
|
{ cachedOnly = false } = {},
|
||||||
) {
|
) {
|
||||||
|
const engine = engineFromViewer(target);
|
||||||
const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service);
|
const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service);
|
||||||
const samples = sampleViewportCartographics(viewer);
|
const viewTarget = engine ? getViewTargetCartographic(engine) : null;
|
||||||
const cameraHeightM = viewer.camera.positionCartographic.height;
|
if (!viewTarget) {
|
||||||
const target = getViewTargetCartographic(viewer);
|
|
||||||
if (!target) {
|
|
||||||
return {
|
return {
|
||||||
placeLabels: [],
|
placeLabels: [],
|
||||||
streetLabels: [],
|
streetLabels: [],
|
||||||
nearbyPlaceLabels: [],
|
nearbyPlaceLabels: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const samples = sampleViewportCartographics(engine);
|
||||||
|
const cameraHeightM = engine.camera.height();
|
||||||
|
|
||||||
const latitude = Number(geo.math.toDegrees(target.latitude).toFixed(6));
|
const latitude = Number(geo.math.toDegrees(viewTarget.latitude).toFixed(6));
|
||||||
const longitude = Number(geo.math.toDegrees(target.longitude).toFixed(6));
|
const longitude = Number(geo.math.toDegrees(viewTarget.longitude).toFixed(6));
|
||||||
const cachedViewportPlaces = viewportPlacesFromCache(
|
const cachedViewportPlaces = viewportPlacesFromCache(
|
||||||
samples,
|
samples,
|
||||||
cameraHeightM,
|
cameraHeightM,
|
||||||
@@ -2646,12 +2703,12 @@ export async function getBasemapLabelContext(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function installViewTargetPrewarm(viewer) {
|
function installViewTargetPrewarm(engine) {
|
||||||
if (viewer.__gevViewTargetPrewarmInstalled) return;
|
if (!engine || prewarmedEngines.has(engine)) return;
|
||||||
viewer.__gevViewTargetPrewarmInstalled = true;
|
prewarmedEngines.add(engine);
|
||||||
let timer = null;
|
let timer = null;
|
||||||
let reportedPrewarmFailure = false;
|
let reportedPrewarmFailure = false;
|
||||||
viewer.camera.moveEnd.addEventListener(() => {
|
engine.camera.moveEnd.addEventListener(() => {
|
||||||
if (timer) window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
timer = window.setTimeout(() => {
|
timer = window.setTimeout(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
@@ -2659,10 +2716,10 @@ function installViewTargetPrewarm(viewer) {
|
|||||||
// because an idle callback is an UNCAUGHT context — nothing above it can
|
// because an idle callback is an UNCAUGHT context — nothing above it can
|
||||||
// handle a surprise from the scene graph, and a red console error on a
|
// handle a surprise from the scene graph, and a red console error on a
|
||||||
// plain camera flight is worse than a cold cache. Reported once per
|
// plain camera flight is worse than a cold cache. Reported once per
|
||||||
// viewer so a repeating cause cannot spam the console.
|
// engine so a repeating cause cannot spam the console.
|
||||||
const warm = () => {
|
const warm = () => {
|
||||||
try {
|
try {
|
||||||
getViewTargetCartographic(viewer);
|
getViewTargetCartographic(engine);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (reportedPrewarmFailure) return;
|
if (reportedPrewarmFailure) return;
|
||||||
reportedPrewarmFailure = true;
|
reportedPrewarmFailure = true;
|
||||||
@@ -2681,7 +2738,7 @@ function installViewTargetPrewarm(viewer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustCameraZoom(viewer, args) {
|
function adjustCameraZoom(engine, args) {
|
||||||
const direction = String(args.direction || '').toLowerCase();
|
const direction = String(args.direction || '').toLowerCase();
|
||||||
if (direction !== 'in' && direction !== 'out') {
|
if (direction !== 'in' && direction !== 'out') {
|
||||||
throw new Error('adjust_camera_zoom direction must be "in" or "out"');
|
throw new Error('adjust_camera_zoom direction must be "in" or "out"');
|
||||||
@@ -2695,10 +2752,10 @@ function adjustCameraZoom(viewer, args) {
|
|||||||
}[amount];
|
}[amount];
|
||||||
if (!fraction) throw new Error(`Unknown zoom amount: ${args.amount}`);
|
if (!fraction) throw new Error(`Unknown zoom amount: ${args.amount}`);
|
||||||
|
|
||||||
const camera = viewer.camera;
|
const camera = engine.camera;
|
||||||
const beforePosition = geo.cartesian3.clone(camera.positionWC);
|
const beforePosition = geo.cartesian3.clone(camera.livePosition);
|
||||||
const beforeHeightM = camera.positionCartographic.height;
|
const beforeHeightM = camera.height();
|
||||||
const target = getViewTargetCartesian(viewer);
|
const target = getViewTargetCartesian(engine);
|
||||||
const targetDistanceM = target
|
const targetDistanceM = target
|
||||||
? geo.cartesian3.distance(beforePosition, target)
|
? geo.cartesian3.distance(beforePosition, target)
|
||||||
: Math.max(100, beforeHeightM);
|
: Math.max(100, beforeHeightM);
|
||||||
@@ -2724,11 +2781,11 @@ function adjustCameraZoom(viewer, args) {
|
|||||||
}
|
}
|
||||||
camera.zoomIn(safeMovementM);
|
camera.zoomIn(safeMovementM);
|
||||||
}
|
}
|
||||||
viewer.scene.requestRender();
|
engine.render.request('voice-zoom');
|
||||||
|
|
||||||
const afterPosition = camera.positionWC;
|
const afterPosition = camera.livePosition;
|
||||||
const movedM = geo.cartesian3.distance(beforePosition, afterPosition);
|
const movedM = geo.cartesian3.distance(beforePosition, afterPosition);
|
||||||
const afterHeightM = camera.positionCartographic.height;
|
const afterHeightM = camera.height();
|
||||||
const moved = movedM >= 0.5;
|
const moved = movedM >= 0.5;
|
||||||
return {
|
return {
|
||||||
ok: moved,
|
ok: moved,
|
||||||
@@ -2766,11 +2823,11 @@ function compassDir(azDeg) {
|
|||||||
return COMPASS_16[Math.round((((azDeg % 360) + 360) % 360) / 22.5) % 16];
|
return COMPASS_16[Math.round((((azDeg % 360) + 360) % 360) / 22.5) % 16];
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextIssPass(viewer, dataManager, args) {
|
function nextIssPass(engine, dataManager, args) {
|
||||||
let latDeg = Number.isFinite(args.latitude) ? args.latitude : null;
|
let latDeg = Number.isFinite(args.latitude) ? args.latitude : null;
|
||||||
let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null;
|
let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null;
|
||||||
if (latDeg == null || lonDeg == null) {
|
if (latDeg == null || lonDeg == null) {
|
||||||
const carto = viewer?.camera?.positionCartographic;
|
const carto = engine ? cameraCartographic(engine) : null;
|
||||||
if (!carto) throw new Error('Camera position unavailable');
|
if (!carto) throw new Error('Camera position unavailable');
|
||||||
latDeg = geo.math.toDegrees(carto.latitude);
|
latDeg = geo.math.toDegrees(carto.latitude);
|
||||||
lonDeg = geo.math.toDegrees(carto.longitude);
|
lonDeg = geo.math.toDegrees(carto.longitude);
|
||||||
@@ -2816,7 +2873,7 @@ function nextIssPass(viewer, dataManager, args) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextSatellitePass(viewer, dataManager, args) {
|
function nextSatellitePass(engine, dataManager, args) {
|
||||||
const layer = dataManager?.layers?.get('satellites')?.module;
|
const layer = dataManager?.layers?.get('satellites')?.module;
|
||||||
const identity = layer?.resolveSatelliteForPass?.(args.target) || {
|
const identity = layer?.resolveSatelliteForPass?.(args.target) || {
|
||||||
status: 'not-found',
|
status: 'not-found',
|
||||||
@@ -2849,7 +2906,7 @@ function nextSatellitePass(viewer, dataManager, args) {
|
|||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
const result = nextIssPass(viewer, adapter, args);
|
const result = nextIssPass(engine, adapter, args);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
result.error = result.error.replace(
|
result.error = result.error.replace(
|
||||||
/ISS/g,
|
/ISS/g,
|
||||||
@@ -3015,7 +3072,7 @@ function normalizeStyle(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function flyToRequestedLocation(
|
async function flyToRequestedLocation(
|
||||||
viewer,
|
engine,
|
||||||
args,
|
args,
|
||||||
{
|
{
|
||||||
placeSearch = unavailablePlaceSearch,
|
placeSearch = unavailablePlaceSearch,
|
||||||
@@ -3067,7 +3124,7 @@ async function flyToRequestedLocation(
|
|||||||
|
|
||||||
if (locationId) {
|
if (locationId) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToPresetLocation(viewer, locationId, {
|
flyToPresetLocation(engine, locationId, {
|
||||||
...(rangeM || args.viewMode === 'close'
|
...(rangeM || args.viewMode === 'close'
|
||||||
? { range: rangeM || 250 }
|
? { range: rangeM || 250 }
|
||||||
: { viewMode: 'overview' }),
|
: { viewMode: 'overview' }),
|
||||||
@@ -3097,7 +3154,7 @@ async function flyToRequestedLocation(
|
|||||||
const longitude = Number(args.longitude);
|
const longitude = Number(args.longitude);
|
||||||
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToLandmark(viewer, latitude, longitude, {
|
flyToLandmark(engine, latitude, longitude, {
|
||||||
range: rangeM || 250,
|
range: rangeM || 250,
|
||||||
pitch: -35,
|
pitch: -35,
|
||||||
heading: 0,
|
heading: 0,
|
||||||
@@ -3129,7 +3186,7 @@ async function flyToRequestedLocation(
|
|||||||
const poiMatch = findPoiByName(query);
|
const poiMatch = findPoiByName(query);
|
||||||
if (poiMatch) {
|
if (poiMatch) {
|
||||||
const result = immediate(() =>
|
const result = immediate(() =>
|
||||||
flyToPOI(viewer, poiMatch.cityId, poiMatch.index, {
|
flyToPOI(engine, poiMatch.cityId, poiMatch.index, {
|
||||||
duration: 2.2,
|
duration: 2.2,
|
||||||
onStart: immediateOnStart,
|
onStart: immediateOnStart,
|
||||||
...arrivalHooks,
|
...arrivalHooks,
|
||||||
@@ -3154,7 +3211,7 @@ async function flyToRequestedLocation(
|
|||||||
if (generation === false) return cancelled(query);
|
if (generation === false) return cancelled(query);
|
||||||
onProgress('search', query);
|
onProgress('search', query);
|
||||||
const managedDeferred = typeof reassertDeferred === 'function';
|
const managedDeferred = typeof reassertDeferred === 'function';
|
||||||
const destination = await searchNavigation(viewer, query, {
|
const destination = await searchNavigation(engine, query, {
|
||||||
placeSearch,
|
placeSearch,
|
||||||
signal,
|
signal,
|
||||||
...(rangeM ? { range: rangeM } : {}),
|
...(rangeM ? { range: rangeM } : {}),
|
||||||
@@ -3195,12 +3252,12 @@ function normalizeLocationId(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentViewState(
|
function getCurrentViewState(
|
||||||
viewer,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
sceneDirector = null,
|
sceneDirector = null,
|
||||||
) {
|
) {
|
||||||
const cartographic = geo.cartographic.fromCartesian(viewer.camera.positionWC);
|
const cartographic = cameraCartographic(engine);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'get_current_view_state',
|
action: 'get_current_view_state',
|
||||||
@@ -3255,7 +3312,7 @@ function getCurrentViewState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getEntityContext(
|
async function getEntityContext(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
styleManager,
|
styleManager,
|
||||||
args = {},
|
args = {},
|
||||||
@@ -3264,15 +3321,15 @@ async function getEntityContext(
|
|||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
const scope = String(args.scope || 'auto').toLowerCase();
|
const scope = String(args.scope || 'auto').toLowerCase();
|
||||||
if (scope === 'target' && args.target) {
|
if (scope === 'target' && args.target) {
|
||||||
return getTargetContext(viewer, dataManager, styleManager, args, service);
|
return getTargetContext(engine, dataManager, styleManager, args, service);
|
||||||
}
|
}
|
||||||
const layerId = normalizeLayerId(args.layerId || args.layer);
|
const layerId = normalizeLayerId(args.layerId || args.layer);
|
||||||
const limit = Math.round(clampNumber(args.limit, 1, 12, 5));
|
const limit = Math.round(clampNumber(args.limit, 1, 12, 5));
|
||||||
const selected = selectedEntityContext(dataManager);
|
const selected = selectedEntityContext(dataManager);
|
||||||
const cameraHeightM = viewer.camera.positionCartographic.height;
|
const cameraHeightM = engine.camera.height();
|
||||||
const viewTarget = getViewTargetCartographic(viewer);
|
const viewTarget = getViewTargetCartographic(engine);
|
||||||
const scenePromise = getSceneContext(
|
const scenePromise = getSceneContext(
|
||||||
viewer,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
viewTarget,
|
viewTarget,
|
||||||
@@ -3282,7 +3339,7 @@ async function getEntityContext(
|
|||||||
selected && (scope === 'selected' || scope === 'auto');
|
selected && (scope === 'selected' || scope === 'auto');
|
||||||
const visible =
|
const visible =
|
||||||
!selectedWillBeReturned && shouldScanVisibleEntities(cameraHeightM)
|
!selectedWillBeReturned && shouldScanVisibleEntities(cameraHeightM)
|
||||||
? visibleEntityContexts(viewer, dataManager, {
|
? visibleEntityContexts(engine, dataManager, {
|
||||||
layerId,
|
layerId,
|
||||||
limit,
|
limit,
|
||||||
target: viewTarget,
|
target: viewTarget,
|
||||||
@@ -3420,7 +3477,7 @@ function describeTargetEntity(dataManager, target) {
|
|||||||
|
|
||||||
/** get_entity_context for a pointed/referenced target (scope 'target'). */
|
/** get_entity_context for a pointed/referenced target (scope 'target'). */
|
||||||
async function getTargetContext(
|
async function getTargetContext(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
styleManager,
|
styleManager,
|
||||||
args,
|
args,
|
||||||
@@ -3440,10 +3497,10 @@ async function getTargetContext(
|
|||||||
const hasPosition = Number.isFinite(lat) && Number.isFinite(lon);
|
const hasPosition = Number.isFinite(lat) && Number.isFinite(lon);
|
||||||
const viewTarget = hasPosition
|
const viewTarget = hasPosition
|
||||||
? geo.cartographic.fromDegrees(lon, lat)
|
? geo.cartographic.fromDegrees(lon, lat)
|
||||||
: getViewTargetCartographic(viewer);
|
: getViewTargetCartographic(engine);
|
||||||
const selected = describeTargetEntity(dataManager, target);
|
const selected = describeTargetEntity(dataManager, target);
|
||||||
const scene = await getSceneContext(
|
const scene = await getSceneContext(
|
||||||
viewer,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
viewTarget,
|
viewTarget,
|
||||||
@@ -3458,9 +3515,9 @@ async function getTargetContext(
|
|||||||
selected,
|
selected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const cameraHeightM = viewer.camera.positionCartographic.height;
|
const cameraHeightM = engine.camera.height();
|
||||||
const visible = shouldScanVisibleEntities(cameraHeightM)
|
const visible = shouldScanVisibleEntities(cameraHeightM)
|
||||||
? visibleEntityContexts(viewer, dataManager, {
|
? visibleEntityContexts(engine, dataManager, {
|
||||||
layerId: normalizeLayerId(args.layerId || args.layer),
|
layerId: normalizeLayerId(args.layerId || args.layer),
|
||||||
limit: Math.round(clampNumber(args.limit, 1, 12, 5)),
|
limit: Math.round(clampNumber(args.limit, 1, 12, 5)),
|
||||||
target: viewTarget,
|
target: viewTarget,
|
||||||
@@ -3747,14 +3804,12 @@ function selectedEntityContext(dataManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function visibleEntityContexts(
|
function visibleEntityContexts(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
{ layerId = null, limit = 5, target = null } = {},
|
{ layerId = null, limit = 5, target = null } = {},
|
||||||
) {
|
) {
|
||||||
const nearbyRecords = [];
|
const nearbyRecords = [];
|
||||||
const canvas = viewer.scene.canvas;
|
const { width, height } = engine.host.size();
|
||||||
const width = canvas.clientWidth || canvas.width || 0;
|
|
||||||
const height = canvas.clientHeight || canvas.height || 0;
|
|
||||||
const centerX = width / 2;
|
const centerX = width / 2;
|
||||||
const centerY = height / 2;
|
const centerY = height / 2;
|
||||||
const targetLat = target ? geo.math.toDegrees(target.latitude) : null;
|
const targetLat = target ? geo.math.toDegrees(target.latitude) : null;
|
||||||
@@ -3795,10 +3850,7 @@ function visibleEntityContexts(
|
|||||||
for (const { record } of nearbyRecords) {
|
for (const { record } of nearbyRecords) {
|
||||||
const position = record.entity?.__localBaseCartesian;
|
const position = record.entity?.__localBaseCartesian;
|
||||||
if (!position) continue;
|
if (!position) continue;
|
||||||
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
const screen = engine.projector.worldToWindow(position);
|
||||||
viewer.scene,
|
|
||||||
position,
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
!screen ||
|
!screen ||
|
||||||
screen.x < 0 ||
|
screen.x < 0 ||
|
||||||
@@ -3843,14 +3895,14 @@ function insertNearestRecord(records, candidate, limit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getSceneContext(
|
async function getSceneContext(
|
||||||
viewer,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
viewTarget = null,
|
viewTarget = null,
|
||||||
service = defaultGeospatial,
|
service = defaultGeospatial,
|
||||||
) {
|
) {
|
||||||
const cartographic = geo.cartographic.fromCartesian(viewer.camera.positionWC);
|
const cartographic = cameraCartographic(engine);
|
||||||
const basemap = await getBasemapContext(viewer, viewTarget, service);
|
const basemap = await getBasemapContext(engine, viewTarget, service);
|
||||||
const enabledLayers = dataManager
|
const enabledLayers = dataManager
|
||||||
.getAll()
|
.getAll()
|
||||||
.filter((layer) => layer.enabled)
|
.filter((layer) => layer.enabled)
|
||||||
@@ -3873,17 +3925,14 @@ async function getSceneContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getBasemapContext(
|
async function getBasemapContext(
|
||||||
viewer,
|
engine,
|
||||||
viewTarget = null,
|
viewTarget = null,
|
||||||
service = defaultGeospatial,
|
service = defaultGeospatial,
|
||||||
) {
|
) {
|
||||||
const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service);
|
const { reverseGeocodeCache, nearbyPlacesCache } = cachesFor(service);
|
||||||
const target = viewTarget;
|
const target = viewTarget;
|
||||||
const samples = sampleViewportCartographics(viewer);
|
const samples = sampleViewportCartographics(engine);
|
||||||
const cameraCartographic = geo.cartographic.fromCartesian(
|
const cameraHeightM = cameraCartographic(engine).height;
|
||||||
viewer.camera.positionWC,
|
|
||||||
);
|
|
||||||
const cameraHeightM = cameraCartographic.height;
|
|
||||||
const viewScale = classifyViewScale(cameraHeightM);
|
const viewScale = classifyViewScale(cameraHeightM);
|
||||||
const cachedViewportPlaces = viewportPlacesFromCache(
|
const cachedViewportPlaces = viewportPlacesFromCache(
|
||||||
samples,
|
samples,
|
||||||
@@ -4076,23 +4125,23 @@ function coarseBasemapPlace(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewTargetCartographic(viewer) {
|
function getViewTargetCartographic(engine) {
|
||||||
const signature = cameraViewSignature(viewer);
|
const signature = cameraViewSignature(engine);
|
||||||
const cached = viewTargetCache.get(viewer);
|
const cached = viewTargetCache.get(engine);
|
||||||
if (
|
if (
|
||||||
cached?.signature === signature &&
|
cached?.signature === signature &&
|
||||||
performance.now() - cached.cachedAt < 2500
|
performance.now() - cached.cachedAt < 2500
|
||||||
) {
|
) {
|
||||||
return cached.target;
|
return cached.target;
|
||||||
}
|
}
|
||||||
const position = getViewTargetCartesian(viewer);
|
const position = getViewTargetCartesian(engine);
|
||||||
// `fromCartesian` still returns undefined for a point too near the ellipsoid
|
// `fromCartesian` still returns undefined for a point too near the ellipsoid
|
||||||
// center to project; normalize that to the same "no target" null the callers
|
// center to project; normalize that to the same "no target" null the callers
|
||||||
// already handle for a missed pick.
|
// already handle for a missed pick.
|
||||||
const target = position
|
const target = position
|
||||||
? geo.cartographic.fromCartesian(position) || null
|
? geo.cartographic.fromCartesian(position) || null
|
||||||
: null;
|
: null;
|
||||||
viewTargetCache.set(viewer, {
|
viewTargetCache.set(engine, {
|
||||||
signature,
|
signature,
|
||||||
target,
|
target,
|
||||||
cachedAt: performance.now(),
|
cachedAt: performance.now(),
|
||||||
@@ -4100,15 +4149,16 @@ function getViewTargetCartographic(viewer) {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cameraViewSignature(viewer) {
|
function cameraViewSignature(engine) {
|
||||||
const camera = viewer.camera;
|
const cartographic = cameraCartographic(engine);
|
||||||
const cartographic = camera.positionCartographic;
|
const height = engine.camera.height();
|
||||||
|
const { heading, pitch } = engine.camera.pose();
|
||||||
return [
|
return [
|
||||||
geo.math.toDegrees(cartographic.latitude).toFixed(5),
|
geo.math.toDegrees(cartographic.latitude).toFixed(5),
|
||||||
geo.math.toDegrees(cartographic.longitude).toFixed(5),
|
geo.math.toDegrees(cartographic.longitude).toFixed(5),
|
||||||
Math.round(cartographic.height / 2),
|
Math.round(height / 2),
|
||||||
camera.heading.toFixed(3),
|
heading.toFixed(3),
|
||||||
camera.pitch.toFixed(3),
|
pitch.toFixed(3),
|
||||||
].join(':');
|
].join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4120,55 +4170,32 @@ function cameraViewSignature(viewer) {
|
|||||||
* MISSED pick, so it falls through to the next stage rather than poisoning
|
* MISSED pick, so it falls through to the next stage rather than poisoning
|
||||||
* every caller downstream.
|
* every caller downstream.
|
||||||
*/
|
*/
|
||||||
function getViewTargetCartesian(viewer) {
|
function getViewTargetCartesian(engine) {
|
||||||
const scene = viewer.scene;
|
if (!engine) return null;
|
||||||
const canvas = scene.canvas;
|
const { width, height } = engine.host.size();
|
||||||
const width = canvas.clientWidth || canvas.width || 0;
|
const center = geo.cartesian2.create(width / 2, height / 2);
|
||||||
const height = canvas.clientHeight || canvas.height || 0;
|
const stages = [
|
||||||
const center = new Cesium.Cartesian2(width / 2, height / 2);
|
() =>
|
||||||
let position = null;
|
engine.picker.pickPositionSupported
|
||||||
|
? engine.picker.pickPosition(center)
|
||||||
if (scene.pickPositionSupported && typeof scene.pickPosition === 'function') {
|
: undefined,
|
||||||
|
() => engine.camera.pickEllipsoid(center),
|
||||||
|
() => engine.picker.pickGlobe(center),
|
||||||
|
];
|
||||||
|
for (const stage of stages) {
|
||||||
|
let position = null;
|
||||||
try {
|
try {
|
||||||
position = scene.pickPosition(center);
|
position = stage();
|
||||||
} catch {
|
} catch {
|
||||||
position = null;
|
position = null;
|
||||||
}
|
}
|
||||||
|
if (isPickedWorldPosition(position)) return position;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
if (
|
|
||||||
!isPickedWorldPosition(position) &&
|
|
||||||
viewer.camera &&
|
|
||||||
typeof viewer.camera.pickEllipsoid === 'function'
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
position = viewer.camera.pickEllipsoid(center, Cesium.Ellipsoid.WGS84);
|
|
||||||
} catch {
|
|
||||||
position = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isPickedWorldPosition(position) &&
|
|
||||||
viewer.camera &&
|
|
||||||
typeof viewer.camera.getPickRay === 'function'
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const ray = viewer.camera.getPickRay(center);
|
|
||||||
position = scene.globe?.pick(ray, scene) || null;
|
|
||||||
} catch {
|
|
||||||
position = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isPickedWorldPosition(position) ? position : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sampleViewportCartographics(viewer) {
|
function sampleViewportCartographics(engine) {
|
||||||
const scene = viewer.scene;
|
const { width, height } = engine.host.size();
|
||||||
const canvas = scene.canvas;
|
|
||||||
const width = canvas.clientWidth || canvas.width || 0;
|
|
||||||
const height = canvas.clientHeight || canvas.height || 0;
|
|
||||||
if (!width || !height) return [];
|
if (!width || !height) return [];
|
||||||
|
|
||||||
const points = [
|
const points = [
|
||||||
@@ -4183,9 +4210,8 @@ function sampleViewportCartographics(viewer) {
|
|||||||
|
|
||||||
const samples = [];
|
const samples = [];
|
||||||
for (const [x, y] of points) {
|
for (const [x, y] of points) {
|
||||||
const cartesian = viewer.camera.pickEllipsoid(
|
const cartesian = engine.camera.pickEllipsoid(
|
||||||
geo.cartesian2.create(width * x, height * y),
|
geo.cartesian2.create(width * x, height * y),
|
||||||
Cesium.Ellipsoid.WGS84,
|
|
||||||
);
|
);
|
||||||
if (!isPickedWorldPosition(cartesian)) continue;
|
if (!isPickedWorldPosition(cartesian)) continue;
|
||||||
const carto = geo.cartographic.fromCartesian(cartesian);
|
const carto = geo.cartographic.fromCartesian(cartesian);
|
||||||
@@ -4664,7 +4690,7 @@ function layerIsWarming(enabledAt, layerKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function analystProviders(
|
function analystProviders(
|
||||||
viewer,
|
engine,
|
||||||
dataManager,
|
dataManager,
|
||||||
{
|
{
|
||||||
recordLimitByLayer = null,
|
recordLimitByLayer = null,
|
||||||
@@ -4751,7 +4777,7 @@ function analystProviders(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getViewContext() {
|
getViewContext() {
|
||||||
const carto = viewer.camera.positionCartographic;
|
const carto = cameraCartographic(engine);
|
||||||
const altKm = carto.height / 1000;
|
const altKm = carto.height / 1000;
|
||||||
// View radius scales with altitude: street-level asks stay local,
|
// View radius scales with altitude: street-level asks stay local,
|
||||||
// country-level asks sweep wide. Clamped so "in view" is never absurd.
|
// country-level asks sweep wide. Clamped so "in view" is never absurd.
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
|
|||||||
import { presentResult } from './resultDisplay.js';
|
import { presentResult } from './resultDisplay.js';
|
||||||
import { attachVoiceResult } from './speech.js';
|
import { attachVoiceResult } from './speech.js';
|
||||||
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
||||||
import { registerViewerEngine } from '../globe/cesium/engineFromViewer.js';
|
import { engineFromViewer, registerViewerEngine } from '../globe/cesium/engineFromViewer.js';
|
||||||
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
|
|
||||||
test('every live basemap is reachable by its own id — no enum value without a voice alias', () => {
|
test('every live basemap is reachable by its own id — no enum value without a voice alias', () => {
|
||||||
// B1 regression: a stack added to MAP_STACKS (and the set_map_stack enum)
|
// B1 regression: a stack added to MAP_STACKS (and the set_map_stack enum)
|
||||||
@@ -125,6 +126,9 @@ function createVoiceNavigationHarness({ cockpitActive = false } = {}) {
|
|||||||
positionCartographic: Cesium.Cartographic.fromCartesian(position),
|
positionCartographic: Cesium.Cartographic.fromCartesian(position),
|
||||||
heading: Cesium.Math.toRadians(28),
|
heading: Cesium.Math.toRadians(28),
|
||||||
pitch: Cesium.Math.toRadians(-45),
|
pitch: Cesium.Math.toRadians(-45),
|
||||||
|
// The rest of the pose the engine camera reads (camera.pose()).
|
||||||
|
roll: 0,
|
||||||
|
frustum: { fovy: 1, aspectRatio: 1.5 },
|
||||||
cancelFlight() { order.push('cancel'); },
|
cancelFlight() { order.push('cancel'); },
|
||||||
flyToBoundingSphere() {
|
flyToBoundingSphere() {
|
||||||
order.push(`fly:${viewer.trackedEntity === undefined ? 'released' : 'owned'}`);
|
order.push(`fly:${viewer.trackedEntity === undefined ? 'released' : 'owned'}`);
|
||||||
@@ -2582,6 +2586,9 @@ function createPrewarmHarness({ pickPosition, positionCartographic } = {}) {
|
|||||||
positionWC: position,
|
positionWC: position,
|
||||||
heading: Cesium.Math.toRadians(28),
|
heading: Cesium.Math.toRadians(28),
|
||||||
pitch: Cesium.Math.toRadians(-45),
|
pitch: Cesium.Math.toRadians(-45),
|
||||||
|
// The rest of the pose the engine camera reads (camera.pose()).
|
||||||
|
roll: 0,
|
||||||
|
frustum: { fovy: 1, aspectRatio: 1.5 },
|
||||||
pickEllipsoid() { calls.pickEllipsoid += 1; return surface; },
|
pickEllipsoid() { calls.pickEllipsoid += 1; return surface; },
|
||||||
getPickRay() { calls.getPickRay += 1; return null; },
|
getPickRay() { calls.getPickRay += 1; return null; },
|
||||||
cancelFlight() {},
|
cancelFlight() {},
|
||||||
@@ -3236,7 +3243,27 @@ test('voice Radio: the keyless path applies no country filter the keyed path wou
|
|||||||
});
|
});
|
||||||
|
|
||||||
const testPlaceSearch = () => createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ });
|
const testPlaceSearch = () => createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ });
|
||||||
function createGevActionRunner(options) { return createActionRunner({ placeSearch: testPlaceSearch(), ...options }); }
|
function createGevActionRunner(options) {
|
||||||
|
registerTestEngine(options.viewer);
|
||||||
|
return createActionRunner({ placeSearch: testPlaceSearch(), ...options });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The runner drives the globe engine registered for its viewer, as the app's
|
||||||
|
* scene engine is (src/app/globeEngine.js): a test viewer gets the Cesium
|
||||||
|
* engine over it or, when its camera only states where it is
|
||||||
|
* (positionCartographic), a fake engine at that place.
|
||||||
|
*/
|
||||||
|
function registerTestEngine(viewer) {
|
||||||
|
if (!viewer || engineFromViewer(viewer)) return;
|
||||||
|
const camera = viewer.camera ?? {};
|
||||||
|
const place = Object.getOwnPropertyDescriptor(camera, 'positionCartographic')?.value;
|
||||||
|
registerViewerEngine(
|
||||||
|
viewer,
|
||||||
|
!camera.positionWC && place
|
||||||
|
? createFakeEngine({ longitude: place.longitude, latitude: place.latitude, altitude: place.height })
|
||||||
|
: createCesiumEngine(viewer),
|
||||||
|
);
|
||||||
|
}
|
||||||
function controlRadio(viewer, manager, args, options) { return runControlRadio(viewer, manager, args, { placeSearch: testPlaceSearch(), ...options }); }
|
function controlRadio(viewer, manager, args, options) { return runControlRadio(viewer, manager, args, { placeSearch: testPlaceSearch(), ...options }); }
|
||||||
|
|
||||||
test('ALPR common names toggle only the registered camera layer through the normal voice action', async () => {
|
test('ALPR common names toggle only the registered camera layer through the normal voice action', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
// The W1 voice actions on the globe engine (phase 3, Task 9): the same
|
||||||
|
// results as on the Cesium viewer, through engine.camera / picker /
|
||||||
|
// projector only, and a structured refusal for the actions that still need
|
||||||
|
// a viewer when there is none (the Navara engine).
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import * as geo from '../geo/index.js';
|
||||||
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
|
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
|
||||||
|
import {
|
||||||
|
UNAVAILABLE_ON_ENGINE,
|
||||||
|
VIEWER_ONLY_ACTIONS,
|
||||||
|
createGevActionRunner,
|
||||||
|
} from './gevActions.js';
|
||||||
|
import { cameraPoseKey } from './pointerContext.js';
|
||||||
|
import { createPointerPicker } from './pointerPick.js';
|
||||||
|
import { renderFreshFrame } from './realtimeViewport.js';
|
||||||
|
|
||||||
|
globalThis.window ||= {
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
requestIdleCallback: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const toRad = geo.math.toRadians;
|
||||||
|
|
||||||
|
/** A fake engine over Austin at `altitude`, looking straight down. */
|
||||||
|
function engineOverAustin(options = {}) {
|
||||||
|
return createFakeEngine({
|
||||||
|
longitude: toRad(-97.74),
|
||||||
|
latitude: toRad(30.27),
|
||||||
|
altitude: 20_000,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The UI navigation seam, without its policy: runs the move. */
|
||||||
|
const navigatingStyleManager = (extra = {}) => ({
|
||||||
|
runImmediateNavigation: (noun, navigate) => navigate(),
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
function dataManagerWith(modules, { enabled = Object.keys(modules) } = {}) {
|
||||||
|
const on = new Set(enabled);
|
||||||
|
const params = [];
|
||||||
|
return {
|
||||||
|
params,
|
||||||
|
layers: new Map(
|
||||||
|
Object.entries(modules).map(([id, module]) => [id, { id, module }]),
|
||||||
|
),
|
||||||
|
isEnabled: (id) => on.has(id),
|
||||||
|
getAll: () =>
|
||||||
|
Object.keys(modules).map((id) => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
enabled: on.has(id),
|
||||||
|
stats: modules[id]?.getStats?.() || {},
|
||||||
|
})),
|
||||||
|
setLayerParams(id, values) {
|
||||||
|
params.push({ id, values });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async setEnabled(id, value) {
|
||||||
|
if (value) on.add(id);
|
||||||
|
else on.delete(id);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnerOn = (engine, options = {}) =>
|
||||||
|
createGevActionRunner({
|
||||||
|
viewer: null,
|
||||||
|
engine,
|
||||||
|
styleManager: navigatingStyleManager(),
|
||||||
|
dataManager: dataManagerWith({}),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const follow = (engine, position) =>
|
||||||
|
engine.camera.follow({
|
||||||
|
position: () => position,
|
||||||
|
viewFrom: { x: 0, y: -2000, z: 800 },
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the viewer-only actions are one exported list of real tools', () => {
|
||||||
|
assert.deepEqual(VIEWER_ONLY_ACTIONS, [
|
||||||
|
'control_scene',
|
||||||
|
'annotate_map',
|
||||||
|
'clear_annotations',
|
||||||
|
'fly_route',
|
||||||
|
'move_camera',
|
||||||
|
'control_cockpit',
|
||||||
|
'set_context_mode',
|
||||||
|
'control_radio',
|
||||||
|
'control_cctv',
|
||||||
|
]);
|
||||||
|
assert.ok(Object.isFrozen(VIEWER_ONLY_ACTIONS));
|
||||||
|
const tools = new Set(GEV_ACTION_SCHEMAS.map((schema) => schema.name));
|
||||||
|
for (const name of VIEWER_ONLY_ACTIONS)
|
||||||
|
assert.ok(tools.has(name), `${name} is a voice tool`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('without a viewer the viewer-only actions refuse with a structured result', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const touched = [];
|
||||||
|
const styleManager = new Proxy(navigatingStyleManager(), {
|
||||||
|
get(target, key) {
|
||||||
|
if (typeof key === 'string') touched.push(key);
|
||||||
|
return target[key];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const runner = runnerOn(engine, { styleManager });
|
||||||
|
engine.fake.resetCalls();
|
||||||
|
for (const name of VIEWER_ONLY_ACTIONS) {
|
||||||
|
const result = await runner(name, { action: 'status' });
|
||||||
|
assert.equal(result.ok, false, name);
|
||||||
|
assert.equal(result.action, name);
|
||||||
|
assert.equal(result.unavailable, true, name);
|
||||||
|
assert.equal(result.code, UNAVAILABLE_ON_ENGINE, name);
|
||||||
|
assert.equal(result.engine, 'navara', name);
|
||||||
|
assert.equal(result.error, 'Unavailable on this engine', name);
|
||||||
|
}
|
||||||
|
assert.deepEqual(touched, [], 'a refusal reaches no controller');
|
||||||
|
assert.deepEqual(engine.fake.calls, [], 'a refusal moves no camera');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a viewer the viewer-only actions still run', async () => {
|
||||||
|
const runner = createGevActionRunner({
|
||||||
|
viewer: {
|
||||||
|
clock: { onTick: { addEventListener: () => () => {} } },
|
||||||
|
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||||
|
camera: { moveEnd: { addEventListener() {} } },
|
||||||
|
},
|
||||||
|
engine: engineOverAustin(),
|
||||||
|
styleManager: {},
|
||||||
|
dataManager: dataManagerWith({}),
|
||||||
|
});
|
||||||
|
const radio = await runner('control_radio', { action: 'status' });
|
||||||
|
assert.equal(radio.error, 'Radio layer unavailable');
|
||||||
|
assert.equal(radio.unavailable, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the runner warms the view target on the engine camera, once per engine', () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
runnerOn(engine);
|
||||||
|
runnerOn(engine);
|
||||||
|
assert.equal(engine.camera.moveEnd.numberOfListeners, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stop_tracking stops the engine follow', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const target = geo.cartesian3.fromDegrees(-97.7, 30.3, 9000);
|
||||||
|
follow(engine, target);
|
||||||
|
assert.ok(engine.camera.following());
|
||||||
|
const released = [];
|
||||||
|
const runner = runnerOn(engine, {
|
||||||
|
dataManager: dataManagerWith({
|
||||||
|
flights: {
|
||||||
|
getTrackedInfo: () => ({ id: 'abc' }),
|
||||||
|
stopTracking: () => released.push('flights'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await runner('stop_tracking');
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(result.released, ['flights']);
|
||||||
|
assert.equal(engine.camera.following(), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zoom_to_globe leaves the follow and flies the engine camera', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
follow(engine, geo.cartesian3.fromDegrees(-97.7, 30.3, 9000));
|
||||||
|
const runner = runnerOn(engine);
|
||||||
|
const result = await runner('zoom_to_globe');
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.heightKm > 1000, true);
|
||||||
|
assert.equal(engine.camera.following(), null);
|
||||||
|
assert.equal(engine.fake.callsOf('camera.flyTo').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fly_to_location to coordinates leaves the follow and flies the engine', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
follow(engine, geo.cartesian3.fromDegrees(-97.7, 30.3, 9000));
|
||||||
|
const runner = runnerOn(engine);
|
||||||
|
const result = await runner('fly_to_location', {
|
||||||
|
latitude: 51.5,
|
||||||
|
longitude: -0.12,
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.navigationMode, 'close-coordinate');
|
||||||
|
assert.equal(engine.camera.following(), null);
|
||||||
|
assert.equal(engine.fake.callsOf('camera.flyToSphere').length, 1);
|
||||||
|
engine.fake.frames(400, 16);
|
||||||
|
const place = engine.camera.pose().cartographic;
|
||||||
|
assert.ok(Math.abs(geo.math.toDegrees(place.latitude) - 51.5) < 0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('track_entity flies to a vessel through the engine', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
let selected = null;
|
||||||
|
const runner = runnerOn(engine, {
|
||||||
|
dataManager: dataManagerWith({
|
||||||
|
'ais-live-vessels': {
|
||||||
|
findByQuery: () => ({
|
||||||
|
mmsi: 366999999,
|
||||||
|
name: 'EVER GIVEN',
|
||||||
|
latitude: 29.7,
|
||||||
|
longitude: -95.0,
|
||||||
|
}),
|
||||||
|
selectById: (mmsi) => (selected = mmsi),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await runner('track_entity', {
|
||||||
|
query: 'ever given',
|
||||||
|
layerId: 'ais-live-vessels',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.label, 'EVER GIVEN');
|
||||||
|
assert.equal(selected, 366999999);
|
||||||
|
assert.equal(engine.fake.callsOf('camera.flyToSphere').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('frame_overhead frames the nearby contacts with flyToSphere as on Cesium', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const center = engine.camera.pickEllipsoid({ x: 512, y: 384 });
|
||||||
|
const positions = [
|
||||||
|
geo.cartesian3.fromDegrees(-97.7, 30.3, 9000),
|
||||||
|
geo.cartesian3.fromDegrees(-97.9, 30.1, 11000),
|
||||||
|
geo.cartesian3.fromDegrees(-97.5, 30.5, 10000),
|
||||||
|
];
|
||||||
|
let asked = null;
|
||||||
|
const runner = runnerOn(engine, {
|
||||||
|
dataManager: dataManagerWith({
|
||||||
|
flights: {
|
||||||
|
getNearby(at, radiusM, limit) {
|
||||||
|
asked = { at, radiusM, limit };
|
||||||
|
return positions.map((position, index) => ({
|
||||||
|
id: `F${index}`,
|
||||||
|
icao24: `a${index}`,
|
||||||
|
label: `F${index}`,
|
||||||
|
position,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const heading = engine.camera.pose().heading;
|
||||||
|
const result = await runner('frame_overhead', { target: 'flights' });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.count, 3);
|
||||||
|
assert.equal(asked.radiusM, 150_000);
|
||||||
|
assert.equal(asked.limit, 80);
|
||||||
|
assert.ok(geo.cartesian3.distance(asked.at, center) < 1e-6);
|
||||||
|
// The same sphere and offset the Cesium path flew.
|
||||||
|
const sphere = Cesium.BoundingSphere.fromPoints(positions);
|
||||||
|
const radius = Math.max(sphere.radius * 1.25, 8000);
|
||||||
|
const [call] = engine.fake.callsOf('camera.flyToSphere');
|
||||||
|
const [flown, options] = call.args;
|
||||||
|
assert.ok(geo.cartesian3.distance(flown.center, sphere.center) < 1e-6);
|
||||||
|
assert.equal(flown.radius, radius);
|
||||||
|
assert.equal(options.duration, 2);
|
||||||
|
assert.equal(options.heading, heading);
|
||||||
|
assert.equal(options.pitch, toRad(-62));
|
||||||
|
assert.equal(options.range, radius * 2.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next_iss_pass observes from the engine camera', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
let observer = null;
|
||||||
|
const runner = runnerOn(engine, {
|
||||||
|
dataManager: dataManagerWith({
|
||||||
|
satellites: {
|
||||||
|
getNextIssPass(options) {
|
||||||
|
observer = options;
|
||||||
|
return { status: 'none' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await runner('next_iss_pass');
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.ok(Math.abs(observer.latDeg - 30.27) < 1e-6);
|
||||||
|
assert.ok(Math.abs(observer.lonDeg + 97.74) < 1e-6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get_current_view_state reads the engine camera', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const runner = runnerOn(engine, {
|
||||||
|
styleManager: navigatingStyleManager({ activeStyle: 'noir' }),
|
||||||
|
});
|
||||||
|
const result = await runner('get_current_view_state');
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.ok(Math.abs(result.camera.latitude - 30.27) < 1e-6);
|
||||||
|
assert.ok(Math.abs(result.camera.longitude + 97.74) < 1e-6);
|
||||||
|
assert.ok(Math.abs(result.camera.heightM - 20_000) < 1e-3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adjust_camera_zoom moves the engine camera toward the view target', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const runner = runnerOn(engine);
|
||||||
|
const result = await runner('adjust_camera_zoom', {
|
||||||
|
direction: 'in',
|
||||||
|
amount: 'medium',
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.movementRequestedM, 11_000);
|
||||||
|
assert.equal(result.movementActualM, 11_000);
|
||||||
|
assert.equal(result.afterHeightM, 9000);
|
||||||
|
assert.equal(engine.fake.callsOf('camera.cancelFlight').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get_entity_context samples the view through the engine', async () => {
|
||||||
|
// Continental: no reverse geocoding, every viewport sample on the globe.
|
||||||
|
const engine = engineOverAustin({ kind: 'navara', altitude: 4_000_000 });
|
||||||
|
const runner = runnerOn(engine);
|
||||||
|
const result = await runner('get_entity_context', { scope: 'in_view' });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.scene.camera.heightM, 4_000_000);
|
||||||
|
assert.equal(result.scene.basemap.viewScale, 'continental');
|
||||||
|
assert.equal(result.scene.basemap.viewportSamples.length, 7);
|
||||||
|
// The view centre, on the ellipsoid under the camera.
|
||||||
|
const [centre] = result.scene.basemap.viewportSamples;
|
||||||
|
assert.ok(Math.abs(centre.latitude - 30.27) < 0.1);
|
||||||
|
assert.equal(centre.longitude, -97.74);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('select_nearest_aircraft runs on the engine end to end', async () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
let tracked = null;
|
||||||
|
const flights = {
|
||||||
|
getStats: () => ({ count: 2, source: 'fixture' }),
|
||||||
|
getAnalystRecords: () => [
|
||||||
|
{
|
||||||
|
id: 'FAR1',
|
||||||
|
icao24: 'f1',
|
||||||
|
callsign: 'FAR1',
|
||||||
|
lat: 30.4,
|
||||||
|
lon: -97.7,
|
||||||
|
onGround: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'NEAR1',
|
||||||
|
icao24: 'n1',
|
||||||
|
callsign: 'NEAR1',
|
||||||
|
lat: 30.3,
|
||||||
|
lon: -97.7,
|
||||||
|
onGround: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
findByQuery: (query) =>
|
||||||
|
query === 'n1'
|
||||||
|
? { icao24: 'n1', callsign: 'NEAR1', latitude: 30.3, longitude: -97.7 }
|
||||||
|
: null,
|
||||||
|
trackById: (id) => (tracked = id),
|
||||||
|
};
|
||||||
|
const dataManager = dataManagerWith({ flights }, { enabled: [] });
|
||||||
|
dataManager.refreshLayer = async () => true;
|
||||||
|
const runner = runnerOn(engine, { dataManager });
|
||||||
|
const pending = runner('select_nearest_aircraft', {
|
||||||
|
latitude: 30.27,
|
||||||
|
longitude: -97.74,
|
||||||
|
});
|
||||||
|
for (let index = 0; index < 50 && !tracked; index++) {
|
||||||
|
engine.fake.frames(20, 16);
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
const result = await pending;
|
||||||
|
assert.equal(result.ok, true, result.error);
|
||||||
|
assert.equal(result.aircraft.id, 'n1');
|
||||||
|
assert.equal(tracked, 'n1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the camera pose key reads the engine as it reads the viewer', () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
const pose = engine.camera.pose();
|
||||||
|
const viewer = {
|
||||||
|
camera: {
|
||||||
|
positionWC: pose.position,
|
||||||
|
heading: pose.heading,
|
||||||
|
pitch: pose.pitch,
|
||||||
|
roll: pose.roll,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(typeof cameraPoseKey(engine), 'string');
|
||||||
|
assert.equal(cameraPoseKey(engine), cameraPoseKey(viewer));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the pointer picker picks through the engine', () => {
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
engine.fake.script('pick', { id: { __gevContextId: 'none' } });
|
||||||
|
const pick = createPointerPicker({ engine, dataManager: null });
|
||||||
|
const result = pick(512, 384);
|
||||||
|
assert.equal(result.entity, null);
|
||||||
|
assert.ok(Math.abs(result.ground.lat - 30.27) < 1e-4);
|
||||||
|
assert.ok(Math.abs(result.ground.lon + 97.74) < 1e-4);
|
||||||
|
assert.equal(result.radiusKm, 15);
|
||||||
|
assert.equal(result.cameraKey, cameraPoseKey(engine));
|
||||||
|
const [screen] = engine.fake.callsOf('picker.pick')[0].args;
|
||||||
|
assert.deepEqual([screen.x, screen.y], [512, 384]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a fresh frame for capture comes from the engine render loop', async () => {
|
||||||
|
const previous = globalThis.document;
|
||||||
|
try {
|
||||||
|
globalThis.document = { hidden: false };
|
||||||
|
const engine = engineOverAustin({ kind: 'navara' });
|
||||||
|
engine.fake.resetCalls();
|
||||||
|
const fresh = renderFreshFrame(engine);
|
||||||
|
assert.equal(engine.fake.renderRequests, 1);
|
||||||
|
engine.fake.frame();
|
||||||
|
assert.equal(await fresh, true);
|
||||||
|
globalThis.document = { hidden: true };
|
||||||
|
assert.equal(await renderFreshFrame(engine), false);
|
||||||
|
assert.equal(engine.fake.renderRequests, 1, 'no render while hidden');
|
||||||
|
} finally {
|
||||||
|
globalThis.document = previous;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { createVoiceCommands } from './commands.js';
|
|||||||
import { cameraPoseKey, createPointerTracker } from './pointerContext.js';
|
import { cameraPoseKey, createPointerTracker } from './pointerContext.js';
|
||||||
import { createPointerPicker } from './pointerPick.js';
|
import { createPointerPicker } from './pointerPick.js';
|
||||||
import { createReferentRegistry } from './referents.js';
|
import { createReferentRegistry } from './referents.js';
|
||||||
|
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||||
export * from './realtimeController.js';
|
export * from './realtimeController.js';
|
||||||
|
|
||||||
const ACTION_NAMES = new Set(GEV_ACTION_SCHEMAS.map((schema) => schema.name));
|
const ACTION_NAMES = new Set(GEV_ACTION_SCHEMAS.map((schema) => schema.name));
|
||||||
@@ -30,10 +31,15 @@ export function withToolCatalog(runner, loadCatalog) {
|
|||||||
return runGevTool;
|
return runGevTool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compose the standalone action runner with the voice controls. */
|
/**
|
||||||
|
* Compose the standalone action runner with the voice controls, on the globe
|
||||||
|
* `engine` (by default the one registered for `options.viewer`). Without a
|
||||||
|
* viewer (the Navara engine) the runner refuses the viewer-only tools.
|
||||||
|
*/
|
||||||
export function initGevVoiceCommands(options) {
|
export function initGevVoiceCommands(options) {
|
||||||
const canvas = options.viewer?.scene?.canvas || null;
|
const engine = options.engine ?? engineFromViewer(options.viewer);
|
||||||
const cameraKey = () => cameraPoseKey(options.viewer);
|
const canvas = engine?.host.canvas || null;
|
||||||
|
const cameraKey = () => cameraPoseKey(engine);
|
||||||
// Point-and-ask: the tracker and referents are shared by the runner (which
|
// Point-and-ask: the tracker and referents are shared by the runner (which
|
||||||
// resolves 'pointer' and referent:n) and the controls (which snapshot the
|
// resolves 'pointer' and referent:n) and the controls (which snapshot the
|
||||||
// pointer at turn start and clear both when the session ends).
|
// pointer at turn start and clear both when the session ends).
|
||||||
@@ -42,7 +48,7 @@ export function initGevVoiceCommands(options) {
|
|||||||
element: canvas,
|
element: canvas,
|
||||||
cameraKey,
|
cameraKey,
|
||||||
pick: createPointerPicker({
|
pick: createPointerPicker({
|
||||||
viewer: options.viewer,
|
engine,
|
||||||
dataManager: options.dataManager,
|
dataManager: options.dataManager,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -55,7 +61,7 @@ export function initGevVoiceCommands(options) {
|
|||||||
pointer,
|
pointer,
|
||||||
referents,
|
referents,
|
||||||
runner: withToolCatalog(
|
runner: withToolCatalog(
|
||||||
createGevActionRunner({ ...options, deixis }),
|
createGevActionRunner({ ...options, engine, deixis }),
|
||||||
options.toolCatalog,
|
options.toolCatalog,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* Provider-neutral and Cesium-free: the pick is injected.
|
* Provider-neutral and Cesium-free: the pick is injected.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isGlobeEngine } from '../globe/contract.js';
|
||||||
|
|
||||||
/** Movement below this many CSS pixels is still the same dwell. */
|
/** Movement below this many CSS pixels is still the same dwell. */
|
||||||
export const POINTER_DWELL_PX = 12;
|
export const POINTER_DWELL_PX = 12;
|
||||||
/** Stillness that makes a dwell point. */
|
/** Stillness that makes a dwell point. */
|
||||||
@@ -57,11 +59,16 @@ const finite = finiteOrNull;
|
|||||||
* Camera pose identity: position to 1 m, orientation to about 0.06 degrees.
|
* Camera pose identity: position to 1 m, orientation to about 0.06 degrees.
|
||||||
* A pixel read under one key names nothing under another. Reads plain
|
* A pixel read under one key names nothing under another. Reads plain
|
||||||
* numbers only, so it needs no Cesium import.
|
* numbers only, so it needs no Cesium import.
|
||||||
* @param {object} viewer Cesium viewer.
|
* @param {object} globe The globe engine (its camera pose), or a Cesium
|
||||||
|
* viewer.
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
export function cameraPoseKey(viewer) {
|
export function cameraPoseKey(globe) {
|
||||||
const camera = viewer?.camera;
|
let camera = globe?.camera;
|
||||||
|
if (isGlobeEngine(globe)) {
|
||||||
|
const pose = camera.pose();
|
||||||
|
camera = { ...pose, positionWC: pose.position };
|
||||||
|
}
|
||||||
const p = camera?.positionWC;
|
const p = camera?.positionWC;
|
||||||
if (!p || !Number.isFinite(p.x)) return null;
|
if (!p || !Number.isFinite(p.x)) return null;
|
||||||
const angle = (value) => Math.round((Number(value) || 0) * 1000);
|
const angle = (value) => Math.round((Number(value) || 0) * 1000);
|
||||||
|
|||||||
+12
-14
@@ -11,37 +11,35 @@ export function pointerRadiusKm(cameraHeightM) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Cesium side of point-and-ask: one entity pick and one depth-aware world
|
* The globe side of point-and-ask: one object pick (engine.picker.pick) and
|
||||||
* pick at canvas CSS pixels. Called lazily by the pointer tracker (dwell and
|
* one depth-aware world pick at canvas CSS pixels, on either engine. Called
|
||||||
* snapshot only), never per mouse move.
|
* lazily by the pointer tracker (dwell and snapshot only), never per mouse
|
||||||
|
* move.
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {object} options.viewer Cesium viewer.
|
* @param {object} options.engine The globe engine.
|
||||||
* @param {object} [options.dataManager] Layer lifecycle.
|
* @param {object} [options.dataManager] Layer lifecycle.
|
||||||
* @param {Function} [options.pickWorldFromScreen] Normalized-pixel world pick.
|
* @param {Function} [options.pickWorldFromScreen] Normalized-pixel world pick.
|
||||||
* @returns {(x: number, y: number) => {entity: object|null, ground: object|null, radiusKm: number}|null}
|
* @returns {(x: number, y: number) => {entity: object|null, ground: object|null, radiusKm: number}|null}
|
||||||
*/
|
*/
|
||||||
export function createPointerPicker({
|
export function createPointerPicker({
|
||||||
viewer,
|
engine,
|
||||||
dataManager = null,
|
dataManager = null,
|
||||||
pickWorldFromScreen = defaultAnnotationResolver.pickWorldFromScreen,
|
pickWorldFromScreen = defaultAnnotationResolver.pickWorldFromScreen,
|
||||||
}) {
|
}) {
|
||||||
return function pickPointer(x, y) {
|
return function pickPointer(x, y) {
|
||||||
const scene = viewer?.scene;
|
if (!engine || engine.isDestroyed?.()) return null;
|
||||||
const canvas = scene?.canvas;
|
const { width, height } = engine.host.size();
|
||||||
if (!scene || !canvas) return null;
|
|
||||||
const width = canvas.clientWidth || canvas.width || 0;
|
|
||||||
const height = canvas.clientHeight || canvas.height || 0;
|
|
||||||
if (!width || !height) return null;
|
if (!width || !height) return null;
|
||||||
let picked = null;
|
let picked = null;
|
||||||
try {
|
try {
|
||||||
picked = scene.pick(geo.cartesian2.create(x, y));
|
picked = engine.picker.pick(geo.cartesian2.create(x, y)) ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
picked = null;
|
picked = null;
|
||||||
}
|
}
|
||||||
const entity = resolvePickedContext(picked, { dataManager });
|
const entity = resolvePickedContext(picked, { dataManager });
|
||||||
let ground = null;
|
let ground = null;
|
||||||
try {
|
try {
|
||||||
const hit = pickWorldFromScreen(viewer, x / width, y / height);
|
const hit = pickWorldFromScreen(engine, x / width, y / height);
|
||||||
if (hit && Number.isFinite(hit.lat) && Number.isFinite(hit.lon))
|
if (hit && Number.isFinite(hit.lat) && Number.isFinite(hit.lon))
|
||||||
ground = {
|
ground = {
|
||||||
lat: hit.lat,
|
lat: hit.lat,
|
||||||
@@ -54,8 +52,8 @@ export function createPointerPicker({
|
|||||||
return {
|
return {
|
||||||
entity,
|
entity,
|
||||||
ground,
|
ground,
|
||||||
radiusKm: pointerRadiusKm(viewer.camera?.positionCartographic?.height),
|
radiusKm: pointerRadiusKm(engine.camera.height()),
|
||||||
cameraKey: cameraPoseKey(viewer),
|
cameraKey: cameraPoseKey(engine),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,22 @@ import {
|
|||||||
hasStructuredViewIdentity,
|
hasStructuredViewIdentity,
|
||||||
} from './realtimeProtocol.js';
|
} from './realtimeProtocol.js';
|
||||||
import { cameraPoseKey } from './pointerContext.js';
|
import { cameraPoseKey } from './pointerContext.js';
|
||||||
|
import { isGlobeEngine } from '../globe/contract.js';
|
||||||
|
|
||||||
/** The live viewer's camera pose identity (Cesium-free read). */
|
/** The page's globe engine (both engines), else its Cesium viewer. */
|
||||||
|
function pageGlobe() {
|
||||||
|
const debug = globalThis.window?.__godsEyeView;
|
||||||
|
return debug?.engine ?? debug?.viewer ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The map canvas: the engine's host canvas, else the viewer's. */
|
||||||
|
function pageCanvas(globe) {
|
||||||
|
return isGlobeEngine(globe) ? globe.host.canvas : globe?.scene?.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The live camera pose identity (Cesium-free read). */
|
||||||
export function readViewerCameraKey() {
|
export function readViewerCameraKey() {
|
||||||
return cameraPoseKey(globalThis.window?.__godsEyeView?.viewer);
|
return cameraPoseKey(pageGlobe());
|
||||||
}
|
}
|
||||||
// Viewport-screenshot size guards (M13). The old code clamped WIDTH only, so a
|
// Viewport-screenshot size guards (M13). The old code clamped WIDTH only, so a
|
||||||
// tall portrait window produced an oversized capture whose dc.send could throw.
|
// tall portrait window produced an oversized capture whose dc.send could throw.
|
||||||
@@ -18,16 +30,18 @@ export const VIEWPORT_MAX_PIXELS = 1200 * 900;
|
|||||||
export const VIEWPORT_MAX_ENCODED_BYTES = 200 * 1024;
|
export const VIEWPORT_MAX_ENCODED_BYTES = 200 * 1024;
|
||||||
|
|
||||||
export async function captureViewportImage() {
|
export async function captureViewportImage() {
|
||||||
const viewer = window.__godsEyeView?.viewer;
|
// Both engines draw with preserveDrawingBuffer (Cesium's contextOptions;
|
||||||
|
// Navara's own WebGL2 canvas, S4), so the canvas holds the last frame.
|
||||||
|
const globe = pageGlobe();
|
||||||
const source =
|
const source =
|
||||||
viewer?.scene?.canvas ||
|
pageCanvas(globe) ||
|
||||||
document.querySelector('#cesiumContainer .cesium-widget canvas');
|
document.querySelector('#cesiumContainer .cesium-widget canvas');
|
||||||
if (!source || !source.width || !source.height) return null;
|
if (!source || !source.width || !source.height) return null;
|
||||||
// No fresh frame (hidden, or the bounded render wait timed out) → no
|
// No fresh frame (hidden, or the bounded render wait timed out) → no
|
||||||
// capture. The caller labels this image "Current"; a stale preserved
|
// capture. The caller labels this image "Current"; a stale preserved
|
||||||
// frame would feed the model old entities as current context. (perf
|
// frame would feed the model old entities as current context. (perf
|
||||||
// wave 2 fix)
|
// wave 2 fix)
|
||||||
const fresh = await renderFreshCesiumFrame(viewer);
|
const fresh = await renderFreshFrame(globe);
|
||||||
if (!fresh) return null;
|
if (!fresh) return null;
|
||||||
|
|
||||||
// Clamp BOTH dimensions by a total-pixel budget so tall portrait windows are
|
// Clamp BOTH dimensions by a total-pixel budget so tall portrait windows are
|
||||||
@@ -129,10 +143,10 @@ export function pointerCropRect({
|
|||||||
* the crop's normalized bounds in the full canvas, or null.
|
* the crop's normalized bounds in the full canvas, or null.
|
||||||
*/
|
*/
|
||||||
export async function capturePointerCrop(screenPx) {
|
export async function capturePointerCrop(screenPx) {
|
||||||
const viewer = window.__godsEyeView?.viewer;
|
const globe = pageGlobe();
|
||||||
const source = viewer?.scene?.canvas;
|
const source = pageCanvas(globe);
|
||||||
if (!source?.width || !source?.height || !screenPx) return null;
|
if (!source?.width || !source?.height || !screenPx) return null;
|
||||||
const fresh = await renderFreshPointerFrame(viewer);
|
const fresh = await renderFreshPointerFrame(globe);
|
||||||
if (!fresh) return null;
|
if (!fresh) return null;
|
||||||
const rect = pointerCropRect({
|
const rect = pointerCropRect({
|
||||||
x: screenPx.x,
|
x: screenPx.x,
|
||||||
@@ -186,10 +200,10 @@ export async function capturePointerCrop(screenPx) {
|
|||||||
* Pointer crops get one bounded recovery request for a slow render loop.
|
* Pointer crops get one bounded recovery request for a slow render loop.
|
||||||
* Full-viewport captures intentionally keep their single 400 ms attempt.
|
* Full-viewport captures intentionally keep their single 400 ms attempt.
|
||||||
*/
|
*/
|
||||||
export async function renderFreshPointerFrame(viewer) {
|
export async function renderFreshPointerFrame(globe) {
|
||||||
if (await renderFreshCesiumFrame(viewer)) return true;
|
if (await renderFreshFrame(globe)) return true;
|
||||||
if (typeof document !== 'undefined' && document.hidden) return false;
|
if (typeof document !== 'undefined' && document.hidden) return false;
|
||||||
return renderFreshCesiumFrame(viewer);
|
return renderFreshFrame(globe);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale (w, h) down so w*h <= maxPixels while preserving aspect ratio. Never
|
// Scale (w, h) down so w*h <= maxPixels while preserving aspect ratio. Never
|
||||||
@@ -228,8 +242,14 @@ export function estimateDataUrlBytes(dataUrl) {
|
|||||||
* when the bounded wait timed out. Callers must not label a non-fresh
|
* when the bounded wait timed out. Callers must not label a non-fresh
|
||||||
* canvas as current. (perf wave 2)
|
* canvas as current. (perf wave 2)
|
||||||
*/
|
*/
|
||||||
export async function renderFreshCesiumFrame(viewer) {
|
export async function renderFreshFrame(globe) {
|
||||||
const scene = viewer?.scene;
|
// The engine's frame events and render request, else the viewer's scene.
|
||||||
|
const scene = isGlobeEngine(globe)
|
||||||
|
? {
|
||||||
|
postRender: globe.frames.postRender,
|
||||||
|
requestRender: () => globe.render.request('voice-capture'),
|
||||||
|
}
|
||||||
|
: globe?.scene;
|
||||||
if (!scene) return false;
|
if (!scene) return false;
|
||||||
// While the document is hidden the render loop is suspended — don't
|
// While the document is hidden the render loop is suspended — don't
|
||||||
// secretly restart rendering for an optional screenshot, and don't pass
|
// secretly restart rendering for an optional screenshot, and don't pass
|
||||||
@@ -488,3 +508,6 @@ export class RealtimeViewport {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** renderFreshFrame for callers that hold a Cesium viewer. */
|
||||||
|
export const renderFreshCesiumFrame = renderFreshFrame;
|
||||||
Reference in new issue
Block a user