Keep a settled flight in reach of a later failed lookup

A new voice fly-to now records the last flight that moved as the one it replaced, landed or not; only a still-flying one passes on its manual-input count. A slow lookup cancelled by a direct flight that landed, followed by a lookup that found nothing, used to drop the queued motion, and a cancelled direct flight in the same place let it fall back to an older landing.
This commit is contained in:
Manjunath committed 2026-11-04 14:48:07 +05:30
1 parent b683b8a64b
commit 4c9b27dc20
2 files changed
+72 -9

No files matched your search

+13 -9
View File
@@ -1144,11 +1144,14 @@ export function createGevActionRunner({
runOptions,
);
}
// Chained fly-tos: a grab during the earlier one counts. The flight
// replaced is the last one that moved, past lookups that found nothing.
// Chained fly-tos: a grab during an earlier one still flying counts.
// The flight replaced is the last one that moved, past lookups that
// found nothing.
const prior = voiceFlightTarget(voiceFlight);
const replaced = prior && !prior.done ? prior : null;
const flight = trackVoiceFlight(replaced?.inputCount, replaced);
const flight = trackVoiceFlight(
prior && !prior.done ? prior.inputCount : undefined,
prior,
);
voiceFlight = flight;
const flown = flyToRequestedLocation(viewer, args, {
onFlightEnd: flight.settle,
@@ -2961,10 +2964,10 @@ let voiceFlightCount = 0;
/**
* A voice flight's end: 'arrived', 'cancelled' or 'failed', settled once.
* `replaced` is the last flight that moved, if still pending when this one
* began (past lookups that found nothing). It is kept only
* while this one may turn out to have moved nothing (a lookup that found no
* place), so a queued motion can look past it.
* `replaced` is the last flight that moved when this one began (past lookups
* that found nothing), landed or not. It is kept only while this one may turn
* out to have moved nothing (a lookup that found no place), so a queued
* motion can look past it to that flight and its outcome.
*/
function trackVoiceFlight(
inputCount = manualCameraInputCount(),
@@ -2996,7 +2999,8 @@ function trackVoiceFlight(
* actually started. A lookup that found nothing moved nothing, so look past
* it to the flight it replaced, but never to one older than `start` (a flight
* from an earlier reply). Null when every later lookup found nothing. With
* no `start`, the latest flight that started, however old.
* no `start`, the latest flight that started, back through lookups that
* found nothing.
*/
function voiceFlightTarget(latest, start = null) {
let flight = latest;
+59
View File
@@ -791,6 +791,65 @@ test('two lookups that found nothing in a row still leave the motion waiting for
assert.equal((await moved).ok, true);
});
// A flight that already settled is still what a later failed lookup replaced.
test('a motion acts at a landed flight that a later failed lookup replaced', async (t) => {
t.after(() => interruptCameraMotion('test-cleanup'));
const { viewer, styleManager, flights } = tiltAfterFlightHarness();
const searches = [];
const runner = createGevActionRunner({
viewer,
styleManager,
dataManager: { layers: new Map(), getAll: () => [] },
searchNavigation: (_viewer, query, options) => new Promise((resolve) => {
searches.push({ query, options, resolve });
}),
});
const flyA = runner('fly_to_location', { query: 'Slow A' });
const moved = runner('move_camera', { motion: 'orbit' });
assert.equal((await runner('fly_to_location', { locationId: 'austin' })).ok, true);
flights[0].complete();
const flyC = runner('fly_to_location', { query: 'Nowhere C' });
await new Promise((resolve) => setTimeout(resolve, 0));
// B's start made A's lookup stale; C then finds nothing.
searches[0].resolve({ cancelled: true });
searches[1].resolve(null);
assert.equal((await flyA).cancelled, true);
assert.equal((await flyC).ok, false);
const result = await moved;
assert.equal(result.ok, true, result.error);
});
test('a motion refuses when the flight a later failed lookup replaced was cancelled', async () => {
const { viewer, styleManager, flights } = tiltAfterFlightHarness();
const searches = [];
const runner = createGevActionRunner({
viewer,
styleManager,
dataManager: { layers: new Map(), getAll: () => [] },
searchNavigation: (_viewer, query, options) => new Promise((resolve) => {
searches.push({ query, options, resolve });
}),
});
assert.equal((await runner('fly_to_location', { locationId: 'austin' })).ok, true);
const moved = runner('move_camera', { motion: 'orbit' });
const flyA = runner('fly_to_location', { query: 'Slow A' });
await new Promise((resolve) => setTimeout(resolve, 0));
// The motion sees Austin land, then follows A's lookup.
flights[0].complete();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal((await runner('fly_to_location', { locationId: 'austin' })).ok, true);
flights[1].cancel();
const flyC = runner('fly_to_location', { query: 'Nowhere C' });
await new Promise((resolve) => setTimeout(resolve, 0));
searches[0].resolve({ cancelled: true });
searches[1].resolve(null);
assert.equal((await flyA).cancelled, true);
assert.equal((await flyC).ok, false);
const result = await moved;
assert.equal(result.ok, false);
assert.match(result.error, /did not arrive/);
});
test('a stale motion does not follow a newer reply\'s fly-to', async () => {
const { viewer, styleManager, flights } = tiltAfterFlightHarness();
const runner = createGevActionRunner({