mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Open the cockpit on a tracked Navara aircraft
This commit is contained in:
1 parent
8b179a7e01
commit
2087b55b47
19 files changed
+142
-65
No files matched your search
@@ -39,7 +39,7 @@ export function navaraModeNotice(availability) {
|
||||
return Object.freeze({
|
||||
label: t('Experimental Navara engine'),
|
||||
detail: names.length
|
||||
? `Data layers: ${names.join(', ')}; the other layers, cockpit and director are unavailable`
|
||||
? `Data layers: ${names.join(', ')}; the other layers and director are unavailable`
|
||||
: 'No data layers on this engine yet; cockpit and director are unavailable',
|
||||
});
|
||||
}
|
||||
@@ -347,7 +347,7 @@ export function createViewerlessTools({
|
||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||
});
|
||||
// Voice on the engine (phase 3, Task 9): the W1 tools drive the engine;
|
||||
// the viewer-only ones (director, annotations, cockpit…) refuse.
|
||||
// the viewer-only ones (director, annotations…) refuse.
|
||||
const voiceCommands = initGevVoiceCommands({
|
||||
...voice,
|
||||
annotationResolver: operations.annotationResolver,
|
||||
|
||||
@@ -119,7 +119,7 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
||||
registry.register('navara', { satellites: () => ({}), flights: () => ({}) });
|
||||
assert.equal(
|
||||
navaraModeNotice(createLayerAvailability('navara', { registry })).detail,
|
||||
'Data layers: Live Flights, Satellites; the other layers, cockpit and director are unavailable',
|
||||
'Data layers: Live Flights, Satellites; the other layers and director are unavailable',
|
||||
);
|
||||
// The governor drives the engine's continuous rendering.
|
||||
assert.equal(isRenderGovernorInstalled(engine), true);
|
||||
|
||||
@@ -42,10 +42,10 @@ function ordered(source, needles, label) {
|
||||
test('Cockpit takeover invalidates deferred work before camera cancellation', () => {
|
||||
const enter = cockpitEnter.toString();
|
||||
ordered(enter, [
|
||||
'if (!info || !entity?.position) return false;',
|
||||
'if (!info || !entity) return false;',
|
||||
'this.onCameraTakeover?.();',
|
||||
'this.viewer.camera.cancelFlight();',
|
||||
'this.viewer.trackedEntity = undefined;',
|
||||
'this.host.camera.cancelFlight();',
|
||||
'this.host.release();',
|
||||
], 'Cockpit takeover');
|
||||
assert.match(
|
||||
ui,
|
||||
|
||||
@@ -223,7 +223,7 @@ test('Cockpit Escape handling precedes form-control shortcut suppression and foc
|
||||
assert.match(enter, /activeElement/);
|
||||
assert.match(enter, /mapViewButton.*focus|focus.*mapViewButton/s);
|
||||
assert.match(exit, /focus\(\{ preventScroll: true \}\)/);
|
||||
assert.match(exit, /this\.restoreTrackingFrame\(entity\)/);
|
||||
assert.match(exit, /this\.host\.restore\(entity, this\.restoreTrackingFrame\)/);
|
||||
assert.doesNotMatch(exit, /applyTrackedCameraFrame/);
|
||||
assert.match(
|
||||
ui,
|
||||
@@ -238,7 +238,7 @@ test('Cockpit shortcut failures do not leak and open utilities own the first Esc
|
||||
assert.match(keydown, /if \(this\.isUtilityDisclosureOpen\?\.\(\)\) return;/);
|
||||
assert.match(
|
||||
keydown,
|
||||
/const cockpitAttempt = !!\(\s*this\.readAircraftInfo\(\) && this\.viewer\.trackedEntity\?\.position\s*\);[\s\S]*?event\.preventDefault\(\);[\s\S]*?event\.stopImmediatePropagation\(\);[\s\S]*?!this\.isEntryAllowed\(\)/,
|
||||
/const cockpitAttempt = !!\(\s*this\.readAircraftInfo\(\) && this\.host\.current\(\)\s*\);[\s\S]*?event\.preventDefault\(\);[\s\S]*?event\.stopImmediatePropagation\(\);[\s\S]*?!this\.isEntryAllowed\(\)/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ test('the cockpit reads its aircraft from the layer that owns Cesium tracking',
|
||||
assert.match(read, /gevTrackedId/);
|
||||
// In cockpit mode the controller moves the entity off viewer.trackedEntity,
|
||||
// so its own handle is the tracked identity there.
|
||||
assert.match(read, /this\.viewer\?\.trackedEntity \|\| this\.trackedEntity/);
|
||||
assert.match(read, /this\.host\.followed\(\) \|\| this\.trackedEntity/);
|
||||
});
|
||||
|
||||
test('programmatic Context layer changes cannot bypass explicit expansion policy', () => {
|
||||
@@ -1165,7 +1165,7 @@ test('cockpit route direction uses self-contained vector artwork, not a font lig
|
||||
test('cockpit aircraft handoff invalidates the prior world-position anchor', () => {
|
||||
const match = _adoptTrackedEntity.toString();
|
||||
assert.ok(match, 'cockpit tracked-entity handoff block is missing');
|
||||
assert.match(match, /this\.viewer\.trackedEntity = undefined;/);
|
||||
assert.match(match, /this\.host\.release\(\);/);
|
||||
assert.match(match, /this\.cockpitAnchorValid = false;/);
|
||||
assert.match(
|
||||
match,
|
||||
|
||||
@@ -412,12 +412,11 @@ export function contextRestoreLayerIds(snapshot = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cockpit entry belongs to the operational Contacts context bundle. A tracked
|
||||
* aircraft alone is not sufficient: both observed-flight feeds must still be
|
||||
* active so the cockpit's surrounding-contact picture is not presented as a
|
||||
* complete context view when one source is disabled.
|
||||
* Contacts mode requires both observed-flight feeds. Navara also supports
|
||||
* first-person flight directly from a single selected aircraft layer.
|
||||
*
|
||||
* @param {object} input Current context and dependency visibility.
|
||||
* @param {boolean} [input.standaloneAircraft] Allow direct entry outside a Context mode.
|
||||
* @param {string|null} input.contextMode Active Context mode.
|
||||
* @param {boolean} input.contextModeChanging Whether the Context transaction is unsettled.
|
||||
* @param {boolean} input.flightsEnabled Whether Live Flights is enabled.
|
||||
@@ -425,11 +424,14 @@ export function contextRestoreLayerIds(snapshot = {}) {
|
||||
* @returns {boolean} Whether cockpit entry may be offered or activated.
|
||||
*/
|
||||
export function cockpitEntryAllowed({
|
||||
standaloneAircraft = false,
|
||||
contextMode,
|
||||
contextModeChanging,
|
||||
flightsEnabled,
|
||||
militaryEnabled,
|
||||
}) {
|
||||
if (standaloneAircraft && !contextMode)
|
||||
return !contextModeChanging && Boolean(flightsEnabled || militaryEnabled);
|
||||
return (
|
||||
contextMode === 'flights' &&
|
||||
!contextModeChanging &&
|
||||
|
||||
@@ -587,3 +587,12 @@ test('session bookkeeping ignores programmatic origins, non-visibility events, a
|
||||
}), false);
|
||||
assert.equal(snapshot.userAdded.size, 0);
|
||||
});
|
||||
|
||||
test('direct aircraft entry on Navara needs an enabled feed and an idle Context', () => {
|
||||
const input = { standaloneAircraft: true, contextMode: null, flightsEnabled: true };
|
||||
assert.equal(cockpitEntryAllowed(input), true);
|
||||
assert.equal(cockpitEntryAllowed({ ...input, flightsEnabled: false, militaryEnabled: true }), true);
|
||||
assert.equal(cockpitEntryAllowed({ ...input, flightsEnabled: false }), false);
|
||||
assert.equal(cockpitEntryAllowed({ ...input, contextModeChanging: true }), false);
|
||||
assert.equal(cockpitEntryAllowed({ ...input, contextMode: 'space-missions' }), false);
|
||||
});
|
||||
@@ -738,6 +738,7 @@ export function createAircraftNavaraRenderer({
|
||||
// The tracked glyph's distance scale is its bank's (as Cesium's).
|
||||
sprite._scaleByDistance = spec.scaleByDistance;
|
||||
sprite.spec = spec;
|
||||
sprite.resumeAircraftFollow = () => follow.start(sprite);
|
||||
sprite.viewFrom = null;
|
||||
trackedSprites.add(sprite);
|
||||
placeTracked(sprite);
|
||||
@@ -783,6 +784,7 @@ export function createAircraftNavaraRenderer({
|
||||
position: () => handle.gevDisplayPosition?.() ?? handle.spec.position(),
|
||||
};
|
||||
return engine.camera.follow({
|
||||
native: handle,
|
||||
position: () => handle.spec.position(),
|
||||
viewFrom: () => handle.viewFrom,
|
||||
label: card,
|
||||
|
||||
@@ -190,6 +190,7 @@ test('the tracked glyph reads its callbacks every frame; follow carries its read
|
||||
assert.equal(typeof stop, 'function');
|
||||
const following = engine.camera.following();
|
||||
assert.equal(following, handle.gevFollowHandle);
|
||||
assert.equal(following.native, handle, 'cockpit follows the rendered aircraft');
|
||||
assert.equal(following.label.id, 'flights:abc123');
|
||||
assert.equal(following.label.model(), handle.gevLabelModel);
|
||||
assert.equal(renderer.follow.isFollowing(handle), true);
|
||||
@@ -197,7 +198,10 @@ test('the tracked glyph reads its callbacks every frame; follow carries its read
|
||||
renderer.follow.release();
|
||||
assert.equal(engine.camera.following(), null);
|
||||
assert.equal(renderer.follow.current(), undefined);
|
||||
handle.resumeAircraftFollow();
|
||||
assert.equal(renderer.follow.current(), handle);
|
||||
renderer.tracked.remove(handle);
|
||||
assert.equal(engine.camera.following(), null);
|
||||
assert.equal(renderer.tracked.isLive(handle), false);
|
||||
renderer.destroy();
|
||||
engine.destroy();
|
||||
|
||||
@@ -371,6 +371,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._recording.hud = this.hud;
|
||||
this._cockpitCoordinator = new CockpitCoordinator({
|
||||
viewer,
|
||||
engine,
|
||||
services: {
|
||||
flightsLayer: services.flightsLayer,
|
||||
militaryFlightsLayer: services.militaryFlightsLayer,
|
||||
|
||||
@@ -45,7 +45,7 @@ export function update() {
|
||||
if (
|
||||
!info ||
|
||||
!this.trackedEntity ||
|
||||
!this.viewer.entities.contains(this.trackedEntity)
|
||||
!this.host.contains(this.trackedEntity)
|
||||
) {
|
||||
if (nowMs < this.contextNavigationDeadlineMs) return;
|
||||
this.exit({ restoreTracking: false });
|
||||
@@ -61,8 +61,8 @@ export function update() {
|
||||
return;
|
||||
this.lastCameraUpdateMs = nowMs;
|
||||
|
||||
const target = this.trackedEntity.position.getValue(
|
||||
this.viewer.clock.currentTime,
|
||||
const target = this.host.position(
|
||||
this.trackedEntity,
|
||||
this.scratchTarget,
|
||||
);
|
||||
if (!target) return;
|
||||
@@ -182,8 +182,8 @@ export function update() {
|
||||
)
|
||||
) {
|
||||
this.lastGroundProbeMs = nowMs;
|
||||
const viewerCartographic = this.viewer.camera.positionCartographic;
|
||||
this.services.sampleMeshFloorCells(this.viewer.scene, groundPoint, {
|
||||
const viewerCartographic = this.host.cartographic();
|
||||
this.services.sampleMeshFloorCells(this.host.surface, groundPoint, {
|
||||
excludeObjects: [this.trackedEntity],
|
||||
viewerLat: geo.math.toDegrees(viewerCartographic.latitude),
|
||||
viewerLon: geo.math.toDegrees(viewerCartographic.longitude),
|
||||
@@ -345,7 +345,7 @@ export function update() {
|
||||
}
|
||||
}
|
||||
|
||||
this.viewer.camera.setView({
|
||||
this.host.camera.setView({
|
||||
destination: this.scratchCamera,
|
||||
orientation: { direction: this.scratchForward, up: this.scratchUp },
|
||||
});
|
||||
|
||||
@@ -63,11 +63,14 @@ import {
|
||||
import { onKeyDown } from './cockpitInput.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
|
||||
import { cockpitHost } from './cockpitHost.js';
|
||||
|
||||
export class CockpitViewController {
|
||||
constructor(
|
||||
viewer,
|
||||
{
|
||||
services,
|
||||
engine = null,
|
||||
onVisionChange = null,
|
||||
getInitialVisionMode = null,
|
||||
onCameraTakeover = null,
|
||||
@@ -79,7 +82,7 @@ export class CockpitViewController {
|
||||
publish = {},
|
||||
} = {},
|
||||
) {
|
||||
this.viewer = viewer;
|
||||
this.host = cockpitHost(viewer, engine);
|
||||
this.services = services;
|
||||
this.destroyed = false;
|
||||
this.disposed = false;
|
||||
@@ -198,8 +201,8 @@ export class CockpitViewController {
|
||||
// from preRender makes 3D Tiles discover a new view after traversal and can
|
||||
// create a self-sustaining refinement loop under a moving cockpit camera.
|
||||
this._listenerRemovers.push(
|
||||
viewer.scene.preUpdate.addEventListener(() => this.update()),
|
||||
viewer.trackedEntityChanged.addEventListener(() => {
|
||||
this.host.preUpdate.addEventListener(() => this.update()),
|
||||
this.host.followChanged.addEventListener(() => {
|
||||
if (this.active) this._adoptTrackedEntity(performance.now());
|
||||
else this.syncEntry();
|
||||
}),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cockpitVisionModeForStyle } from '../cockpitVisionPolicy.js';
|
||||
export class CockpitCoordinator {
|
||||
constructor({
|
||||
viewer,
|
||||
engine,
|
||||
services,
|
||||
elements,
|
||||
operations,
|
||||
@@ -23,6 +24,7 @@ export class CockpitCoordinator {
|
||||
this,
|
||||
{
|
||||
viewer,
|
||||
engine,
|
||||
services,
|
||||
readDataManager,
|
||||
readContext,
|
||||
@@ -35,10 +37,7 @@ export class CockpitCoordinator {
|
||||
operations,
|
||||
);
|
||||
this._cockpitDisplayPortal = null;
|
||||
// The cockpit flies the Cesium camera after tracked aircraft; without a
|
||||
// viewer (the Navara engine) there is no cockpit and controlCockpit
|
||||
// reports it unavailable.
|
||||
this.cockpitView = viewer
|
||||
this.cockpitView = viewer || engine?.kind === 'navara'
|
||||
? this._createCockpitView(viewer, services)
|
||||
: null;
|
||||
}
|
||||
@@ -62,6 +61,7 @@ export class CockpitCoordinator {
|
||||
weatherCodeLabel,
|
||||
} = services;
|
||||
return new CockpitViewController(viewer, {
|
||||
engine: this.engine,
|
||||
services: {
|
||||
flightsLayer,
|
||||
militaryFlightsLayer,
|
||||
@@ -88,6 +88,7 @@ export class CockpitCoordinator {
|
||||
this._stampNavigation({ cancelPendingSelection: false }),
|
||||
isEntryAllowed: () =>
|
||||
cockpitEntryAllowed({
|
||||
standaloneAircraft: this.engine?.kind === 'navara',
|
||||
contextMode: this._contextMode,
|
||||
contextModeChanging: this._contextModeChanging,
|
||||
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
||||
@@ -136,7 +137,7 @@ export class CockpitCoordinator {
|
||||
// (including the voice model) read that as a broken half-entered state.
|
||||
const entryAllowed =
|
||||
!active &&
|
||||
Boolean(gateOpen && info && this.viewer?.trackedEntity?.position);
|
||||
Boolean(gateOpen && info && this.cockpitView?.host.current());
|
||||
return {
|
||||
active,
|
||||
entryAllowed,
|
||||
@@ -278,7 +279,9 @@ export class CockpitCoordinator {
|
||||
action: 'control_cockpit',
|
||||
error: this._contextModeChanging
|
||||
? 'Contacts is still starting up — try Cockpit again in a moment'
|
||||
: 'Contacts must be active to enter Cockpit — say "open contacts" first',
|
||||
: this.engine?.kind === 'navara'
|
||||
? 'Enable an aircraft layer and track a flight to enter Cockpit'
|
||||
: 'Contacts must be active to enter Cockpit — say "open contacts" first',
|
||||
state: this.getCockpitState(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Camera and aircraft ownership used by the shared first-person controller.
|
||||
export function cockpitHost(viewer, engine) {
|
||||
if (viewer) {
|
||||
return {
|
||||
preUpdate: viewer.scene.preUpdate,
|
||||
followChanged: viewer.trackedEntityChanged,
|
||||
followed: () => viewer.trackedEntity,
|
||||
current: () =>
|
||||
viewer.trackedEntity?.position ? viewer.trackedEntity : null,
|
||||
contains: (entity) => viewer.entities.contains(entity),
|
||||
position: (entity, result) =>
|
||||
entity.position.getValue(viewer.clock.currentTime, result),
|
||||
release: () => {
|
||||
viewer.trackedEntity = undefined;
|
||||
},
|
||||
restore: (entity, refocus) => {
|
||||
viewer.trackedEntity = entity;
|
||||
refocus(entity);
|
||||
},
|
||||
camera: viewer.camera,
|
||||
cartographic: () => viewer.camera.positionCartographic,
|
||||
surface: viewer.scene,
|
||||
inputs: (enabled) => {
|
||||
viewer.scene.screenSpaceCameraController.enableInputs = enabled;
|
||||
},
|
||||
};
|
||||
}
|
||||
const aircraft = () => {
|
||||
const target = engine.camera.following()?.native;
|
||||
return target?.resumeAircraftFollow ? target : null;
|
||||
};
|
||||
return {
|
||||
preUpdate: engine.frames.preUpdate,
|
||||
followChanged: engine.camera.followChanged,
|
||||
followed: aircraft,
|
||||
current: aircraft,
|
||||
contains: (target) => target.live,
|
||||
position: (target) => target.spec.position(),
|
||||
release: () => engine.camera.following()?.stop(),
|
||||
restore: (target) => target.resumeAircraftFollow(),
|
||||
camera: engine.camera,
|
||||
cartographic: () => engine.camera.pose().cartographic,
|
||||
surface: engine,
|
||||
inputs: (enabled) => engine.input.setCameraInputs(enabled),
|
||||
};
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function onKeyDown(event) {
|
||||
if (key === 'c' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
||||
if (!this.active) {
|
||||
const cockpitAttempt = !!(
|
||||
this.readAircraftInfo() && this.viewer.trackedEntity?.position
|
||||
this.readAircraftInfo() && this.host.current()
|
||||
);
|
||||
if (!cockpitAttempt) return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
export function readAircraftInfo() {
|
||||
// In cockpit mode the controller takes the entity off `viewer.trackedEntity`
|
||||
// (see update()), so the cockpit's own handle is the tracked identity there.
|
||||
const trackedEntity = this.viewer?.trackedEntity || this.trackedEntity;
|
||||
const trackedEntity = this.host.followed() || this.trackedEntity;
|
||||
return resolveTrackedAircraftInfo({
|
||||
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
|
||||
military: this.services.militaryFlightsLayer.getTrackedInfo?.() || null,
|
||||
@@ -77,7 +77,7 @@ export function syncEntry() {
|
||||
if (this.destroyed) return false;
|
||||
if (this.active) return;
|
||||
const info = this.readAircraftInfo();
|
||||
const trackedContact = !!(info && this.viewer.trackedEntity?.position);
|
||||
const trackedContact = !!(info && this.host.current());
|
||||
this.syncTr3bToggle(trackedContact ? info : null);
|
||||
const available = !!(this.isEntryAllowed() && trackedContact);
|
||||
// Change-only publishes: this runs on a preUpdate cadence, and a fresh
|
||||
@@ -108,22 +108,22 @@ export function navigateContext(direction, options = {}) {
|
||||
}
|
||||
|
||||
export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
||||
const nextEntity = this.viewer.trackedEntity;
|
||||
const nextEntity = this.host.current();
|
||||
if (
|
||||
!this.active ||
|
||||
!nextEntity?.position ||
|
||||
!nextEntity ||
|
||||
nextEntity === this.trackedEntity
|
||||
)
|
||||
return false;
|
||||
const info = suppliedInfo || this.readAircraftInfo();
|
||||
if (!info) return false;
|
||||
if (this.trackedEntity && this.viewer.entities.contains(this.trackedEntity)) {
|
||||
if (this.trackedEntity && this.host.contains(this.trackedEntity)) {
|
||||
this.trackedEntity.show = this.trackedEntityWasShown;
|
||||
}
|
||||
this.trackedEntity = nextEntity;
|
||||
this.trackedEntityWasShown = nextEntity.show;
|
||||
nextEntity.show = false;
|
||||
this.viewer.trackedEntity = undefined;
|
||||
this.host.release();
|
||||
this.cockpitAnchorValid = false;
|
||||
this.heading = normalizeHeading(info.track ?? 0);
|
||||
this.lastFrameMs = nowMs;
|
||||
@@ -140,15 +140,15 @@ export function enter() {
|
||||
if (this.active) return false;
|
||||
if (!this.isEntryAllowed()) return false;
|
||||
const info = this.readAircraftInfo();
|
||||
const entity = this.viewer.trackedEntity;
|
||||
if (!info || !entity?.position) return false;
|
||||
const entity = this.host.current();
|
||||
if (!info || !entity) return false;
|
||||
this.entryFocusOrigin =
|
||||
document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null;
|
||||
// Retire deferred navigation before cancelFlight can run its callbacks.
|
||||
this.onCameraTakeover?.();
|
||||
this.viewer.camera.cancelFlight();
|
||||
this.host.camera.cancelFlight();
|
||||
this.trackedEntity = entity;
|
||||
this.trackedEntityWasShown = entity.show;
|
||||
entity.show = false;
|
||||
@@ -169,8 +169,8 @@ export function enter() {
|
||||
// runs on rendered frames, so idle mode would freeze the cockpit solid.
|
||||
// (perf wave 2)
|
||||
this.services.holdContinuousRender('cockpit');
|
||||
this.viewer.trackedEntity = undefined;
|
||||
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
|
||||
this.host.release();
|
||||
this.host.inputs(false);
|
||||
document.body.classList.add('cockpit-mode');
|
||||
// Activation hides the entry and TR-3B chip; clearing the chip's signature
|
||||
// lets the exit-path syncEntry publish it again.
|
||||
@@ -237,14 +237,13 @@ export function exit({ restoreTracking = true } = {}) {
|
||||
this.routeView.visible = false;
|
||||
this.clearPredictiveRoute();
|
||||
this.setVisionMode('optical');
|
||||
this.viewer.scene.screenSpaceCameraController.enableInputs = true;
|
||||
if (entity && this.viewer.entities.contains(entity))
|
||||
this.host.inputs(true);
|
||||
if (entity && this.host.contains(entity))
|
||||
entity.show = this.trackedEntityWasShown;
|
||||
this.trackedEntityWasShown = true;
|
||||
this.dispatchCockpitModeChanged(false);
|
||||
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
|
||||
this.viewer.trackedEntity = entity;
|
||||
this.restoreTrackingFrame(entity);
|
||||
if (restoreTracking && entity && this.host.contains(entity)) {
|
||||
this.host.restore(entity, this.restoreTrackingFrame);
|
||||
}
|
||||
this.syncEntry();
|
||||
const restoreTarget =
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t, translateKnown } from '../i18n/index.js';
|
||||
import {
|
||||
SPACE_MISSIONS_LAYER_BLOCK_REASON,
|
||||
cockpitEntryAllowed,
|
||||
isExplicitUserIntentOrigin,
|
||||
parseContextLayerBlockReason,
|
||||
recordContextRestoreExplicitChange,
|
||||
@@ -245,15 +244,7 @@ export function _handleContextLayerChange(change) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.cockpitView?.active &&
|
||||
!cockpitEntryAllowed({
|
||||
contextMode: this._contextMode,
|
||||
contextModeChanging: this._contextModeChanging,
|
||||
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
||||
militaryEnabled: !!this._dataManager?.isEnabled('military'),
|
||||
})
|
||||
) {
|
||||
if (this.cockpitView?.active && !this.cockpitView.isEntryAllowed()) {
|
||||
this.cockpitView.exit({ restoreTracking: false });
|
||||
}
|
||||
this._syncContextModeButtons();
|
||||
|
||||
@@ -55,10 +55,12 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
||||
</div>
|
||||
<section id="context-flights-view" class="context-mode-view" role="tabpanel" aria-labelledby="global-context-flights-btn" :hidden="!ctx?.flightsActive">
|
||||
<div class="global-context-actions" :aria-label="t('Contact Context actions')">
|
||||
<button id="cockpit-entry" type="button" :hidden="!cockpit?.entryAvailable">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">flight</span>
|
||||
<span>{{ t('COCKPIT') }}</span>
|
||||
</button>
|
||||
<Teleport to="body" :disabled="!cockpit?.entryAvailable || ctx?.flightsActive">
|
||||
<button id="cockpit-entry" type="button" :hidden="!cockpit?.entryAvailable">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">flight</span>
|
||||
<span>{{ t('COCKPIT') }}</span>
|
||||
</button>
|
||||
</Teleport>
|
||||
<button id="installations-search-btn" class="panel-layer-toggle" type="button" :aria-disabled="ctx && String(ctx.installationsBusy)" :aria-busy="ctx && String(ctx.installationsBusy)">{{ t('SEARCH NEARBY SITES') }}</button>
|
||||
<UiButton id="tr3b-toggle" class="tr3b-toggle" :pressed="!!cockpit?.tr3b?.converted" :hidden="!cockpit?.tr3b" :aria-label="t('Reclassify tracked contact as TR-3B')" :title="cockpit?.tr3b?.converted ? t('Restore real aircraft') : t('Reclassify as TR-3B')">🛸</UiButton>
|
||||
</div>
|
||||
|
||||
@@ -229,7 +229,7 @@ const FRAME_TARGETS = new Map([
|
||||
/**
|
||||
* 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
|
||||
* (orbit, pan, tilt), Contacts (the context mode), Radio and
|
||||
* CCTV. Without a viewer (the Navara engine) each answers a structured
|
||||
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
||||
*/
|
||||
@@ -239,7 +239,6 @@ export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||
'clear_annotations',
|
||||
'fly_route',
|
||||
'move_camera',
|
||||
'control_cockpit',
|
||||
'set_context_mode',
|
||||
'control_radio',
|
||||
'control_cctv',
|
||||
@@ -265,7 +264,6 @@ export const UNAVAILABLE_FEATURES = Object.freeze({
|
||||
clear_annotations: 'Map annotation',
|
||||
fly_route: 'Route flying',
|
||||
move_camera: 'Camera motion (orbit, pan, tilt)',
|
||||
control_cockpit: 'The cockpit',
|
||||
set_context_mode: 'The Contacts mode',
|
||||
control_radio: 'Radio',
|
||||
control_cctv: 'CCTV',
|
||||
@@ -1078,7 +1076,7 @@ export function createGevActionRunner({
|
||||
contextState?.mode === 'flights' &&
|
||||
contextState?.active !== false &&
|
||||
contextState?.changing !== true;
|
||||
if (!contactsReady) {
|
||||
if (!contactsReady && engine?.kind !== 'navara') {
|
||||
const contextResult = await styleManager.setContextMode('flights', {
|
||||
signal: runOptions.signal,
|
||||
isCurrent: runOptions.isCurrent,
|
||||
|
||||
@@ -94,7 +94,6 @@ test('the viewer-only actions are one exported list of real tools', () => {
|
||||
'clear_annotations',
|
||||
'fly_route',
|
||||
'move_camera',
|
||||
'control_cockpit',
|
||||
'set_context_mode',
|
||||
'control_radio',
|
||||
'control_cctv',
|
||||
@@ -105,6 +104,24 @@ test('the viewer-only actions are one exported list of real tools', () => {
|
||||
assert.ok(tools.has(name), `${name} is a voice tool`);
|
||||
});
|
||||
|
||||
test('Navara enters cockpit on the tracked aircraft without starting Contacts', async () => {
|
||||
const engine = engineOverAustin({ kind: 'navara' });
|
||||
const runner = runnerOn(engine, {
|
||||
styleManager: {
|
||||
getAircraftTrackingTarget: () => ({ layerId: 'flights', id: '507c84' }),
|
||||
setContextMode: () => assert.fail('the selected flight already owns the camera'),
|
||||
controlCockpit: (action, { rollbackTarget }) => {
|
||||
assert.equal(action, 'enter');
|
||||
assert.equal(rollbackTarget.id, '507c84');
|
||||
return { ok: true, state: { active: true } };
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await runner('control_cockpit', { action: 'enter' });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.state.active, true);
|
||||
});
|
||||
|
||||
test('without a viewer the viewer-only actions refuse with a structured result', async () => {
|
||||
const engine = engineOverAustin({ kind: 'navara' });
|
||||
const touched = [];
|
||||
|
||||
Reference in new issue
Block a user