From dcfb1219d5e9ef73ab4902ff71c610a1f5624c82 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 03:48:11 +0400 Subject: [PATCH] feat(globe): a follow handle names its target FollowHandle.target() returns the followed point, checked by the contract on both engines. The contract's model check now waits for ready() before looking for the model's pixels: the magenta point and sprites could fill the region before the glTF loaded (qa-globe-contract cesium 73/73, navara 79/79). --- src/globe/cesium/follow.js | 9 +++++++++ src/globe/contract.js | 2 ++ src/globe/follow.js | 7 +++++++ src/globe/testing/contractSuite.mjs | 9 ++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/globe/cesium/follow.js b/src/globe/cesium/follow.js index 2fc90b16e..601e134e3 100644 --- a/src/globe/cesium/follow.js +++ b/src/globe/cesium/follow.js @@ -188,6 +188,8 @@ export function createCesiumFollow(viewer, { commanded }) { if (!handle) { handle = { native: entity, + target: () => + displayPosition(entity, viewer.clock.currentTime, undefined), stop() { if (viewer.trackedEntity === entity) viewer.trackedEntity = undefined; }, @@ -306,6 +308,13 @@ export function createCesiumFollow(viewer, { commanded }) { const state = { entity: null, proxy: null, options, handle: null }; state.handle = { native: options.native, + target() { + try { + return state.options.position(); + } catch { + return undefined; + } + }, stop() { if (own !== state) return; if (viewer.trackedEntity === state.entity) diff --git a/src/globe/contract.js b/src/globe/contract.js index 5f787b813..931f478a0 100644 --- a/src/globe/contract.js +++ b/src/globe/contract.js @@ -150,6 +150,8 @@ * @typedef {object} FollowHandle * @property {() => void} stop * @property {(next: Partial) => void} retarget + * @property {() => (Cartesian3 | undefined)} target Where the followed + * target is now (what the follow centres). * @property {any} [native] * @property {object} [label] FollowOptions.label. * diff --git a/src/globe/follow.js b/src/globe/follow.js index 40bbdb108..5ba343b25 100644 --- a/src/globe/follow.js +++ b/src/globe/follow.js @@ -350,6 +350,13 @@ export function createFollowController({ label: options.label, offset: null, previous: null, + target() { + try { + return handle.options.position(); + } catch { + return undefined; + } + }, stop: () => end(handle), retarget(next) { const updated = normalizeFollowOptions({ ...options, ...next }); diff --git a/src/globe/testing/contractSuite.mjs b/src/globe/testing/contractSuite.mjs index d6b6761df..1a84d9619 100644 --- a/src/globe/testing/contractSuite.mjs +++ b/src/globe/testing/contractSuite.mjs @@ -1209,8 +1209,10 @@ export function createContractSuite( count++; return count; }; + // ready() turns true once the glTF has loaded (the magenta point + // and sprites above can already fill the region before then). + await until(() => models.ready(), 'the model set ready()'); await until(() => modelPixels() > 150, 'the model drawn'); - ensure(models.ready(), 'a drawn model set is not ready()'); } finally { for (const set of sets) set.destroy(); } @@ -2572,6 +2574,11 @@ export function createContractSuite( ); await until(() => offCentre() <= 3, 'the follow centring its target'); near(range(), Math.hypot(1000, 400), 5, 'follow range at viewFrom'); + // target(): where the followed target is now. + ensure( + cartesian3.distance(handle.target(), at(step)) < 1, + 'following().target() is not the target', + ); // 2 s of motion (at least 120 frames), centred within 3 px. const began = performance.now(); const firstStep = step;