mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(director): land on the requested shot
A shot action seeks to the target shot's startProgress, which sceneTimingForShot computes as start / total. sceneSeekState multiplied that back into seconds and compared it with each shot's end, but (start / total) * total can round one ulp below start. The comparison then put the seek at the very end of the previous shot. With five equal 3.9 s shots, "go to" shots 2, 3 and 5 landed on shots 1, 2 and 4. Compare the requested progress against each shot's end / total instead, the same division the caller used, so a shot's own start always selects that shot. Exact boundaries still select the next shot, and anything below a start still selects the previous one.
This commit is contained in:
1 parent
d4492d8f8c
commit
bd48533b03
4 files changed
+54
-4
No files matched your search
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Director shot actions land on the requested shot. A seek to a shot's start
|
||||||
|
could round a hair below it and select the end of the previous shot for some
|
||||||
|
shot counts and durations.
|
||||||
|
|
||||||
- Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the
|
- Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the
|
||||||
city's PBSC system) and AMBici (237 e-bike stations across the metropolitan
|
city's PBSC system) and AMBici (237 e-bike stations across the metropolitan
|
||||||
area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in
|
area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in
|
||||||
|
|||||||
@@ -427,6 +427,8 @@ reference. Legacy projects retain their ordinary flights, content and edits.
|
|||||||
Seeking between ordinary camera poses uses the shortest longitude arc across the
|
Seeking between ordinary camera poses uses the shortest longitude arc across the
|
||||||
dateline while preserving both authored endpoint longitudes, and turns heading
|
dateline while preserving both authored endpoint longitudes, and turns heading
|
||||||
and roll the short way for any authored angles.
|
and roll the short way for any authored angles.
|
||||||
|
A seek to a shot's start, as a shot action makes, selects that shot rather than
|
||||||
|
the end of the previous one.
|
||||||
See [camera directions](DIRECTOR-CAMERA.md).
|
See [camera directions](DIRECTOR-CAMERA.md).
|
||||||
|
|
||||||
Realtime voice composes separate connection, response/tool, Radio handoff, input
|
Realtime voice composes separate connection, response/tool, Radio handoff, input
|
||||||
|
|||||||
@@ -72,19 +72,27 @@ export function sceneSeekState(scene, progress, durationForShot, holdForShot) {
|
|||||||
let shotIndex = scene.shots.length - 1;
|
let shotIndex = scene.shots.length - 1;
|
||||||
for (let index = 0; index < scene.shots.length; index += 1) {
|
for (let index = 0; index < scene.shots.length; index += 1) {
|
||||||
const endElapsedSec = startElapsedSec + durations[index];
|
const endElapsedSec = startElapsedSec + durations[index];
|
||||||
if (targetSec < endElapsedSec || index === scene.shots.length - 1) {
|
// Compare in progress units: a shot's startProgress (start / total)
|
||||||
|
// times total can round below its start and select the previous shot.
|
||||||
|
if (
|
||||||
|
normalized < endElapsedSec / totalSec ||
|
||||||
|
index === scene.shots.length - 1
|
||||||
|
) {
|
||||||
shotIndex = index;
|
shotIndex = index;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
startElapsedSec = endElapsedSec;
|
startElapsedSec = endElapsedSec;
|
||||||
}
|
}
|
||||||
|
// Keep the elapsed time inside the selected shot, so it agrees with
|
||||||
|
// shotIndex when targetSec rounded below the shot's start.
|
||||||
|
const elapsedSec = Math.max(targetSec, startElapsedSec);
|
||||||
const shot = scene.shots[shotIndex];
|
const shot = scene.shots[shotIndex];
|
||||||
const flightDurationSec = shot.durationSec || DEFAULT_SHOT_DURATION_SEC;
|
const flightDurationSec = shot.durationSec || DEFAULT_SHOT_DURATION_SEC;
|
||||||
const holdDurationSec = holdForShot(scene, shot);
|
const holdDurationSec = holdForShot(scene, shot);
|
||||||
const shotDurationSec = Math.max(0.001, flightDurationSec + holdDurationSec);
|
const shotDurationSec = Math.max(0.001, flightDurationSec + holdDurationSec);
|
||||||
const shotElapsedSec = Math.max(
|
const shotElapsedSec = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(shotDurationSec, targetSec - startElapsedSec),
|
Math.min(shotDurationSec, elapsedSec - startElapsedSec),
|
||||||
);
|
);
|
||||||
const cameraProgress = clamp01(
|
const cameraProgress = clamp01(
|
||||||
shotElapsedSec / Math.max(0.001, flightDurationSec),
|
shotElapsedSec / Math.max(0.001, flightDurationSec),
|
||||||
@@ -99,8 +107,8 @@ export function sceneSeekState(scene, progress, durationForShot, holdForShot) {
|
|||||||
);
|
);
|
||||||
const move = resolveCameraMove(scene, shot);
|
const move = resolveCameraMove(scene, shot);
|
||||||
return {
|
return {
|
||||||
sceneProgress: totalSec > 0 ? targetSec / totalSec : 0,
|
sceneProgress: totalSec > 0 ? elapsedSec / totalSec : 0,
|
||||||
sceneElapsedSec: targetSec,
|
sceneElapsedSec: elapsedSec,
|
||||||
sceneDurationSec: totalSec,
|
sceneDurationSec: totalSec,
|
||||||
shotIndex,
|
shotIndex,
|
||||||
shot,
|
shot,
|
||||||
|
|||||||
@@ -49,3 +49,39 @@ test('camera seeking preserves cubic easing and shortest-angle orientation', ()
|
|||||||
);
|
);
|
||||||
assert.deepEqual(cameraAtProgress(null, camera(20, 350), 0), camera(20, 350));
|
assert.deepEqual(cameraAtProgress(null, camera(20, 350), 0), camera(20, 350));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('seeking to a shot start lands in that shot for every shot count and duration', () => {
|
||||||
|
const misses = [];
|
||||||
|
for (let count = 2; count <= 12; count += 1) {
|
||||||
|
for (let flightTenths = 1; flightTenths <= 100; flightTenths += 1) {
|
||||||
|
for (let holdTenths = 0; holdTenths <= 50; holdTenths += 1) {
|
||||||
|
const shots = Array.from({ length: count }, (_, index) => ({
|
||||||
|
id: `s${index}`,
|
||||||
|
// Every third shot is longer, so boundaries are not all equal steps.
|
||||||
|
durationSec: flightTenths / 10 + (index % 3 === 2 ? 1.7 : 0),
|
||||||
|
holdSec: holdTenths / 10,
|
||||||
|
camera: camera(20, 0),
|
||||||
|
}));
|
||||||
|
const mixed = { shots };
|
||||||
|
for (const [index, shot] of shots.entries()) {
|
||||||
|
const { startProgress, startElapsedSec } = sceneTimingForShot(
|
||||||
|
mixed,
|
||||||
|
shot,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
const seek = sceneSeekState(mixed, startProgress, duration, hold);
|
||||||
|
// One or two ulps earlier still belongs to the previous shot.
|
||||||
|
const before = startProgress * (1 - Number.EPSILON);
|
||||||
|
const previous = sceneSeekState(mixed, before, duration, hold);
|
||||||
|
if (
|
||||||
|
seek.shotIndex !== index ||
|
||||||
|
seek.sceneElapsedSec < startElapsedSec ||
|
||||||
|
previous.shotIndex !== Math.max(0, index - 1)
|
||||||
|
)
|
||||||
|
misses.push(`${count}x${flightTenths}/${holdTenths}#${index}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(misses.slice(0, 5), []);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user