mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 20:34:02 +00:00
Read the followed target, not trackedEntity, in Core's follow readers
The tracked card, screen annotations, north-up, share links and Cockpit ask data/followed.js what the camera follows, so they work the same when a layer follows through the camera port. followed.js gains entityFollowTarget (a FollowTarget whose presentation reads the drawn entity's record), resumeFollowing and followedPosition.
This commit is contained in:
1 parent
2adec097de
commit
e5f8e5753a
16 files changed
+215
-84
No files matched your search
@@ -879,6 +879,7 @@
|
||||
"src/render/cesium/common.js",
|
||||
"src/ui/cockpitContext.js",
|
||||
"src/ui/cockpitController.js",
|
||||
"src/data/followed.js",
|
||||
"src/ui/cockpitDisplayPortal.js",
|
||||
"src/ui/displayPanelScroll.js",
|
||||
"src/ui/cockpitInput.js",
|
||||
@@ -4412,6 +4413,7 @@
|
||||
"src/navigationPolicy.js",
|
||||
"src/share/follow.js",
|
||||
"src/ui/shareRestoration.js",
|
||||
"src/data/followed.js",
|
||||
"src/ui/uiLifetime.js",
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
@@ -4426,7 +4428,8 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/data/entityRecords.js"
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/rendererOf.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf } from '../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { getOverlayPaintRect } from '../overlays/worldOverlay.js';
|
||||
import {
|
||||
@@ -8,7 +9,6 @@ import { t } from '../i18n/index.js';
|
||||
import { nextTick } from 'vue';
|
||||
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||
import { readEntityRecord } from '../data/entityRecords.js';
|
||||
import { heightsFor } from '../queries/heights.js';
|
||||
|
||||
/** A label whose detailed outline could not be resolved. */
|
||||
@@ -421,8 +421,8 @@ export function createScreenAnnotationRenderer(
|
||||
let top = painted?.y;
|
||||
let bottom = painted ? painted.y + painted.h : undefined;
|
||||
|
||||
const ent = viewer.trackedEntity;
|
||||
const displayPosition = readEntityRecord(ent)?.displayPosition;
|
||||
const followed = followedOf(viewer);
|
||||
const displayPosition = followed?.displayPosition;
|
||||
const world =
|
||||
typeof displayPosition === 'function' ? displayPosition() : null;
|
||||
const win = world
|
||||
@@ -435,7 +435,7 @@ export function createScreenAnnotationRenderer(
|
||||
|
||||
// Billboard box — centered on the anchor, magnified by scaleByDistance, from
|
||||
// the size the tracking layer recorded for its sprite.
|
||||
const sprite = readEntityRecord(ent)?.sprite;
|
||||
const sprite = followed?.sprite;
|
||||
if (sprite && win && Number.isFinite(win.x) && Number.isFinite(win.y)) {
|
||||
const baseW = sprite.width ?? 28;
|
||||
const baseH = sprite.height ?? 28;
|
||||
|
||||
@@ -45,7 +45,7 @@ test('Cockpit takeover invalidates deferred work before camera cancellation', ()
|
||||
'if (!info || !entity?.position) return false;',
|
||||
'this.onCameraTakeover?.();',
|
||||
'cameraOf(this.viewer).cancelFlight();',
|
||||
'this.viewer.trackedEntity = undefined;',
|
||||
'stopFollowing(this.viewer);',
|
||||
], 'Cockpit takeover');
|
||||
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\(\) && followedOf\(this\.viewer\)\?\.entity\?\.position\s*\);[\s\S]*?event\.preventDefault\(\);[\s\S]*?event\.stopImmediatePropagation\(\);[\s\S]*?!this\.isEntryAllowed\(\)/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -295,14 +295,14 @@ test('the Contact panel never hides itself out from under its own NEXT button',
|
||||
);
|
||||
});
|
||||
|
||||
test('the cockpit reads its aircraft from the layer that owns Cesium tracking', () => {
|
||||
test('the cockpit reads its aircraft from the layer the camera follows', () => {
|
||||
const read = readAircraftInfo.toString();
|
||||
assert.ok(read, 'readAircraftInfo is missing');
|
||||
assert.match(read, /resolveTrackedAircraftInfo\(\{/);
|
||||
assert.match(read, /readEntityRecord\([^)]*\)\?\.trackedId/);
|
||||
// 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, /trackedId: followed\?\.key/);
|
||||
// In cockpit mode the controller stops the camera following, so its own
|
||||
// handle is the tracked identity there.
|
||||
assert.match(read, /followedOf\(this\.viewer\) \|\| this\.trackedFollow/);
|
||||
});
|
||||
|
||||
test('programmatic Context layer changes cannot bypass explicit expansion policy', () => {
|
||||
@@ -1171,7 +1171,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, /stopFollowing\(this\.viewer\);/);
|
||||
assert.match(match, /this\.cockpitAnchorValid = false;/);
|
||||
assert.match(
|
||||
match,
|
||||
|
||||
+60
-1
@@ -7,7 +7,14 @@
|
||||
* Until every layer does, a layer may still track an entity of its own
|
||||
* through Cesium's `trackedEntity`; that entity is seen here through the
|
||||
* same fields, read from its entity record. Readers ask this module, never
|
||||
* `trackedEntity`, so they work either way.
|
||||
* `trackedEntity`, so they work either way. A layer that still draws its
|
||||
* target as an entity follows through the port with `entityFollowTarget`,
|
||||
* whose presentation fields read that same record.
|
||||
*
|
||||
* Core's readers (the tracked card, Cockpit, the share link's thing view,
|
||||
* north-up) still read the drawn entity and its `displayPosition`: a port
|
||||
* target without them is followed by the camera but not shown by them,
|
||||
* until those readers move with the drawing to the renderer.
|
||||
*/
|
||||
import { findCamera } from '../render/rendererOf.js';
|
||||
import { readEntityRecord } from './entityRecords.js';
|
||||
@@ -125,3 +132,55 @@ export function stopFollowing(viewer) {
|
||||
if (camera?.followed) camera.follow(null);
|
||||
else if (viewer.trackedEntity) viewer.trackedEntity = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A FollowTarget for a layer that draws its target as an entity of its own:
|
||||
* the camera fields given (`position`, `viewFrom`, `frame`, `minRange`, ...)
|
||||
* plus what Core shows about it, read live from the entity's record, so the
|
||||
* layer keeps writing that record as before. `entity` is the drawn entity.
|
||||
* @param {object} entity
|
||||
* @param {object} camera The FollowTarget's camera fields.
|
||||
* @returns {Followed & object}
|
||||
*/
|
||||
export function entityFollowTarget(entity, camera) {
|
||||
const target = Object.create(entityView(entity));
|
||||
// Defined, not assigned: the record's fields are getters, and a camera
|
||||
// field of the same name (`key`, say) takes their place.
|
||||
for (const [name, value] of Object.entries(camera))
|
||||
Object.defineProperty(target, name, {
|
||||
value,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow `followed` again after `stopFollowing`: a port target through the
|
||||
* camera port, a layer's own tracked entity through `trackedEntity`.
|
||||
* @param {object} viewer
|
||||
* @param {Followed|null} followed What `followedOf` gave.
|
||||
*/
|
||||
export function resumeFollowing(viewer, followed) {
|
||||
if (!viewer || !followed) return;
|
||||
if (entityViews.get(followed.entity) === followed)
|
||||
viewer.trackedEntity = followed.entity;
|
||||
else findCamera(viewer)?.follow(followed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where `followed` is shown this frame: the layer's display cache, else its
|
||||
* entity's position at the viewer's time; null when neither answers.
|
||||
* @param {object} viewer
|
||||
* @param {Followed|null} followed
|
||||
*/
|
||||
export function followedPosition(viewer, followed) {
|
||||
const display = followed?.displayPosition;
|
||||
const shown = typeof display === 'function' ? display() : null;
|
||||
return (
|
||||
shown ||
|
||||
followed?.entity?.position?.getValue?.(viewer?.clock?.currentTime) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,12 @@ import test from 'node:test';
|
||||
import * as Cesium from 'cesium';
|
||||
import { entityRecord } from './entityRecords.js';
|
||||
import {
|
||||
entityFollowTarget,
|
||||
followedOf,
|
||||
followedPosition,
|
||||
isFollowing,
|
||||
onFollowChanged,
|
||||
resumeFollowing,
|
||||
stopFollowing,
|
||||
} from './followed.js';
|
||||
import { bindRenderer } from '../render/rendererOf.js';
|
||||
@@ -121,3 +124,52 @@ test('a renderer bound after subscribing is still heard', (t) => {
|
||||
camera.follow(null);
|
||||
assert.deepEqual(seen, ['k', null]);
|
||||
});
|
||||
|
||||
test('an entity follow target reads its entity record live', () => {
|
||||
const entity = new Cesium.Entity({ id: 'drawn', name: 'N2' });
|
||||
const position = () => ({ x: 1, y: 2, z: 3 });
|
||||
const target = entityFollowTarget(entity, { position, minRange: 150 });
|
||||
assert.equal(target.key, 'entity:drawn');
|
||||
entityRecord(entity).trackedId = 'flights:abc';
|
||||
entityRecord(entity).labelModel = { title: 'N2' };
|
||||
assert.equal(target.key, 'flights:abc');
|
||||
assert.equal(target.labelModel.title, 'N2');
|
||||
assert.equal(target.entity, entity);
|
||||
assert.equal(target.position, position);
|
||||
assert.equal(target.minRange, 150);
|
||||
const keyed = entityFollowTarget(entity, { key: 'flights:given' });
|
||||
assert.equal(keyed.key, 'flights:given', 'a camera field takes the place');
|
||||
assert.equal(keyed.labelModel.title, 'N2');
|
||||
});
|
||||
|
||||
test('resumeFollowing follows a port target again, or re-tracks an entity', (t) => {
|
||||
const viewer = trackingViewer();
|
||||
const entity = new Cesium.Entity({ id: 'legacy' });
|
||||
viewer.trackedEntity = entity;
|
||||
const legacy = followedOf(viewer);
|
||||
stopFollowing(viewer);
|
||||
resumeFollowing(viewer, legacy);
|
||||
assert.equal(viewer.trackedEntity, entity);
|
||||
assert.equal(followedOf(viewer), legacy);
|
||||
viewer.trackedEntity = undefined;
|
||||
const camera = followingPort();
|
||||
t.after(bindRenderer(viewer, { camera }));
|
||||
const target = entityFollowTarget(new Cesium.Entity({ id: 'p' }), {});
|
||||
camera.follow(target);
|
||||
stopFollowing(viewer);
|
||||
resumeFollowing(viewer, target);
|
||||
assert.equal(camera.followed, target);
|
||||
resumeFollowing(viewer, null);
|
||||
assert.equal(camera.followed, target);
|
||||
});
|
||||
|
||||
test('followedPosition prefers the display cache over the entity position', () => {
|
||||
const at = new Cesium.Cartesian3(4, 5, 6);
|
||||
const viewer = { clock: { currentTime: Cesium.JulianDate.now() } };
|
||||
const entity = new Cesium.Entity({ id: 'e', position: at });
|
||||
const view = entityFollowTarget(entity, {});
|
||||
assert.deepEqual(followedPosition(viewer, view), at);
|
||||
entityRecord(entity).displayPosition = () => ({ x: 7, y: 8, z: 9 });
|
||||
assert.deepEqual(followedPosition(viewer, view), { x: 7, y: 8, z: 9 });
|
||||
assert.equal(followedPosition(viewer, null), null);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf, isFollowing, onFollowChanged } from './followed.js';
|
||||
import {
|
||||
clearOverlaySource,
|
||||
setOverlayEntries,
|
||||
@@ -126,7 +127,7 @@ function entryIdFor(entity) {
|
||||
}
|
||||
|
||||
function activeEntity() {
|
||||
return _viewer?.trackedEntity || _selectedContext?.entity || null;
|
||||
return followedOf(_viewer)?.entity || _selectedContext?.entity || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,11 +236,10 @@ export function initTrackedReadout(viewer) {
|
||||
if (_viewer) destroyTrackedReadout();
|
||||
_viewer = viewer;
|
||||
_overlayHost.setVisible(TRACKED_OVERLAY_SOURCE_ID, true);
|
||||
_trackedEntityChangedRemove =
|
||||
viewer.trackedEntityChanged?.addEventListener?.(() => {
|
||||
if (viewer.trackedEntity) _selectedContext = null;
|
||||
syncActiveEntity();
|
||||
}) || null;
|
||||
_trackedEntityChangedRemove = onFollowChanged(viewer, (followed) => {
|
||||
if (followed) _selectedContext = null;
|
||||
syncActiveEntity();
|
||||
});
|
||||
_contextSelectedHandler = (event) => {
|
||||
const record = event.detail;
|
||||
if (READOUT_CONTEXT_LAYERS.has(record?.layerId)) {
|
||||
@@ -263,7 +263,7 @@ export function initTrackedReadout(viewer) {
|
||||
};
|
||||
_aircraftSelectedHandler = () => {
|
||||
_selectedContext = null;
|
||||
if (!_viewer?.trackedEntity) clearTrackedSource();
|
||||
if (!isFollowing(_viewer)) clearTrackedSource();
|
||||
};
|
||||
window.addEventListener('gev:entity-selected', _contextSelectedHandler);
|
||||
window.addEventListener(
|
||||
|
||||
@@ -196,8 +196,8 @@ export class StyleManager extends ShellFacade {
|
||||
this._showGlobalStatusNotice(message, options),
|
||||
feedback: this._feedback,
|
||||
updateFeedback: () => this._updateGlobalLoadingFeedback(),
|
||||
applyFollowOffset: (entity, offset) =>
|
||||
applyShareFollowOffset(viewer, entity, offset),
|
||||
applyFollowOffset: (followed, offset) =>
|
||||
applyShareFollowOffset(viewer, followed, offset),
|
||||
});
|
||||
|
||||
this._visualSettings = new VisualSettings({
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { readEntityRecord } from '../data/entityRecords.js';
|
||||
import {
|
||||
followedOf,
|
||||
followedPosition,
|
||||
isFollowing,
|
||||
onFollowChanged,
|
||||
} from '../data/followed.js';
|
||||
|
||||
function trackedTarget(viewer) {
|
||||
const entity = viewer.trackedEntity;
|
||||
// Read the visual's sample first; sampling a flight CallbackProperty twice
|
||||
// can advance dead reckoning ahead of the displayed aircraft.
|
||||
return (
|
||||
readEntityRecord(entity)?.displayPosition?.() ||
|
||||
entity?.position?.getValue(viewer.clock?.currentTime)
|
||||
);
|
||||
// The visual's sample first: sampling a flight CallbackProperty twice can
|
||||
// advance dead reckoning ahead of the displayed aircraft.
|
||||
return followedPosition(viewer, followedOf(viewer));
|
||||
}
|
||||
|
||||
export const OBLIQUE_PITCH = Cesium.Math.toRadians(-35);
|
||||
@@ -75,7 +76,7 @@ export function pickViewTarget(viewer) {
|
||||
/** Describe the camera as an orbit around the current viewport center. */
|
||||
export function readCameraTargetFrame(viewer) {
|
||||
const camera = viewer?.camera;
|
||||
const target = viewer?.trackedEntity
|
||||
const target = isFollowing(viewer)
|
||||
? trackedTarget(viewer)
|
||||
: pickViewTarget(viewer);
|
||||
if (!camera || !target || !isPickedWorldPosition(camera.positionWC))
|
||||
@@ -112,7 +113,7 @@ function setCameraTargetFrame(viewer, frame) {
|
||||
const camera = viewer?.camera;
|
||||
if (!camera || !frame?.target || !Number.isFinite(frame.range)) return false;
|
||||
try {
|
||||
const trackedTransform = viewer.trackedEntity
|
||||
const trackedTransform = isFollowing(viewer)
|
||||
? Cesium.Matrix4.clone(camera.transform)
|
||||
: null;
|
||||
camera.lookAt(
|
||||
@@ -200,20 +201,20 @@ export function createCameraOrientationAnimator(
|
||||
cancel();
|
||||
if (!viewer.scene?.preUpdate?.addEventListener || duration <= 0)
|
||||
return setCameraTargetFrame(viewer, { ...frame, ...destination });
|
||||
const entity = viewer.trackedEntity;
|
||||
const followed = followedOf(viewer);
|
||||
pending = destination;
|
||||
const start = now();
|
||||
const headingDelta = Cesium.Math.negativePiToPi(
|
||||
destination.heading - frame.heading,
|
||||
);
|
||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||
if (viewer.isDestroyed?.() || viewer.trackedEntity !== entity) {
|
||||
if (viewer.isDestroyed?.() || followedOf(viewer) !== followed) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
const progress = Cesium.Math.clamp((now() - start) / duration, 0, 1);
|
||||
const eased = Cesium.EasingFunction.CUBIC_IN_OUT(progress);
|
||||
const target = entity ? trackedTarget(viewer) : frame.target;
|
||||
const target = followed ? trackedTarget(viewer) : frame.target;
|
||||
if (
|
||||
!isPickedWorldPosition(target) ||
|
||||
!setCameraTargetFrame(viewer, {
|
||||
@@ -338,7 +339,7 @@ export function bindCameraOrientationControls({
|
||||
// The exact tilt state is refreshed once per gesture, not once per frame —
|
||||
// moveEnd fires when the camera stops, which is where the pick is affordable.
|
||||
subscribe(viewer?.camera?.moveEnd, refreshTilt);
|
||||
subscribe(viewer?.trackedEntityChanged, animator.cancel);
|
||||
if (viewer) removers.push(onFollowChanged(viewer, animator.cancel));
|
||||
for (const type of ['pointerdown', 'wheel']) {
|
||||
const canvas = viewer?.scene?.canvas;
|
||||
canvas?.addEventListener?.(type, animator.cancel, { passive: true });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** Coordinate Cockpit lifecycle through supplied aircraft, terrain, rendering and briefing operations. */
|
||||
import { onFollowChanged } from '../data/followed.js';
|
||||
import {
|
||||
readAircraftInfo,
|
||||
dispatchCockpitModeChanged,
|
||||
@@ -87,6 +88,8 @@ export class CockpitViewController {
|
||||
/** Gives camera input back; held from enter to exit. */
|
||||
this.releaseCameraInput = null;
|
||||
this.trackedEntity = null;
|
||||
/** What the camera followed before Cockpit took it, to follow again. */
|
||||
this.trackedFollow = null;
|
||||
this.trackedEntityWasShown = true;
|
||||
this.heading = null;
|
||||
this.lastFrameMs = 0;
|
||||
@@ -201,7 +204,7 @@ export class CockpitViewController {
|
||||
// create a self-sustaining refinement loop under a moving cockpit camera.
|
||||
this._listenerRemovers.push(
|
||||
viewer.scene.preUpdate.addEventListener(() => this.update()),
|
||||
viewer.trackedEntityChanged.addEventListener(() => {
|
||||
onFollowChanged(viewer, () => {
|
||||
if (this.active) this._adoptTrackedEntity(performance.now());
|
||||
else this.syncEntry();
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf } from '../data/followed.js';
|
||||
import { nextTick } from 'vue';
|
||||
import { CockpitViewController, CockpitDisplayPortal } from './cockpit.js';
|
||||
import { cockpitEntryAllowed } from '../contextModePolicy.js';
|
||||
@@ -124,12 +125,12 @@ export class CockpitCoordinator {
|
||||
const gateOpen = Boolean(this.cockpitView?.isEntryAllowed?.());
|
||||
// "Could Cockpit be ENTERED right now" — so it is false while already
|
||||
// inside, unconditionally. Cockpit takes the entity off
|
||||
// `viewer.trackedEntity` on entry and NEXT puts one back, which made this
|
||||
// the camera's follow on entry and NEXT puts one back, which made this
|
||||
// flip true/false between calls while `active` stayed true; readers
|
||||
// (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 && followedOf(this.viewer)?.entity?.position);
|
||||
return {
|
||||
active,
|
||||
entryAllowed,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** Route Cockpit keys while preserving the active disclosure and form-control owner. */
|
||||
import { followedOf } from '../data/followed.js';
|
||||
|
||||
export function onKeyDown(event) {
|
||||
if (this.destroyed) return false;
|
||||
@@ -49,7 +50,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() && followedOf(this.viewer)?.entity?.position
|
||||
);
|
||||
if (!cockpitAttempt) return;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ import {
|
||||
normalizeHeading,
|
||||
resolveTrackedAircraftInfo,
|
||||
} from '../cockpitMath.js';
|
||||
import { readEntityRecord } from '../data/entityRecords.js';
|
||||
import {
|
||||
followedOf,
|
||||
resumeFollowing,
|
||||
stopFollowing,
|
||||
} from '../data/followed.js';
|
||||
import { inputFor } from '../input/dispatcher.js';
|
||||
|
||||
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;
|
||||
// In cockpit mode the controller stops the camera following (see enter()),
|
||||
// so the cockpit's own handle is the tracked identity there.
|
||||
const followed = followedOf(this.viewer) || this.trackedFollow;
|
||||
return resolveTrackedAircraftInfo({
|
||||
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
|
||||
military: this.services.militaryFlightsLayer.getTrackedInfo?.() || null,
|
||||
trackedId: readEntityRecord(trackedEntity)?.trackedId || '',
|
||||
trackedId: followed?.key || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,7 +84,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 && followedOf(this.viewer)?.entity?.position);
|
||||
this.syncTr3bToggle(trackedContact ? info : null);
|
||||
const available = !!(this.isEntryAllowed() && trackedContact);
|
||||
// Change-only publishes: this runs on a preUpdate cadence, and a fresh
|
||||
@@ -111,7 +115,8 @@ export function navigateContext(direction, options = {}) {
|
||||
}
|
||||
|
||||
export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
||||
const nextEntity = this.viewer.trackedEntity;
|
||||
const nextFollowed = followedOf(this.viewer);
|
||||
const nextEntity = nextFollowed?.entity;
|
||||
if (
|
||||
!this.active ||
|
||||
!nextEntity?.position ||
|
||||
@@ -124,9 +129,10 @@ export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
||||
this.trackedEntity.show = this.trackedEntityWasShown;
|
||||
}
|
||||
this.trackedEntity = nextEntity;
|
||||
this.trackedFollow = nextFollowed;
|
||||
this.trackedEntityWasShown = nextEntity.show;
|
||||
nextEntity.show = false;
|
||||
this.viewer.trackedEntity = undefined;
|
||||
stopFollowing(this.viewer);
|
||||
this.cockpitAnchorValid = false;
|
||||
this.heading = normalizeHeading(info.track ?? 0);
|
||||
this.lastFrameMs = nowMs;
|
||||
@@ -143,7 +149,8 @@ export function enter() {
|
||||
if (this.active) return false;
|
||||
if (!this.isEntryAllowed()) return false;
|
||||
const info = this.readAircraftInfo();
|
||||
const entity = this.viewer.trackedEntity;
|
||||
const followed = followedOf(this.viewer);
|
||||
const entity = followed?.entity;
|
||||
if (!info || !entity?.position) return false;
|
||||
this.entryFocusOrigin =
|
||||
document.activeElement instanceof HTMLElement
|
||||
@@ -153,6 +160,7 @@ export function enter() {
|
||||
this.onCameraTakeover?.();
|
||||
cameraOf(this.viewer).cancelFlight();
|
||||
this.trackedEntity = entity;
|
||||
this.trackedFollow = followed;
|
||||
this.trackedEntityWasShown = entity.show;
|
||||
entity.show = false;
|
||||
this.heading = normalizeHeading(info.track ?? 0);
|
||||
@@ -172,7 +180,7 @@ 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;
|
||||
stopFollowing(this.viewer);
|
||||
this.releaseCameraInput?.();
|
||||
this.releaseCameraInput = inputFor(this.viewer).suspendCamera('cockpit');
|
||||
document.body.classList.add('cockpit-mode');
|
||||
@@ -217,9 +225,11 @@ export function enter() {
|
||||
export function exit({ restoreTracking = true } = {}) {
|
||||
if (!this.active) return false;
|
||||
const entity = this.trackedEntity;
|
||||
const followed = this.trackedFollow;
|
||||
this.active = false;
|
||||
this.services.releaseContinuousRender('cockpit');
|
||||
this.trackedEntity = null;
|
||||
this.trackedFollow = null;
|
||||
this.heading = null;
|
||||
this.cockpitAnchorValid = false;
|
||||
this.surfaceWaitStartedMs = 0;
|
||||
@@ -248,7 +258,7 @@ export function exit({ restoreTracking = true } = {}) {
|
||||
this.trackedEntityWasShown = true;
|
||||
this.dispatchCockpitModeChanged(false);
|
||||
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
|
||||
this.viewer.trackedEntity = entity;
|
||||
resumeFollowing(this.viewer, followed);
|
||||
this.restoreTrackingFrame(entity);
|
||||
}
|
||||
this.syncEntry();
|
||||
|
||||
+27
-26
@@ -1,11 +1,11 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { trackedDisplayPositionForCamera } from '../data/trackedCamera.js';
|
||||
import { findCamera } from '../render/rendererOf.js';
|
||||
import { followedOf, followedPosition } from '../data/followed.js';
|
||||
import {
|
||||
SHARE_VIEW_ENTITY_KINDS,
|
||||
followOffsetFromEnu,
|
||||
followOffsetToEnu,
|
||||
} from '../share/follow.js';
|
||||
import { readEntityRecord } from '../data/entityRecords.js';
|
||||
|
||||
/**
|
||||
* The bridge between the camera following an entity and a share link's thing
|
||||
@@ -18,6 +18,12 @@ const KIND_BY_LAYER = new Map(
|
||||
);
|
||||
const MAX_FRAMES = 120;
|
||||
|
||||
/** Where `followed` is shown now, as a Cesium position, or null. */
|
||||
function shownAt(viewer, followed) {
|
||||
const p = followedPosition(viewer, followed);
|
||||
return p ? new Cesium.Cartesian3(p.x, p.y, p.z) : null;
|
||||
}
|
||||
|
||||
/** `position`'s local east/north/up offset from `origin`, in metres. */
|
||||
function enuOffset(origin, position) {
|
||||
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
||||
@@ -39,19 +45,13 @@ function enuOffset(origin, position) {
|
||||
export function readShareFollowSubject({ viewer, nowMs = Date.now() }) {
|
||||
const cameraPosition = viewer?.camera?.positionWC;
|
||||
if (!cameraPosition) return null;
|
||||
const tracked = viewer.trackedEntity;
|
||||
const trackedId = String(readEntityRecord(tracked)?.trackedId || '');
|
||||
const followed = followedOf(viewer);
|
||||
const trackedId = String(followed?.key || '');
|
||||
const separator = trackedId.indexOf(':');
|
||||
if (separator <= 0) return null;
|
||||
const kind = KIND_BY_LAYER.get(trackedId.slice(0, separator));
|
||||
const id = trackedId.slice(separator + 1);
|
||||
const target = kind
|
||||
? trackedDisplayPositionForCamera(
|
||||
tracked,
|
||||
viewer.clock.currentTime,
|
||||
new Cesium.Cartesian3(),
|
||||
)
|
||||
: null;
|
||||
const target = kind ? shownAt(viewer, followed) : null;
|
||||
if (!kind || !id || !target) return null;
|
||||
const offset = followOffsetFromEnu(enuOffset(target, cameraPosition));
|
||||
const fix = Cesium.Cartographic.fromCartesian(target);
|
||||
@@ -73,14 +73,14 @@ export function readShareFollowSubject({ viewer, nowMs = Date.now() }) {
|
||||
const MAX_TURN_PER_FRAME = Math.PI / 3;
|
||||
|
||||
/**
|
||||
* Put the camera at a shared follow offset from `entity` once following it
|
||||
* has framed the camera. Cesium keeps the offset afterwards, as it keeps one
|
||||
* Put the camera at a shared follow offset from `followed` (what
|
||||
* `followedOf` gave) once following it has framed the camera. Cesium keeps the offset afterwards, as it keeps one
|
||||
* the user orbits to. The camera swings round the entity at most 60° a frame:
|
||||
* the follow's own guard treats a jump to the far side as the camera crossing
|
||||
* through the entity and puts it back. Returns a disposer.
|
||||
*/
|
||||
export function applyShareFollowOffset(viewer, entity, offset) {
|
||||
if (!viewer?.scene?.preUpdate || !entity || !(offset?.range > 0))
|
||||
export function applyShareFollowOffset(viewer, followed, offset) {
|
||||
if (!viewer?.scene?.preUpdate || !followed || !(offset?.range > 0))
|
||||
return () => {};
|
||||
const local = followOffsetToEnu(offset);
|
||||
let frames = 0;
|
||||
@@ -90,23 +90,20 @@ export function applyShareFollowOffset(viewer, entity, offset) {
|
||||
remove = null;
|
||||
};
|
||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||
if (viewer.isDestroyed?.() || viewer.trackedEntity !== entity) {
|
||||
if (viewer.isDestroyed?.() || followedOf(viewer) !== followed) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
frames += 1;
|
||||
const camera = viewer.camera;
|
||||
// Let the follow frame the camera first (its first frame), then move it.
|
||||
const port = findCamera(viewer);
|
||||
const framed =
|
||||
frames > 1 &&
|
||||
!Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY);
|
||||
const target = framed
|
||||
? trackedDisplayPositionForCamera(
|
||||
entity,
|
||||
viewer.clock.currentTime,
|
||||
new Cesium.Cartesian3(),
|
||||
)
|
||||
: null;
|
||||
(port?.followed === followed
|
||||
? port.followFramed
|
||||
: !Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY));
|
||||
const target = framed ? shownAt(viewer, followed) : null;
|
||||
if (!target) {
|
||||
if (frames >= MAX_FRAMES) stop();
|
||||
return;
|
||||
@@ -136,8 +133,12 @@ export function applyShareFollowOffset(viewer, entity, offset) {
|
||||
: 0;
|
||||
const done = !(angle > MAX_TURN_PER_FRAME);
|
||||
const next = done ? goal : turnToward(from, goal, MAX_TURN_PER_FRAME);
|
||||
camera.lookAtTransform(camera.transform, next);
|
||||
viewer.scene.requestRender?.();
|
||||
// Through the port when it follows: its close-range guard keeps the eye.
|
||||
if (port?.followed === followed) port.followOffset.set(next);
|
||||
else {
|
||||
camera.lookAtTransform(camera.transform, next);
|
||||
viewer.scene.requestRender?.();
|
||||
}
|
||||
if (done) stop();
|
||||
});
|
||||
return stop;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../share/viewCodec.js';
|
||||
import { entityRecord, withEntityRecord } from '../data/entityRecords.js';
|
||||
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||
import { followedOf } from '../data/followed.js';
|
||||
|
||||
const NOW_MS = Date.now();
|
||||
const FIX = { lat: 37.6213, lon: -122.379, alt: 3200 };
|
||||
@@ -494,7 +495,7 @@ test('a shared offset on the far side swings round, never jumping through the en
|
||||
};
|
||||
// Looking south at the nose: the opposite side of the app's framing.
|
||||
const offset = { range: 5800, heading: 180, pitch: -35 };
|
||||
applyShareFollowOffset(viewer, entity, offset);
|
||||
applyShareFollowOffset(viewer, followedOf(viewer), offset);
|
||||
let previous = viewer.camera.position;
|
||||
for (let frame = 0; frame < 10 && listeners.length; frame += 1) {
|
||||
listeners[0]();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
||||
import { t, msg, formatDate } from '../i18n/index.js';
|
||||
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { readEntityRecord } from '../data/entityRecords.js';
|
||||
import { followedOf } from '../data/followed.js';
|
||||
|
||||
/** How long a shared vessel may take to appear in the vessel feed. */
|
||||
export const SHARED_VESSEL_WAIT_MS = 120_000;
|
||||
@@ -420,17 +420,16 @@ export class ShareRestoration {
|
||||
/** Frame a shared entity the camera now follows from the shared offset. */
|
||||
_applySharedFollowOffset(result) {
|
||||
const view = this._sharedThingFor(result);
|
||||
const entity = this.viewer?.trackedEntity;
|
||||
const followed = followedOf(this.viewer);
|
||||
// Only a link made while following carries a real offset (with the fix);
|
||||
// a long link's follow keeps the app's own framing.
|
||||
if (
|
||||
!view?.lastKnown ||
|
||||
String(readEntityRecord(entity)?.trackedId || '') !==
|
||||
`${result.layerId}:${result.targetId}`
|
||||
String(followed?.key || '') !== `${result.layerId}:${result.targetId}`
|
||||
)
|
||||
return;
|
||||
this._stopFollowOffset?.();
|
||||
this._stopFollowOffset = this.applyFollowOffset?.(entity, view.offset);
|
||||
this._stopFollowOffset = this.applyFollowOffset?.(followed, view.offset);
|
||||
}
|
||||
|
||||
cancelSelection() {
|
||||
|
||||
Reference in new issue
Block a user