mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +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/render/cesium/common.js",
|
||||||
"src/ui/cockpitContext.js",
|
"src/ui/cockpitContext.js",
|
||||||
"src/ui/cockpitController.js",
|
"src/ui/cockpitController.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/ui/cockpitDisplayPortal.js",
|
"src/ui/cockpitDisplayPortal.js",
|
||||||
"src/ui/displayPanelScroll.js",
|
"src/ui/displayPanelScroll.js",
|
||||||
"src/ui/cockpitInput.js",
|
"src/ui/cockpitInput.js",
|
||||||
@@ -4412,6 +4413,7 @@
|
|||||||
"src/navigationPolicy.js",
|
"src/navigationPolicy.js",
|
||||||
"src/share/follow.js",
|
"src/share/follow.js",
|
||||||
"src/ui/shareRestoration.js",
|
"src/ui/shareRestoration.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/ui/uiLifetime.js",
|
"src/ui/uiLifetime.js",
|
||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
@@ -4426,7 +4428,8 @@
|
|||||||
"src/i18n/locales/uk.json",
|
"src/i18n/locales/uk.json",
|
||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.json",
|
"src/i18n/locales/zh-Hant.json",
|
||||||
"src/data/entityRecords.js"
|
"src/data/entityRecords.js",
|
||||||
|
"src/render/rendererOf.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { followedOf } from '../data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { getOverlayPaintRect } from '../overlays/worldOverlay.js';
|
import { getOverlayPaintRect } from '../overlays/worldOverlay.js';
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +9,6 @@ import { t } from '../i18n/index.js';
|
|||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
import { useScreenAnnotationsStore } from '../ui/stores/screenAnnotations.js';
|
||||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||||
import { readEntityRecord } from '../data/entityRecords.js';
|
|
||||||
import { heightsFor } from '../queries/heights.js';
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
|
||||||
/** A label whose detailed outline could not be resolved. */
|
/** A label whose detailed outline could not be resolved. */
|
||||||
@@ -421,8 +421,8 @@ export function createScreenAnnotationRenderer(
|
|||||||
let top = painted?.y;
|
let top = painted?.y;
|
||||||
let bottom = painted ? painted.y + painted.h : undefined;
|
let bottom = painted ? painted.y + painted.h : undefined;
|
||||||
|
|
||||||
const ent = viewer.trackedEntity;
|
const followed = followedOf(viewer);
|
||||||
const displayPosition = readEntityRecord(ent)?.displayPosition;
|
const displayPosition = followed?.displayPosition;
|
||||||
const world =
|
const world =
|
||||||
typeof displayPosition === 'function' ? displayPosition() : null;
|
typeof displayPosition === 'function' ? displayPosition() : null;
|
||||||
const win = world
|
const win = world
|
||||||
@@ -435,7 +435,7 @@ export function createScreenAnnotationRenderer(
|
|||||||
|
|
||||||
// Billboard box — centered on the anchor, magnified by scaleByDistance, from
|
// Billboard box — centered on the anchor, magnified by scaleByDistance, from
|
||||||
// the size the tracking layer recorded for its sprite.
|
// 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)) {
|
if (sprite && win && Number.isFinite(win.x) && Number.isFinite(win.y)) {
|
||||||
const baseW = sprite.width ?? 28;
|
const baseW = sprite.width ?? 28;
|
||||||
const baseH = sprite.height ?? 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;',
|
'if (!info || !entity?.position) return false;',
|
||||||
'this.onCameraTakeover?.();',
|
'this.onCameraTakeover?.();',
|
||||||
'cameraOf(this.viewer).cancelFlight();',
|
'cameraOf(this.viewer).cancelFlight();',
|
||||||
'this.viewer.trackedEntity = undefined;',
|
'stopFollowing(this.viewer);',
|
||||||
], 'Cockpit takeover');
|
], 'Cockpit takeover');
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
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, /if \(this\.isUtilityDisclosureOpen\?\.\(\)\) return;/);
|
||||||
assert.match(
|
assert.match(
|
||||||
keydown,
|
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();
|
const read = readAircraftInfo.toString();
|
||||||
assert.ok(read, 'readAircraftInfo is missing');
|
assert.ok(read, 'readAircraftInfo is missing');
|
||||||
assert.match(read, /resolveTrackedAircraftInfo\(\{/);
|
assert.match(read, /resolveTrackedAircraftInfo\(\{/);
|
||||||
assert.match(read, /readEntityRecord\([^)]*\)\?\.trackedId/);
|
assert.match(read, /trackedId: followed\?\.key/);
|
||||||
// In cockpit mode the controller moves the entity off viewer.trackedEntity,
|
// In cockpit mode the controller stops the camera following, so its own
|
||||||
// so its own handle is the tracked identity there.
|
// handle is the tracked identity there.
|
||||||
assert.match(read, /this\.viewer\?\.trackedEntity \|\| this\.trackedEntity/);
|
assert.match(read, /followedOf\(this\.viewer\) \|\| this\.trackedFollow/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('programmatic Context layer changes cannot bypass explicit expansion policy', () => {
|
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', () => {
|
test('cockpit aircraft handoff invalidates the prior world-position anchor', () => {
|
||||||
const match = _adoptTrackedEntity.toString();
|
const match = _adoptTrackedEntity.toString();
|
||||||
assert.ok(match, 'cockpit tracked-entity handoff block is missing');
|
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, /this\.cockpitAnchorValid = false;/);
|
||||||
assert.match(
|
assert.match(
|
||||||
match,
|
match,
|
||||||
|
|||||||
+60
-1
@@ -7,7 +7,14 @@
|
|||||||
* Until every layer does, a layer may still track an entity of its own
|
* 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
|
* through Cesium's `trackedEntity`; that entity is seen here through the
|
||||||
* same fields, read from its entity record. Readers ask this module, never
|
* 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 { findCamera } from '../render/rendererOf.js';
|
||||||
import { readEntityRecord } from './entityRecords.js';
|
import { readEntityRecord } from './entityRecords.js';
|
||||||
@@ -125,3 +132,55 @@ export function stopFollowing(viewer) {
|
|||||||
if (camera?.followed) camera.follow(null);
|
if (camera?.followed) camera.follow(null);
|
||||||
else if (viewer.trackedEntity) viewer.trackedEntity = undefined;
|
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 * as Cesium from 'cesium';
|
||||||
import { entityRecord } from './entityRecords.js';
|
import { entityRecord } from './entityRecords.js';
|
||||||
import {
|
import {
|
||||||
|
entityFollowTarget,
|
||||||
followedOf,
|
followedOf,
|
||||||
|
followedPosition,
|
||||||
isFollowing,
|
isFollowing,
|
||||||
onFollowChanged,
|
onFollowChanged,
|
||||||
|
resumeFollowing,
|
||||||
stopFollowing,
|
stopFollowing,
|
||||||
} from './followed.js';
|
} from './followed.js';
|
||||||
import { bindRenderer } from '../render/rendererOf.js';
|
import { bindRenderer } from '../render/rendererOf.js';
|
||||||
@@ -121,3 +124,52 @@ test('a renderer bound after subscribing is still heard', (t) => {
|
|||||||
camera.follow(null);
|
camera.follow(null);
|
||||||
assert.deepEqual(seen, ['k', 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 {
|
import {
|
||||||
clearOverlaySource,
|
clearOverlaySource,
|
||||||
setOverlayEntries,
|
setOverlayEntries,
|
||||||
@@ -126,7 +127,7 @@ function entryIdFor(entity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activeEntity() {
|
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();
|
if (_viewer) destroyTrackedReadout();
|
||||||
_viewer = viewer;
|
_viewer = viewer;
|
||||||
_overlayHost.setVisible(TRACKED_OVERLAY_SOURCE_ID, true);
|
_overlayHost.setVisible(TRACKED_OVERLAY_SOURCE_ID, true);
|
||||||
_trackedEntityChangedRemove =
|
_trackedEntityChangedRemove = onFollowChanged(viewer, (followed) => {
|
||||||
viewer.trackedEntityChanged?.addEventListener?.(() => {
|
if (followed) _selectedContext = null;
|
||||||
if (viewer.trackedEntity) _selectedContext = null;
|
syncActiveEntity();
|
||||||
syncActiveEntity();
|
});
|
||||||
}) || null;
|
|
||||||
_contextSelectedHandler = (event) => {
|
_contextSelectedHandler = (event) => {
|
||||||
const record = event.detail;
|
const record = event.detail;
|
||||||
if (READOUT_CONTEXT_LAYERS.has(record?.layerId)) {
|
if (READOUT_CONTEXT_LAYERS.has(record?.layerId)) {
|
||||||
@@ -263,7 +263,7 @@ export function initTrackedReadout(viewer) {
|
|||||||
};
|
};
|
||||||
_aircraftSelectedHandler = () => {
|
_aircraftSelectedHandler = () => {
|
||||||
_selectedContext = null;
|
_selectedContext = null;
|
||||||
if (!_viewer?.trackedEntity) clearTrackedSource();
|
if (!isFollowing(_viewer)) clearTrackedSource();
|
||||||
};
|
};
|
||||||
window.addEventListener('gev:entity-selected', _contextSelectedHandler);
|
window.addEventListener('gev:entity-selected', _contextSelectedHandler);
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ export class StyleManager extends ShellFacade {
|
|||||||
this._showGlobalStatusNotice(message, options),
|
this._showGlobalStatusNotice(message, options),
|
||||||
feedback: this._feedback,
|
feedback: this._feedback,
|
||||||
updateFeedback: () => this._updateGlobalLoadingFeedback(),
|
updateFeedback: () => this._updateGlobalLoadingFeedback(),
|
||||||
applyFollowOffset: (entity, offset) =>
|
applyFollowOffset: (followed, offset) =>
|
||||||
applyShareFollowOffset(viewer, entity, offset),
|
applyShareFollowOffset(viewer, followed, offset),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._visualSettings = new VisualSettings({
|
this._visualSettings = new VisualSettings({
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||||
import { t } from '../i18n/index.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) {
|
function trackedTarget(viewer) {
|
||||||
const entity = viewer.trackedEntity;
|
// The visual's sample first: sampling a flight CallbackProperty twice can
|
||||||
// Read the visual's sample first; sampling a flight CallbackProperty twice
|
// advance dead reckoning ahead of the displayed aircraft.
|
||||||
// can advance dead reckoning ahead of the displayed aircraft.
|
return followedPosition(viewer, followedOf(viewer));
|
||||||
return (
|
|
||||||
readEntityRecord(entity)?.displayPosition?.() ||
|
|
||||||
entity?.position?.getValue(viewer.clock?.currentTime)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OBLIQUE_PITCH = Cesium.Math.toRadians(-35);
|
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. */
|
/** Describe the camera as an orbit around the current viewport center. */
|
||||||
export function readCameraTargetFrame(viewer) {
|
export function readCameraTargetFrame(viewer) {
|
||||||
const camera = viewer?.camera;
|
const camera = viewer?.camera;
|
||||||
const target = viewer?.trackedEntity
|
const target = isFollowing(viewer)
|
||||||
? trackedTarget(viewer)
|
? trackedTarget(viewer)
|
||||||
: pickViewTarget(viewer);
|
: pickViewTarget(viewer);
|
||||||
if (!camera || !target || !isPickedWorldPosition(camera.positionWC))
|
if (!camera || !target || !isPickedWorldPosition(camera.positionWC))
|
||||||
@@ -112,7 +113,7 @@ function setCameraTargetFrame(viewer, frame) {
|
|||||||
const camera = viewer?.camera;
|
const camera = viewer?.camera;
|
||||||
if (!camera || !frame?.target || !Number.isFinite(frame.range)) return false;
|
if (!camera || !frame?.target || !Number.isFinite(frame.range)) return false;
|
||||||
try {
|
try {
|
||||||
const trackedTransform = viewer.trackedEntity
|
const trackedTransform = isFollowing(viewer)
|
||||||
? Cesium.Matrix4.clone(camera.transform)
|
? Cesium.Matrix4.clone(camera.transform)
|
||||||
: null;
|
: null;
|
||||||
camera.lookAt(
|
camera.lookAt(
|
||||||
@@ -200,20 +201,20 @@ export function createCameraOrientationAnimator(
|
|||||||
cancel();
|
cancel();
|
||||||
if (!viewer.scene?.preUpdate?.addEventListener || duration <= 0)
|
if (!viewer.scene?.preUpdate?.addEventListener || duration <= 0)
|
||||||
return setCameraTargetFrame(viewer, { ...frame, ...destination });
|
return setCameraTargetFrame(viewer, { ...frame, ...destination });
|
||||||
const entity = viewer.trackedEntity;
|
const followed = followedOf(viewer);
|
||||||
pending = destination;
|
pending = destination;
|
||||||
const start = now();
|
const start = now();
|
||||||
const headingDelta = Cesium.Math.negativePiToPi(
|
const headingDelta = Cesium.Math.negativePiToPi(
|
||||||
destination.heading - frame.heading,
|
destination.heading - frame.heading,
|
||||||
);
|
);
|
||||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||||
if (viewer.isDestroyed?.() || viewer.trackedEntity !== entity) {
|
if (viewer.isDestroyed?.() || followedOf(viewer) !== followed) {
|
||||||
cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const progress = Cesium.Math.clamp((now() - start) / duration, 0, 1);
|
const progress = Cesium.Math.clamp((now() - start) / duration, 0, 1);
|
||||||
const eased = Cesium.EasingFunction.CUBIC_IN_OUT(progress);
|
const eased = Cesium.EasingFunction.CUBIC_IN_OUT(progress);
|
||||||
const target = entity ? trackedTarget(viewer) : frame.target;
|
const target = followed ? trackedTarget(viewer) : frame.target;
|
||||||
if (
|
if (
|
||||||
!isPickedWorldPosition(target) ||
|
!isPickedWorldPosition(target) ||
|
||||||
!setCameraTargetFrame(viewer, {
|
!setCameraTargetFrame(viewer, {
|
||||||
@@ -338,7 +339,7 @@ export function bindCameraOrientationControls({
|
|||||||
// The exact tilt state is refreshed once per gesture, not once per frame —
|
// 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.
|
// moveEnd fires when the camera stops, which is where the pick is affordable.
|
||||||
subscribe(viewer?.camera?.moveEnd, refreshTilt);
|
subscribe(viewer?.camera?.moveEnd, refreshTilt);
|
||||||
subscribe(viewer?.trackedEntityChanged, animator.cancel);
|
if (viewer) removers.push(onFollowChanged(viewer, animator.cancel));
|
||||||
for (const type of ['pointerdown', 'wheel']) {
|
for (const type of ['pointerdown', 'wheel']) {
|
||||||
const canvas = viewer?.scene?.canvas;
|
const canvas = viewer?.scene?.canvas;
|
||||||
canvas?.addEventListener?.(type, animator.cancel, { passive: true });
|
canvas?.addEventListener?.(type, animator.cancel, { passive: true });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** Coordinate Cockpit lifecycle through supplied aircraft, terrain, rendering and briefing operations. */
|
/** Coordinate Cockpit lifecycle through supplied aircraft, terrain, rendering and briefing operations. */
|
||||||
|
import { onFollowChanged } from '../data/followed.js';
|
||||||
import {
|
import {
|
||||||
readAircraftInfo,
|
readAircraftInfo,
|
||||||
dispatchCockpitModeChanged,
|
dispatchCockpitModeChanged,
|
||||||
@@ -87,6 +88,8 @@ export class CockpitViewController {
|
|||||||
/** Gives camera input back; held from enter to exit. */
|
/** Gives camera input back; held from enter to exit. */
|
||||||
this.releaseCameraInput = null;
|
this.releaseCameraInput = null;
|
||||||
this.trackedEntity = null;
|
this.trackedEntity = null;
|
||||||
|
/** What the camera followed before Cockpit took it, to follow again. */
|
||||||
|
this.trackedFollow = null;
|
||||||
this.trackedEntityWasShown = true;
|
this.trackedEntityWasShown = true;
|
||||||
this.heading = null;
|
this.heading = null;
|
||||||
this.lastFrameMs = 0;
|
this.lastFrameMs = 0;
|
||||||
@@ -201,7 +204,7 @@ export class CockpitViewController {
|
|||||||
// create a self-sustaining refinement loop under a moving cockpit camera.
|
// create a self-sustaining refinement loop under a moving cockpit camera.
|
||||||
this._listenerRemovers.push(
|
this._listenerRemovers.push(
|
||||||
viewer.scene.preUpdate.addEventListener(() => this.update()),
|
viewer.scene.preUpdate.addEventListener(() => this.update()),
|
||||||
viewer.trackedEntityChanged.addEventListener(() => {
|
onFollowChanged(viewer, () => {
|
||||||
if (this.active) this._adoptTrackedEntity(performance.now());
|
if (this.active) this._adoptTrackedEntity(performance.now());
|
||||||
else this.syncEntry();
|
else this.syncEntry();
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { followedOf } from '../data/followed.js';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { CockpitViewController, CockpitDisplayPortal } from './cockpit.js';
|
import { CockpitViewController, CockpitDisplayPortal } from './cockpit.js';
|
||||||
import { cockpitEntryAllowed } from '../contextModePolicy.js';
|
import { cockpitEntryAllowed } from '../contextModePolicy.js';
|
||||||
@@ -124,12 +125,12 @@ export class CockpitCoordinator {
|
|||||||
const gateOpen = Boolean(this.cockpitView?.isEntryAllowed?.());
|
const gateOpen = Boolean(this.cockpitView?.isEntryAllowed?.());
|
||||||
// "Could Cockpit be ENTERED right now" — so it is false while already
|
// "Could Cockpit be ENTERED right now" — so it is false while already
|
||||||
// inside, unconditionally. Cockpit takes the entity off
|
// 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
|
// flip true/false between calls while `active` stayed true; readers
|
||||||
// (including the voice model) read that as a broken half-entered state.
|
// (including the voice model) read that as a broken half-entered state.
|
||||||
const entryAllowed =
|
const entryAllowed =
|
||||||
!active &&
|
!active &&
|
||||||
Boolean(gateOpen && info && this.viewer?.trackedEntity?.position);
|
Boolean(gateOpen && info && followedOf(this.viewer)?.entity?.position);
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
entryAllowed,
|
entryAllowed,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** Route Cockpit keys while preserving the active disclosure and form-control owner. */
|
/** Route Cockpit keys while preserving the active disclosure and form-control owner. */
|
||||||
|
import { followedOf } from '../data/followed.js';
|
||||||
|
|
||||||
export function onKeyDown(event) {
|
export function onKeyDown(event) {
|
||||||
if (this.destroyed) return false;
|
if (this.destroyed) return false;
|
||||||
@@ -49,7 +50,7 @@ export function onKeyDown(event) {
|
|||||||
if (key === 'c' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
if (key === 'c' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
const cockpitAttempt = !!(
|
const cockpitAttempt = !!(
|
||||||
this.readAircraftInfo() && this.viewer.trackedEntity?.position
|
this.readAircraftInfo() && followedOf(this.viewer)?.entity?.position
|
||||||
);
|
);
|
||||||
if (!cockpitAttempt) return;
|
if (!cockpitAttempt) return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,21 @@ import {
|
|||||||
normalizeHeading,
|
normalizeHeading,
|
||||||
resolveTrackedAircraftInfo,
|
resolveTrackedAircraftInfo,
|
||||||
} from '../cockpitMath.js';
|
} from '../cockpitMath.js';
|
||||||
import { readEntityRecord } from '../data/entityRecords.js';
|
import {
|
||||||
|
followedOf,
|
||||||
|
resumeFollowing,
|
||||||
|
stopFollowing,
|
||||||
|
} from '../data/followed.js';
|
||||||
import { inputFor } from '../input/dispatcher.js';
|
import { inputFor } from '../input/dispatcher.js';
|
||||||
|
|
||||||
export function readAircraftInfo() {
|
export function readAircraftInfo() {
|
||||||
// In cockpit mode the controller takes the entity off `viewer.trackedEntity`
|
// In cockpit mode the controller stops the camera following (see enter()),
|
||||||
// (see update()), so the cockpit's own handle is the tracked identity there.
|
// so the cockpit's own handle is the tracked identity there.
|
||||||
const trackedEntity = this.viewer?.trackedEntity || this.trackedEntity;
|
const followed = followedOf(this.viewer) || this.trackedFollow;
|
||||||
return resolveTrackedAircraftInfo({
|
return resolveTrackedAircraftInfo({
|
||||||
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
|
civilian: this.services.flightsLayer.getTrackedInfo?.() || null,
|
||||||
military: this.services.militaryFlightsLayer.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.destroyed) return false;
|
||||||
if (this.active) return;
|
if (this.active) return;
|
||||||
const info = this.readAircraftInfo();
|
const info = this.readAircraftInfo();
|
||||||
const trackedContact = !!(info && this.viewer.trackedEntity?.position);
|
const trackedContact = !!(info && followedOf(this.viewer)?.entity?.position);
|
||||||
this.syncTr3bToggle(trackedContact ? info : null);
|
this.syncTr3bToggle(trackedContact ? info : null);
|
||||||
const available = !!(this.isEntryAllowed() && trackedContact);
|
const available = !!(this.isEntryAllowed() && trackedContact);
|
||||||
// Change-only publishes: this runs on a preUpdate cadence, and a fresh
|
// 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) {
|
export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
||||||
const nextEntity = this.viewer.trackedEntity;
|
const nextFollowed = followedOf(this.viewer);
|
||||||
|
const nextEntity = nextFollowed?.entity;
|
||||||
if (
|
if (
|
||||||
!this.active ||
|
!this.active ||
|
||||||
!nextEntity?.position ||
|
!nextEntity?.position ||
|
||||||
@@ -124,9 +129,10 @@ export function _adoptTrackedEntity(nowMs, suppliedInfo = null) {
|
|||||||
this.trackedEntity.show = this.trackedEntityWasShown;
|
this.trackedEntity.show = this.trackedEntityWasShown;
|
||||||
}
|
}
|
||||||
this.trackedEntity = nextEntity;
|
this.trackedEntity = nextEntity;
|
||||||
|
this.trackedFollow = nextFollowed;
|
||||||
this.trackedEntityWasShown = nextEntity.show;
|
this.trackedEntityWasShown = nextEntity.show;
|
||||||
nextEntity.show = false;
|
nextEntity.show = false;
|
||||||
this.viewer.trackedEntity = undefined;
|
stopFollowing(this.viewer);
|
||||||
this.cockpitAnchorValid = false;
|
this.cockpitAnchorValid = false;
|
||||||
this.heading = normalizeHeading(info.track ?? 0);
|
this.heading = normalizeHeading(info.track ?? 0);
|
||||||
this.lastFrameMs = nowMs;
|
this.lastFrameMs = nowMs;
|
||||||
@@ -143,7 +149,8 @@ export function enter() {
|
|||||||
if (this.active) return false;
|
if (this.active) return false;
|
||||||
if (!this.isEntryAllowed()) return false;
|
if (!this.isEntryAllowed()) return false;
|
||||||
const info = this.readAircraftInfo();
|
const info = this.readAircraftInfo();
|
||||||
const entity = this.viewer.trackedEntity;
|
const followed = followedOf(this.viewer);
|
||||||
|
const entity = followed?.entity;
|
||||||
if (!info || !entity?.position) return false;
|
if (!info || !entity?.position) return false;
|
||||||
this.entryFocusOrigin =
|
this.entryFocusOrigin =
|
||||||
document.activeElement instanceof HTMLElement
|
document.activeElement instanceof HTMLElement
|
||||||
@@ -153,6 +160,7 @@ export function enter() {
|
|||||||
this.onCameraTakeover?.();
|
this.onCameraTakeover?.();
|
||||||
cameraOf(this.viewer).cancelFlight();
|
cameraOf(this.viewer).cancelFlight();
|
||||||
this.trackedEntity = entity;
|
this.trackedEntity = entity;
|
||||||
|
this.trackedFollow = followed;
|
||||||
this.trackedEntityWasShown = entity.show;
|
this.trackedEntityWasShown = entity.show;
|
||||||
entity.show = false;
|
entity.show = false;
|
||||||
this.heading = normalizeHeading(info.track ?? 0);
|
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.
|
// runs on rendered frames, so idle mode would freeze the cockpit solid.
|
||||||
// (perf wave 2)
|
// (perf wave 2)
|
||||||
this.services.holdContinuousRender('cockpit');
|
this.services.holdContinuousRender('cockpit');
|
||||||
this.viewer.trackedEntity = undefined;
|
stopFollowing(this.viewer);
|
||||||
this.releaseCameraInput?.();
|
this.releaseCameraInput?.();
|
||||||
this.releaseCameraInput = inputFor(this.viewer).suspendCamera('cockpit');
|
this.releaseCameraInput = inputFor(this.viewer).suspendCamera('cockpit');
|
||||||
document.body.classList.add('cockpit-mode');
|
document.body.classList.add('cockpit-mode');
|
||||||
@@ -217,9 +225,11 @@ export function enter() {
|
|||||||
export function exit({ restoreTracking = true } = {}) {
|
export function exit({ restoreTracking = true } = {}) {
|
||||||
if (!this.active) return false;
|
if (!this.active) return false;
|
||||||
const entity = this.trackedEntity;
|
const entity = this.trackedEntity;
|
||||||
|
const followed = this.trackedFollow;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.services.releaseContinuousRender('cockpit');
|
this.services.releaseContinuousRender('cockpit');
|
||||||
this.trackedEntity = null;
|
this.trackedEntity = null;
|
||||||
|
this.trackedFollow = null;
|
||||||
this.heading = null;
|
this.heading = null;
|
||||||
this.cockpitAnchorValid = false;
|
this.cockpitAnchorValid = false;
|
||||||
this.surfaceWaitStartedMs = 0;
|
this.surfaceWaitStartedMs = 0;
|
||||||
@@ -248,7 +258,7 @@ export function exit({ restoreTracking = true } = {}) {
|
|||||||
this.trackedEntityWasShown = true;
|
this.trackedEntityWasShown = true;
|
||||||
this.dispatchCockpitModeChanged(false);
|
this.dispatchCockpitModeChanged(false);
|
||||||
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
|
if (restoreTracking && entity && this.viewer.entities.contains(entity)) {
|
||||||
this.viewer.trackedEntity = entity;
|
resumeFollowing(this.viewer, followed);
|
||||||
this.restoreTrackingFrame(entity);
|
this.restoreTrackingFrame(entity);
|
||||||
}
|
}
|
||||||
this.syncEntry();
|
this.syncEntry();
|
||||||
|
|||||||
+27
-26
@@ -1,11 +1,11 @@
|
|||||||
import * as Cesium from 'cesium';
|
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 {
|
import {
|
||||||
SHARE_VIEW_ENTITY_KINDS,
|
SHARE_VIEW_ENTITY_KINDS,
|
||||||
followOffsetFromEnu,
|
followOffsetFromEnu,
|
||||||
followOffsetToEnu,
|
followOffsetToEnu,
|
||||||
} from '../share/follow.js';
|
} from '../share/follow.js';
|
||||||
import { readEntityRecord } from '../data/entityRecords.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bridge between the camera following an entity and a share link's thing
|
* 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;
|
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. */
|
/** `position`'s local east/north/up offset from `origin`, in metres. */
|
||||||
function enuOffset(origin, position) {
|
function enuOffset(origin, position) {
|
||||||
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
||||||
@@ -39,19 +45,13 @@ function enuOffset(origin, position) {
|
|||||||
export function readShareFollowSubject({ viewer, nowMs = Date.now() }) {
|
export function readShareFollowSubject({ viewer, nowMs = Date.now() }) {
|
||||||
const cameraPosition = viewer?.camera?.positionWC;
|
const cameraPosition = viewer?.camera?.positionWC;
|
||||||
if (!cameraPosition) return null;
|
if (!cameraPosition) return null;
|
||||||
const tracked = viewer.trackedEntity;
|
const followed = followedOf(viewer);
|
||||||
const trackedId = String(readEntityRecord(tracked)?.trackedId || '');
|
const trackedId = String(followed?.key || '');
|
||||||
const separator = trackedId.indexOf(':');
|
const separator = trackedId.indexOf(':');
|
||||||
if (separator <= 0) return null;
|
if (separator <= 0) return null;
|
||||||
const kind = KIND_BY_LAYER.get(trackedId.slice(0, separator));
|
const kind = KIND_BY_LAYER.get(trackedId.slice(0, separator));
|
||||||
const id = trackedId.slice(separator + 1);
|
const id = trackedId.slice(separator + 1);
|
||||||
const target = kind
|
const target = kind ? shownAt(viewer, followed) : null;
|
||||||
? trackedDisplayPositionForCamera(
|
|
||||||
tracked,
|
|
||||||
viewer.clock.currentTime,
|
|
||||||
new Cesium.Cartesian3(),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
if (!kind || !id || !target) return null;
|
if (!kind || !id || !target) return null;
|
||||||
const offset = followOffsetFromEnu(enuOffset(target, cameraPosition));
|
const offset = followOffsetFromEnu(enuOffset(target, cameraPosition));
|
||||||
const fix = Cesium.Cartographic.fromCartesian(target);
|
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;
|
const MAX_TURN_PER_FRAME = Math.PI / 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put the camera at a shared follow offset from `entity` once following it
|
* Put the camera at a shared follow offset from `followed` (what
|
||||||
* has framed the camera. Cesium keeps the offset afterwards, as it keeps one
|
* `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 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
|
* 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.
|
* through the entity and puts it back. Returns a disposer.
|
||||||
*/
|
*/
|
||||||
export function applyShareFollowOffset(viewer, entity, offset) {
|
export function applyShareFollowOffset(viewer, followed, offset) {
|
||||||
if (!viewer?.scene?.preUpdate || !entity || !(offset?.range > 0))
|
if (!viewer?.scene?.preUpdate || !followed || !(offset?.range > 0))
|
||||||
return () => {};
|
return () => {};
|
||||||
const local = followOffsetToEnu(offset);
|
const local = followOffsetToEnu(offset);
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
@@ -90,23 +90,20 @@ export function applyShareFollowOffset(viewer, entity, offset) {
|
|||||||
remove = null;
|
remove = null;
|
||||||
};
|
};
|
||||||
remove = viewer.scene.preUpdate.addEventListener(() => {
|
remove = viewer.scene.preUpdate.addEventListener(() => {
|
||||||
if (viewer.isDestroyed?.() || viewer.trackedEntity !== entity) {
|
if (viewer.isDestroyed?.() || followedOf(viewer) !== followed) {
|
||||||
stop();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frames += 1;
|
frames += 1;
|
||||||
const camera = viewer.camera;
|
const camera = viewer.camera;
|
||||||
// Let the follow frame the camera first (its first frame), then move it.
|
// Let the follow frame the camera first (its first frame), then move it.
|
||||||
|
const port = findCamera(viewer);
|
||||||
const framed =
|
const framed =
|
||||||
frames > 1 &&
|
frames > 1 &&
|
||||||
!Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY);
|
(port?.followed === followed
|
||||||
const target = framed
|
? port.followFramed
|
||||||
? trackedDisplayPositionForCamera(
|
: !Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY));
|
||||||
entity,
|
const target = framed ? shownAt(viewer, followed) : null;
|
||||||
viewer.clock.currentTime,
|
|
||||||
new Cesium.Cartesian3(),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
if (frames >= MAX_FRAMES) stop();
|
if (frames >= MAX_FRAMES) stop();
|
||||||
return;
|
return;
|
||||||
@@ -136,8 +133,12 @@ export function applyShareFollowOffset(viewer, entity, offset) {
|
|||||||
: 0;
|
: 0;
|
||||||
const done = !(angle > MAX_TURN_PER_FRAME);
|
const done = !(angle > MAX_TURN_PER_FRAME);
|
||||||
const next = done ? goal : turnToward(from, goal, MAX_TURN_PER_FRAME);
|
const next = done ? goal : turnToward(from, goal, MAX_TURN_PER_FRAME);
|
||||||
camera.lookAtTransform(camera.transform, next);
|
// Through the port when it follows: its close-range guard keeps the eye.
|
||||||
viewer.scene.requestRender?.();
|
if (port?.followed === followed) port.followOffset.set(next);
|
||||||
|
else {
|
||||||
|
camera.lookAtTransform(camera.transform, next);
|
||||||
|
viewer.scene.requestRender?.();
|
||||||
|
}
|
||||||
if (done) stop();
|
if (done) stop();
|
||||||
});
|
});
|
||||||
return stop;
|
return stop;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '../share/viewCodec.js';
|
} from '../share/viewCodec.js';
|
||||||
import { entityRecord, withEntityRecord } from '../data/entityRecords.js';
|
import { entityRecord, withEntityRecord } from '../data/entityRecords.js';
|
||||||
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
|
import { followedOf } from '../data/followed.js';
|
||||||
|
|
||||||
const NOW_MS = Date.now();
|
const NOW_MS = Date.now();
|
||||||
const FIX = { lat: 37.6213, lon: -122.379, alt: 3200 };
|
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.
|
// Looking south at the nose: the opposite side of the app's framing.
|
||||||
const offset = { range: 5800, heading: 180, pitch: -35 };
|
const offset = { range: 5800, heading: 180, pitch: -35 };
|
||||||
applyShareFollowOffset(viewer, entity, offset);
|
applyShareFollowOffset(viewer, followedOf(viewer), offset);
|
||||||
let previous = viewer.camera.position;
|
let previous = viewer.camera.position;
|
||||||
for (let frame = 0; frame < 10 && listeners.length; frame += 1) {
|
for (let frame = 0; frame < 10 && listeners.length; frame += 1) {
|
||||||
listeners[0]();
|
listeners[0]();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
|||||||
import { t, msg, formatDate } from '../i18n/index.js';
|
import { t, msg, formatDate } from '../i18n/index.js';
|
||||||
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
||||||
import { UiLifetime } from './uiLifetime.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. */
|
/** How long a shared vessel may take to appear in the vessel feed. */
|
||||||
export const SHARED_VESSEL_WAIT_MS = 120_000;
|
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. */
|
/** Frame a shared entity the camera now follows from the shared offset. */
|
||||||
_applySharedFollowOffset(result) {
|
_applySharedFollowOffset(result) {
|
||||||
const view = this._sharedThingFor(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);
|
// Only a link made while following carries a real offset (with the fix);
|
||||||
// a long link's follow keeps the app's own framing.
|
// a long link's follow keeps the app's own framing.
|
||||||
if (
|
if (
|
||||||
!view?.lastKnown ||
|
!view?.lastKnown ||
|
||||||
String(readEntityRecord(entity)?.trackedId || '') !==
|
String(followed?.key || '') !== `${result.layerId}:${result.targetId}`
|
||||||
`${result.layerId}:${result.targetId}`
|
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
this._stopFollowOffset?.();
|
this._stopFollowOffset?.();
|
||||||
this._stopFollowOffset = this.applyFollowOffset?.(entity, view.offset);
|
this._stopFollowOffset = this.applyFollowOffset?.(followed, view.offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelSelection() {
|
cancelSelection() {
|
||||||
|
|||||||
Reference in new issue
Block a user