fix(ui): north-up and tilt keep an engine's own follow (Navara)

Inside a Navara follow the orientation controls frame the follow's target
and retarget its viewFrom instead of moving the camera, and the release
cancels only a running flight (a cancel is a camera command, which ends
the follow). Cesium's trackedEntity path is unchanged. Updated test:
cameraOrientationControls.test.mjs (orientation navigation starts a
flight before expecting its cancel; no flight, no cancel).
This commit is contained in:
German Khokhlov committed 2026-10-17 03:48:12 +04:00
1 parent dcfb1219d5
commit 2da7b6d0d9
3 files changed
+84 -1

No files matched your search

+31 -1
View File
@@ -24,6 +24,18 @@ function followedEntity(engine, viewer) {
return handle ? (handle.native ?? null) : (viewer?.trackedEntity ?? null);
}
// An engine's own follow (no tracked entity: Navara): its handle, which
// knows its target and takes a new offset without ending the follow.
function engineFollow(engine, viewer) {
const handle = engine?.camera.following?.();
return handle &&
!followedEntity(engine, viewer) &&
typeof handle.target === 'function' &&
typeof handle.retarget === 'function'
? handle
: null;
}
function trackedTarget(entity, viewer) {
// Read the visual's sample first; sampling a flight CallbackProperty twice
// can advance dead reckoning ahead of the displayed aircraft.
@@ -89,9 +101,10 @@ export function readCameraTargetFrame(target, tracker) {
if (!engine) return null;
const viewer = trackerOf(target, tracker);
const entity = followedEntity(engine, viewer);
const follow = engineFollow(engine, viewer);
const center = entity
? trackedTarget(entity, viewer)
: pickViewTarget(engine);
: (follow?.target() ?? pickViewTarget(engine));
const pose = engine.camera.pose();
if (!center || !isPickedWorldPosition(pose.position)) return null;
@@ -133,6 +146,23 @@ export function setCameraTargetFrame(target, frame, tracker) {
if (!engine || !frame?.target || !Number.isFinite(frame.range)) return false;
const viewer = trackerOf(target, tracker);
const { camera } = engine;
// Inside an engine's own follow: the new frame is the follow's offset (a
// camera command would end the follow; Cesium's tracked entity keeps its
// follow through lookAt below, so both engines keep following).
const follow = engineFollow(engine, viewer);
if (follow) {
const heading = normalizedHeading(frame.heading);
const flat = Math.cos(frame.pitch);
follow.retarget({
viewFrom: geo.cartesian3.create(
-frame.range * Math.sin(heading) * flat,
-frame.range * Math.cos(heading) * flat,
-frame.range * Math.sin(frame.pitch),
),
});
engine.render.request('camera-orientation');
return true;
}
try {
const trackedTransform = followedEntity(engine, viewer)
? camera.transform()
+50
View File
@@ -13,6 +13,7 @@ import {
} from './cameraOrientationControls.js';
import { NavigationController } from './navigationController.js';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import * as geo from '../geo/index.js';
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
import { registerViewerEngine } from '../globe/cesium/engineFromViewer.js';
@@ -651,6 +652,10 @@ test('orientation navigation preserves follow and selection; new navigation canc
cancelFlight();
};
let cockpit = false;
engine.camera.flyTo({
destination: geo.cartesian3.fromDegrees(30, 40, 5000),
duration: 1,
});
const navigation = new NavigationController({
viewer,
engine,
@@ -675,6 +680,15 @@ test('orientation navigation preserves follow and selection; new navigation canc
'cancel-flight',
]);
calls.length = 0;
// No running flight: a cancel would be a camera command, ending an engine's
// own follow, so the orientation leaves the camera alone.
navigation.runOrientation('camera', () => 'animated');
assert.deepEqual(calls, [
'cancel-orientation',
'interrupt-motion',
'stop-orbit',
]);
calls.length = 0;
navigation._stampNavigation({ cancelPendingSelection: false });
assert.deepEqual(calls, ['cancel-orientation', 'clear-location']);
calls.length = 0;
@@ -761,3 +775,39 @@ test('the controls run on a globe engine alone', (t) => {
assert.equal(engine.frames.preUpdate.numberOfListeners, 0);
assert.equal(engine.camera.moveEnd.numberOfListeners, 0);
});
test("north-up and tilt inside an engine's own follow keep following (Navara)", () => {
const engine = createFakeEngine();
const target = Cesium.Cartesian3.fromDegrees(-73.78, 40.64, 3000);
const handle = engine.camera.follow({
position: () => target,
// 2 km south-east of the target, 1 km up: heading about 315°.
viewFrom: { x: 1400, y: -1400, z: 1000 },
});
engine.fake.frame();
const before = readCameraTargetFrame(engine);
assert.ok(before, 'a frame around the followed target');
sameXYZ(before.target, target, 'the frame centres the follow target');
assert.equal(resetCameraNorth(engine), true);
engine.fake.frame();
assert.equal(engine.camera.following(), handle, 'north-up keeps the follow');
const north = readCameraTargetFrame(engine);
const headingDeg = Cesium.Math.toDegrees(north.heading);
assert.ok(
Math.min(headingDeg, 360 - headingDeg) < 0.01,
`north-up left the follow at ${headingDeg.toFixed(2)} deg`,
);
assert.ok(Math.abs(north.range - before.range) < 1, 'same range');
assert.ok(Math.abs(north.pitch - before.pitch) < 1e-6, 'same pitch');
const tilt = toggleCameraTilt(engine);
assert.ok(tilt, 'tilt applied');
engine.fake.frame();
assert.equal(engine.camera.following(), handle, 'tilt keeps the follow');
const tilted = readCameraTargetFrame(engine);
assert.ok(
Math.abs(tilted.pitch - tilt.pitch) < 1e-6,
'the follow took the tilted pitch',
);
handle.stop();
engine.destroy();
});
+3
View File
@@ -220,6 +220,9 @@ export class NavigationController {
release: () => {
this.interruptCameraMotion('camera-orientation');
this.stopOrbit();
// Only a running flight: a cancel is a camera command, which ends an
// engine's own follow (Navara) that the orientation keeps.
if (this.engine?.camera.flightActive?.() !== false)
this.engine?.camera.cancelFlight();
},
navigate,