mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Hold a voice orbit, pan or rotate until its fly-to lands
"Fly to Paris and orbit it" reaches the app as two sibling tool calls, which OpenAI runs at once. With a place that needs a lookup, the orbit started over the old view and the late flight then replaced it; with a known place, pan and rotate cancelled the flight already in the air. Orbit, pan and rotate now wait for a pending voice flight the same way tilt does: up to 30 s, and not run (saying so) if the flight is cancelled, finds nothing or times out. Stop and rotate_globe do not wait. Orbit keeps its own arming for flights the voice runner does not track.
This commit is contained in:
1 parent
91a3f787d4
commit
75000cf6a9
4 files changed
+117
-10
No files matched your search
+5
-3
@@ -1747,9 +1747,11 @@ export function moveCamera(args = {}, runNavigation = null) {
|
||||
}
|
||||
if (motion === 'orbit') {
|
||||
if (_viewer.scene.tweens.length > 0) {
|
||||
// A camera flight is in progress ("fly to X and orbit it") — ALWAYS arm
|
||||
// and capture at the destination; a target existing right now is
|
||||
// meaningless (the globe is always under the crosshair).
|
||||
// A camera flight is in progress — ALWAYS arm and capture at the
|
||||
// destination; a target existing right now is meaningless (the globe
|
||||
// is always under the crosshair). The voice runner already holds an
|
||||
// orbit until its own fly-to lands; this covers any other
|
||||
// flight.
|
||||
setActiveMotion({
|
||||
kind: 'orbit',
|
||||
direction: direction || 'right',
|
||||
|
||||
+18
-6
@@ -1188,8 +1188,9 @@ export function createGevActionRunner({
|
||||
}
|
||||
|
||||
if (name === 'move_camera') {
|
||||
const motion = String(args.motion || '').toLowerCase();
|
||||
if (
|
||||
String(args.motion || '').toLowerCase() === 'tilt' &&
|
||||
VOICE_FLIGHT_WAITING_MOTIONS.has(motion) &&
|
||||
voiceFlight &&
|
||||
!voiceFlight.done
|
||||
) {
|
||||
@@ -1200,9 +1201,8 @@ export function createGevActionRunner({
|
||||
return {
|
||||
ok: false,
|
||||
action: 'move_camera',
|
||||
motion: 'tilt',
|
||||
error:
|
||||
'Not run: the flight this tilt was waiting for did not arrive.',
|
||||
motion,
|
||||
error: `Not run: the flight this ${motion} was waiting for did not arrive.`,
|
||||
};
|
||||
}
|
||||
return moveCamera(args, (navigate, releaseOptions) =>
|
||||
@@ -2721,7 +2721,6 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Run one validated voice camera mutation through the UI-owned authority seam. */
|
||||
/** A voice flight's end: 'arrived', 'cancelled' or 'failed', settled once. */
|
||||
function trackVoiceFlight() {
|
||||
let resolve;
|
||||
@@ -2739,9 +2738,21 @@ function trackVoiceFlight() {
|
||||
return flight;
|
||||
}
|
||||
|
||||
/** Longest a tilt waits for a flight (geocode plus flight) before giving up. */
|
||||
/** Longest a motion waits for a flight (geocode plus flight) before giving up. */
|
||||
const VOICE_FLIGHT_WAIT_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Motions that act on the place the camera ends up at, so one sent beside a
|
||||
* fly-to waits for it. Stop is not one, and rotate_globe turns the
|
||||
* whole Earth from Globe View rather than at a place.
|
||||
*/
|
||||
const VOICE_FLIGHT_WAITING_MOTIONS = new Set([
|
||||
'tilt',
|
||||
'orbit',
|
||||
'pan',
|
||||
'rotate',
|
||||
]);
|
||||
|
||||
/** Wait for a voice flight to end; 'timeout' or 'aborted' if it does not. */
|
||||
function waitForVoiceFlight(flight, signal) {
|
||||
if (signal?.aborted) return Promise.resolve('aborted');
|
||||
@@ -2758,6 +2769,7 @@ function waitForVoiceFlight(flight, signal) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Run one validated voice camera mutation through the UI-owned authority seam. */
|
||||
function runManagedVoiceNavigation(
|
||||
styleManager,
|
||||
noun,
|
||||
|
||||
@@ -340,6 +340,99 @@ test('a tilt with no voice flight pending runs at once', async () => {
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
|
||||
// Orbit, pan and rotate also act at the place the camera ends up, so "fly to
|
||||
// Paris and orbit it" holds the orbit the same way.
|
||||
for (const motion of [
|
||||
{ motion: 'orbit' },
|
||||
{ motion: 'pan', direction: 'left' },
|
||||
{ motion: 'rotate', direction: 'right' },
|
||||
]) {
|
||||
test(`${motion.motion} in the same reply as a fly-to waits for the flight to land`, async () => {
|
||||
const { viewer, styleManager, order, flights } = tiltAfterFlightHarness();
|
||||
const runner = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager: { layers: new Map(), getAll: () => [] },
|
||||
});
|
||||
assert.equal((await runner('fly_to_location', { locationId: 'austin' })).ok, true);
|
||||
const moved = runner('move_camera', motion);
|
||||
const movedDone = settledYet(moved);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(movedDone(), false, `the ${motion.motion} must wait while the camera flies`);
|
||||
assert.equal(
|
||||
order.slice(order.indexOf('fly')).includes('cancel'),
|
||||
false,
|
||||
`the ${motion.motion} must not cancel the flight`,
|
||||
);
|
||||
flights[0].complete();
|
||||
const result = await moved;
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.motion, motion.motion);
|
||||
interruptCameraMotion('test-cleanup');
|
||||
});
|
||||
|
||||
test(`${motion.motion} waits through a geocoded fly-to search, then for its flight`, async () => {
|
||||
const { viewer, styleManager } = tiltAfterFlightHarness();
|
||||
let finishSearch;
|
||||
let landFlight;
|
||||
const runner = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager: { layers: new Map(), getAll: () => [] },
|
||||
searchNavigation: (_viewer, query, options) => new Promise((resolve) => {
|
||||
finishSearch = () => {
|
||||
landFlight = options.onComplete;
|
||||
resolve({ label: query, navigationMode: 'city-overview' });
|
||||
};
|
||||
}),
|
||||
});
|
||||
const fly = runner('fly_to_location', { query: 'Somewhere Far' });
|
||||
const moved = runner('move_camera', motion);
|
||||
const movedDone = settledYet(moved);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(movedDone(), false, `the ${motion.motion} must wait for the place lookup`);
|
||||
assert.equal(getActiveCameraMotion(), null, 'nothing moves before the flight');
|
||||
finishSearch();
|
||||
assert.equal((await fly).ok, true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(movedDone(), false, `the ${motion.motion} must wait for the flight too`);
|
||||
landFlight();
|
||||
assert.equal((await moved).ok, true);
|
||||
assert.equal(getActiveCameraMotion()?.kind, motion.motion);
|
||||
interruptCameraMotion('test-cleanup');
|
||||
});
|
||||
|
||||
test(`${motion.motion} waiting on a flight that never lands is not run`, async () => {
|
||||
const { viewer, styleManager, flights } = tiltAfterFlightHarness();
|
||||
const runner = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager: { layers: new Map(), getAll: () => [] },
|
||||
});
|
||||
await runner('fly_to_location', { locationId: 'austin' });
|
||||
const moved = runner('move_camera', motion);
|
||||
flights[0].cancel();
|
||||
const result = await moved;
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.motion, motion.motion);
|
||||
assert.match(result.error, new RegExp(`this ${motion.motion} was waiting for did not arrive`));
|
||||
assert.equal(getActiveCameraMotion(), null);
|
||||
});
|
||||
}
|
||||
|
||||
test('stop does not wait for a voice flight', async () => {
|
||||
const { viewer, styleManager } = tiltAfterFlightHarness();
|
||||
const runner = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager: { layers: new Map(), getAll: () => [] },
|
||||
});
|
||||
await runner('fly_to_location', { locationId: 'austin' });
|
||||
const result = await runner('move_camera', { motion: 'stop' });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.motion, 'stop');
|
||||
});
|
||||
|
||||
test('nearest-aircraft voice action serializes layer enable, arrival, refresh, airborne query, and selection', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const { viewer, styleManager } = createVoiceNavigationHarness();
|
||||
|
||||
Reference in new issue
Block a user