fix(director): restore layer visibility on same-shot seeks

This commit is contained in:
Alex O'Neill committed 2026-10-24 23:05:56 +01:00
1 parent 1c234ce027
commit 1644acd2a3
5 files changed
+133 -4

No files matched your search

@@ -0,0 +1,3 @@
- Restore a shot's declared layer visibility when seeking within it after manual
layer changes. Refused restoration reports a failed seek, while undeclared
operator layers and the unchanged-stack shortcut are preserved (@oneill9).
+4
View File
@@ -506,6 +506,10 @@ dateline while preserving both authored endpoint longitudes, and turns heading
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.
Same-shot seeking restores declared layer visibility after a manual toggle,
using the cancellable layer admission path and reporting refused restoration
as a failed seek. Undeclared operator layers remain untouched, and unchanged
declared visibility retains the lightweight seek path.
See [camera directions](DIRECTOR-CAMERA.md).
Realtime voice composes separate connection, response/tool, Radio handoff, input
+51
View File
@@ -88,6 +88,57 @@ try {
};
});
for (const [name, passed] of Object.entries(result)) check(name, passed);
const visibilityRestored = await page.evaluate(async () => {
const { sceneDirector: director, dataManager } = window.__godsEyeView;
const layerId = 'director-seek-visibility-qa';
window.__gevQaRegisterLayer(dataManager, {
id: layerId,
name: 'Director seek visibility QA',
updateInterval: -1,
init() {},
enable() {},
disable() {},
update() {},
destroy() {},
getStats: () => ({ count: 0 }),
});
const scene = {
id: 'visibility-qa',
title: 'Visibility QA',
shots: [
{
id: 'visibility-shot',
title: 'Visibility shot',
durationSec: 0.4,
holdSec: 0,
camera: director.styleManager.getCameraState(),
visual: director.styleManager.getVisualState(),
layers: { [layerId]: { enabled: true } },
},
],
};
director._project.scenes.push(scene);
try {
const loaded = await director.loadShot(scene.id, scene.shots[0].id, {
flyDuration: 0.2,
});
await dataManager.setEnabled(layerId, false, { origin: 'user' });
const sought = await director.seekScene(scene.id, 0.5);
return (
loaded?.started && sought && dataManager.layers.get(layerId).enabled
);
} finally {
director.stopScene();
director._project.scenes = director._project.scenes.filter(
({ id }) => id !== scene.id,
);
await window.__gevQaUnregisterLayer(dataManager, layerId);
}
});
check(
'same-shot seek restores manually changed layer visibility',
visibilityRestored,
);
const content = await page.evaluate(async () => {
const d = window.__godsEyeView.sceneDirector;
const scene = d._project.scenes.find(({ title }) =>
+15 -4
View File
@@ -1529,16 +1529,27 @@ export class SceneDirector {
this._selectedShotId !== shot.id
)
return false;
const states = this._layerStatesForShot(scene, shot, {
cameraSettled: seekState.cameraProgress >= 1,
sceneSeek: seekState,
});
const layers = new Map(
this.dataManager.getAll().map((layer) => [layer.id, layer.enabled]),
);
// A manual toggle invalidates the loaded stack. Let LOAD reconcile it
// through the normal cancellable admission path before reporting success.
if (
sceneLayerPlan(states, new Set(layers.keys())).some(
({ id, enabled }) => Boolean(layers.get(id)) !== enabled,
)
)
return false;
if (!this._claimCameraOwnership()) return false;
this._cancelActiveSceneTravel();
this._loadAbort?.abort();
this._loadAbort = null;
this._loadGeneration += 1;
this._clock.stopShot();
const states = this._layerStatesForShot(scene, shot, {
cameraSettled: seekState.cameraProgress >= 1,
sceneSeek: seekState,
});
let applied = true;
for (const [layerId, state] of Object.entries(states)) {
if (!state?.enabled || !state.params) continue;
+60
View File
@@ -174,6 +174,66 @@ test('scene clock seek resolves the exact shot phase and camera in both directio
}
});
test('same-shot seeking restores declared layer visibility after manual changes', async () => {
const { director, dataManager, restore } = makeDirector();
try {
const shot = director._project.scenes[0].shots[0];
shot.layers = { flights: { enabled: true }, traffic: { enabled: false } };
await director.loadShot('scene-1', 'shot-a', { flyDuration: 0 });
await dataManager.setEnabled('flights', false);
await dataManager.setEnabled('traffic', true);
await dataManager.setEnabled('satellites', true);
dataManager.setEnabledCalls.length = 0;
assert.equal(await director.seekScene('scene-1', 0.25), true);
const enabled = new Map(dataManager.getAll().map(({ id, enabled }) => [id, enabled]));
assert.equal(enabled.get('flights'), true, 'restore the shot\'s enabled layer');
assert.equal(enabled.get('traffic'), false, 'restore the shot\'s disabled layer');
assert.equal(enabled.get('satellites'), true, 'preserve undeclared operator layers');
assert.ok(dataManager.setEnabledCalls.every(({ id }) => id !== 'satellites'));
} finally {
await director.destroy();
restore();
}
});
test('same-shot seeking reports refusal when a manually disabled layer cannot be restored', async () => {
let refuse = false;
const { director, dataManager, restore } = makeDirector({
data: { refuse: () => refuse },
});
try {
await director.loadShot('scene-1', 'shot-a', { flyDuration: 0 });
await dataManager.setEnabled('flights', false);
refuse = true;
assert.equal(await director.seekScene('scene-1', 0.25), false);
assert.equal(dataManager.getAll().find(({ id }) => id === 'flights').enabled, false);
assert.equal(director.getPlaybackTimingState().activeTimers, 0);
} finally {
await director.destroy();
restore();
}
});
test('same-shot seeking keeps the shortcut when declared visibility is unchanged', async () => {
const { director, dataManager, styleManager, restore } = makeDirector();
try {
await director.loadShot('scene-1', 'shot-a', { flyDuration: 0 });
await dataManager.setEnabled('satellites', true);
dataManager.setEnabledCalls.length = 0;
const visualCalls = styleManager.visualCalls.length;
assert.equal(await director.seekScene('scene-1', 0.25), true);
assert.deepEqual(dataManager.setEnabledCalls, []);
assert.equal(styleManager.visualCalls.length, visualCalls);
assert.equal(director.getPlaybackTimingState().snapshot.sceneProgress, 0.25);
} finally {
await director.destroy();
restore();
}
});
test('scene clock subscribers receive authoritative forward playback snapshots', () => {
const { director, restore } = makeDirector();
const snapshots = [];