mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor: separate Scene controls from playback and project operations
This commit is contained in:
1 parent
8b5ad67dcb
commit
cd72feb4fe
12 files changed
+566
-222
No files matched your search
+48
-219
@@ -12,6 +12,7 @@
|
||||
* State is persisted to localStorage and can be exported/imported as JSON.
|
||||
*/
|
||||
|
||||
import { SceneControls } from '../ui/scenes.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { SCENE_RECIPES } from './recipes.js';
|
||||
import { sceneLayerPlan, sceneRequiresContextModeExit } from './scenePolicy.js';
|
||||
@@ -21,8 +22,6 @@ import {
|
||||
decodeBloomIntensity,
|
||||
} from '../bloom.js';
|
||||
|
||||
/** @constant {string} Key code used to abort a running scene */
|
||||
const ESCAPE_KEY = 'Escape';
|
||||
/** @constant {string} localStorage key for the serialized project */
|
||||
const STORAGE_KEY = 'godsEyeView.sceneProject.v2';
|
||||
/** @constant {number} Current schema version for project migration */
|
||||
@@ -32,15 +31,6 @@ const DEFAULT_SHOT_DURATION_SEC = 4;
|
||||
/** @constant {number} Default hold/pause after a shot completes (seconds) */
|
||||
const DEFAULT_HOLD_SEC = 0.9;
|
||||
|
||||
/**
|
||||
* Clamp a numeric value to the [0, 1] range.
|
||||
* @param {number} value
|
||||
* @returns {number}
|
||||
*/
|
||||
function clamp01(value) {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a short random identifier with the given prefix.
|
||||
* @param {string} prefix - e.g. 'shot', 'scene'
|
||||
@@ -298,7 +288,6 @@ export class SceneDirector {
|
||||
constructor(viewer, styleManager, dataManager) {
|
||||
this._destroyed = false;
|
||||
this._pendingWork = new Set();
|
||||
this._uiRemovers = [];
|
||||
this.viewer = viewer;
|
||||
this.styleManager = styleManager;
|
||||
this.dataManager = dataManager;
|
||||
@@ -321,39 +310,14 @@ export class SceneDirector {
|
||||
this._lastRun = null;
|
||||
/** @type {string} JSON string of _lastRun for download */
|
||||
this._lastRunJson = '';
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
|
||||
this._project = this._loadProject();
|
||||
this._selectedSceneId = this._project.scenes[0]?.id || null;
|
||||
this._selectedShotId = this._project.scenes[0]?.shots[0]?.id || null;
|
||||
|
||||
// Cache DOM element references for the scene panel UI
|
||||
this._scenePanel = document.getElementById('scene-panel');
|
||||
this._sceneSelect = document.getElementById('scene-select');
|
||||
this._sceneNewBtn = document.getElementById('scene-new-btn');
|
||||
this._sceneDeleteBtn = document.getElementById('scene-delete-btn');
|
||||
this._sceneCaptureBtn = document.getElementById('scene-capture-btn');
|
||||
this._sceneUpdateShotBtn = document.getElementById('scene-update-shot-btn');
|
||||
this._sceneShotList = document.getElementById('scene-shot-list');
|
||||
this._sceneStartBtn = document.getElementById('scene-start-btn');
|
||||
this._sceneStopBtn = document.getElementById('scene-stop-btn');
|
||||
this._sceneNextBtn = document.getElementById('scene-next-btn');
|
||||
this._sceneExportBtn = document.getElementById('scene-export-btn');
|
||||
this._sceneImportBtn = document.getElementById('scene-import-btn');
|
||||
this._sceneImportFile = document.getElementById('scene-import-file');
|
||||
this._sceneDownloadBtn = document.getElementById('scene-download-btn');
|
||||
this._sceneStatus = document.getElementById('scene-status');
|
||||
this._sceneProgressFill = document.getElementById('scene-progress-fill');
|
||||
this._sceneRuntime = document.getElementById('scene-runtime');
|
||||
|
||||
this._initUI();
|
||||
}
|
||||
|
||||
_listen(target, type, listener) {
|
||||
if (!target) return;
|
||||
target.addEventListener(type, listener);
|
||||
this._uiRemovers.push(() => target.removeEventListener(type, listener));
|
||||
}
|
||||
|
||||
_trackWork(promise) {
|
||||
this._pendingWork ||= new Set();
|
||||
@@ -367,18 +331,15 @@ export class SceneDirector {
|
||||
destroy() {
|
||||
if (this._destroyPromise) return this._destroyPromise;
|
||||
this._destroyed = true;
|
||||
this._controls?.destroy();
|
||||
this._destroyPromise = Promise.resolve().then(async () => {
|
||||
this.stopScene('Stopped');
|
||||
this._loadAbort?.abort();
|
||||
this._loadGeneration++;
|
||||
this.viewer.camera.cancelFlight();
|
||||
for (const remove of this._uiRemovers || []) remove();
|
||||
this._uiRemovers = [];
|
||||
if (this._sceneShotList) this._sceneShotList.textContent = '';
|
||||
clearTimeout(this._storageToastTimer);
|
||||
await Promise.allSettled(this._pendingWork || []);
|
||||
clearInterval(this._progressTimer);
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
});
|
||||
return this._destroyPromise;
|
||||
}
|
||||
@@ -434,79 +395,50 @@ export class SceneDirector {
|
||||
* Exits silently if the scene-select element is missing (headless/test mode).
|
||||
*/
|
||||
_initUI() {
|
||||
if (!this._sceneSelect) return;
|
||||
|
||||
this._renderSceneSelect();
|
||||
this._renderShotList();
|
||||
|
||||
this._listen(this._sceneSelect, 'change', () => {
|
||||
this._selectedSceneId = this._sceneSelect.value;
|
||||
const scene = this._getSelectedScene();
|
||||
this._selectedShotId = scene?.shots[0]?.id || null;
|
||||
this._renderShotList();
|
||||
this._controls = new SceneControls({
|
||||
read: () => ({
|
||||
scenes: this._project.scenes,
|
||||
selectedSceneId: this._selectedSceneId,
|
||||
selectedShotId: this._selectedShotId,
|
||||
running: this._running,
|
||||
hasRun: !!this._lastRunJson,
|
||||
}),
|
||||
actions: {
|
||||
selectScene: (id) => {
|
||||
this._selectedSceneId = id;
|
||||
this._selectedShotId = this._getSelectedScene()?.shots[0]?.id || null;
|
||||
this._renderShotList();
|
||||
},
|
||||
selectShot: (id) => { this._selectedShotId = id; this._renderShotList(); },
|
||||
renameShot: (sceneId, shotId, title) => {
|
||||
const { shot } = this._getShot(sceneId, shotId);
|
||||
if (!shot) return;
|
||||
shot.title = title.trim() || shot.title;
|
||||
this._saveProject();
|
||||
this._renderShotList();
|
||||
},
|
||||
create: () => this._createScene(),
|
||||
deleteScene: () => this._deleteSelectedScene(),
|
||||
capture: () => this.captureShot(),
|
||||
update: () => this.updateSelectedShot(),
|
||||
start: (id) => this.startScene(id),
|
||||
stop: (reason) => this.stopScene(reason),
|
||||
next: () => this.runNextScene(),
|
||||
export: () => this.exportProject(),
|
||||
import: (file) => this.importProjectFile(file),
|
||||
download: () => this.downloadLastRunMetadata(),
|
||||
load: (sceneId, shotId) => this.loadShot(sceneId, shotId),
|
||||
deleteShot: (sceneId, shotId) => this.deleteShot(sceneId, shotId),
|
||||
},
|
||||
});
|
||||
|
||||
this._listen(this._sceneNewBtn, 'click', () => this._createScene());
|
||||
this._listen(this._sceneDeleteBtn, 'click', () => this._deleteSelectedScene());
|
||||
this._listen(this._sceneCaptureBtn, 'click', () => this.captureShot());
|
||||
this._listen(this._sceneUpdateShotBtn, 'click', () => this.updateSelectedShot());
|
||||
|
||||
this._listen(this._sceneStartBtn, 'click', () => {
|
||||
this.startScene(this._selectedSceneId);
|
||||
});
|
||||
|
||||
this._listen(this._sceneStopBtn, 'click', () => {
|
||||
this.stopScene('Stopped');
|
||||
});
|
||||
|
||||
this._listen(this._sceneNextBtn, 'click', () => {
|
||||
this.runNextScene();
|
||||
});
|
||||
|
||||
this._listen(this._sceneExportBtn, 'click', () => {
|
||||
this.exportProject();
|
||||
});
|
||||
|
||||
this._listen(this._sceneImportBtn, 'click', () => {
|
||||
this._sceneImportFile?.click();
|
||||
});
|
||||
|
||||
this._listen(this._sceneImportFile, 'change', async () => {
|
||||
const file = this._sceneImportFile?.files?.[0];
|
||||
if (!file) return;
|
||||
await this.importProjectFile(file);
|
||||
this._sceneImportFile.value = '';
|
||||
});
|
||||
|
||||
this._listen(this._sceneDownloadBtn, 'click', () => {
|
||||
this.downloadLastRunMetadata();
|
||||
});
|
||||
|
||||
this._updateStatus('Ready');
|
||||
this._setProgress(0);
|
||||
this._setButtons(false);
|
||||
}
|
||||
|
||||
/** Rebuild the scene dropdown options and sync the selected value. */
|
||||
_renderSceneSelect() {
|
||||
if (!this._sceneSelect) return;
|
||||
|
||||
this._sceneSelect.innerHTML = '';
|
||||
for (const scene of this._project.scenes) {
|
||||
const option = document.createElement('option');
|
||||
option.value = scene.id;
|
||||
option.textContent = scene.title;
|
||||
this._sceneSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Reset selection if the previously selected scene no longer exists
|
||||
if (!this._project.scenes.some((scene) => scene.id === this._selectedSceneId)) {
|
||||
this._selectedSceneId = this._project.scenes[0]?.id || null;
|
||||
}
|
||||
|
||||
if (this._selectedSceneId) {
|
||||
this._sceneSelect.value = this._selectedSceneId;
|
||||
}
|
||||
this._controls?.renderSceneSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,79 +447,11 @@ export class SceneDirector {
|
||||
* LOAD/DEL action buttons. Supports click-to-select and double-click rename.
|
||||
*/
|
||||
_renderShotList() {
|
||||
if (!this._sceneShotList) return;
|
||||
|
||||
const scene = this._getSelectedScene();
|
||||
this._sceneShotList.innerHTML = '';
|
||||
|
||||
if (!scene || scene.shots.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'scene-shot-empty';
|
||||
empty.textContent = 'No shots yet. Use CAPTURE SHOT to save current look.';
|
||||
this._sceneShotList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-select first shot if current selection is stale
|
||||
if (!scene.shots.some((shot) => shot.id === this._selectedShotId)) {
|
||||
if (scene?.shots.length && !scene.shots.some((shot) => shot.id === this._selectedShotId)) {
|
||||
this._selectedShotId = scene.shots[0].id;
|
||||
}
|
||||
|
||||
for (const shot of scene.shots) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'scene-shot-row';
|
||||
row.classList.toggle('active', shot.id === this._selectedShotId);
|
||||
|
||||
const top = document.createElement('div');
|
||||
top.className = 'scene-shot-top';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'scene-shot-label';
|
||||
label.textContent = shot.title;
|
||||
label.addEventListener('click', () => {
|
||||
this._selectedShotId = shot.id;
|
||||
this._renderShotList();
|
||||
});
|
||||
label.addEventListener('dblclick', () => {
|
||||
const nextTitle = window.prompt('Shot title', shot.title);
|
||||
if (!nextTitle) return;
|
||||
shot.title = nextTitle.trim() || shot.title;
|
||||
this._saveProject();
|
||||
this._renderShotList();
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'scene-shot-actions';
|
||||
|
||||
const loadBtn = document.createElement('button');
|
||||
loadBtn.className = 'scene-shot-btn';
|
||||
loadBtn.textContent = 'LOAD';
|
||||
loadBtn.addEventListener('click', () => {
|
||||
this.loadShot(scene.id, shot.id);
|
||||
});
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'scene-shot-btn scene-shot-danger';
|
||||
deleteBtn.textContent = 'DEL';
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
this.deleteShot(scene.id, shot.id);
|
||||
});
|
||||
|
||||
actions.appendChild(loadBtn);
|
||||
actions.appendChild(deleteBtn);
|
||||
top.appendChild(label);
|
||||
top.appendChild(actions);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'scene-shot-meta';
|
||||
const mode = shot.visual?.detection?.mode || 'OFF';
|
||||
const style = shot.visual?.style || 'normal';
|
||||
meta.textContent = `${style.toUpperCase()} · ${mode} · ${shot.durationSec.toFixed(1)}s + ${shot.holdSec.toFixed(1)}s`;
|
||||
|
||||
row.appendChild(top);
|
||||
row.appendChild(meta);
|
||||
this._sceneShotList.appendChild(row);
|
||||
}
|
||||
this._controls?.renderShotList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -949,7 +813,7 @@ export class SceneDirector {
|
||||
|
||||
// Transition to running state
|
||||
this._running = true;
|
||||
document.body.classList.add('scene-playback-mode');
|
||||
this._controls?.setPlaybackActive(true);
|
||||
this._setButtons(true);
|
||||
this._setProgress(0);
|
||||
|
||||
@@ -984,7 +848,7 @@ export class SceneDirector {
|
||||
|
||||
this._startProgressTicker(estimatedDurationSec || 1);
|
||||
this._logEvent('scene_run_start', { count: queue.length });
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
this._controls?.setPlaybackKeyboardEnabled(true);
|
||||
|
||||
try {
|
||||
// Main shot sequencing loop
|
||||
@@ -1340,14 +1204,14 @@ export class SceneDirector {
|
||||
_finishRun() {
|
||||
clearInterval(this._progressTimer);
|
||||
this._progressTimer = null;
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._controls?.setPlaybackKeyboardEnabled(false);
|
||||
// Covers the error path too: a run that threw mid-shot must not leave a
|
||||
// layer transition running against a director that has stopped watching.
|
||||
this._runAbort?.abort();
|
||||
this._runAbort = null;
|
||||
|
||||
this.styleManager.setRecordingMode(false);
|
||||
document.body.classList.remove('scene-playback-mode');
|
||||
this._controls?.setPlaybackActive(false);
|
||||
this._updateRuntime('');
|
||||
this._running = false;
|
||||
|
||||
@@ -1369,51 +1233,25 @@ export class SceneDirector {
|
||||
* Editing controls are disabled during a run; stop is disabled when idle.
|
||||
* @param {boolean} isRunning
|
||||
*/
|
||||
_setButtons(isRunning) {
|
||||
if (this._sceneStartBtn) this._sceneStartBtn.disabled = isRunning;
|
||||
if (this._sceneNextBtn) this._sceneNextBtn.disabled = isRunning;
|
||||
if (this._sceneSelect) this._sceneSelect.disabled = isRunning;
|
||||
|
||||
if (this._sceneNewBtn) this._sceneNewBtn.disabled = isRunning;
|
||||
if (this._sceneDeleteBtn) this._sceneDeleteBtn.disabled = isRunning;
|
||||
if (this._sceneCaptureBtn) this._sceneCaptureBtn.disabled = isRunning;
|
||||
if (this._sceneUpdateShotBtn) this._sceneUpdateShotBtn.disabled = isRunning;
|
||||
if (this._sceneExportBtn) this._sceneExportBtn.disabled = isRunning;
|
||||
if (this._sceneImportBtn) this._sceneImportBtn.disabled = isRunning;
|
||||
|
||||
if (this._sceneStopBtn) this._sceneStopBtn.disabled = !isRunning;
|
||||
if (this._sceneDownloadBtn) this._sceneDownloadBtn.disabled = !this._lastRunJson;
|
||||
if (this._scenePanel) this._scenePanel.classList.toggle('running', isRunning);
|
||||
}
|
||||
_setButtons(isRunning) { this._controls?.setButtons(isRunning); }
|
||||
|
||||
/**
|
||||
* Update the progress bar fill width and label.
|
||||
* @param {number} progress - Value in [0, 1]
|
||||
*/
|
||||
_setProgress(progress) {
|
||||
if (!this._sceneProgressFill) return;
|
||||
const pct = Math.round(clamp01(progress) * 100);
|
||||
this._sceneProgressFill.style.width = `${pct}%`;
|
||||
this._sceneProgressFill.textContent = `${pct}%`;
|
||||
}
|
||||
_setProgress(progress) { this._controls?.setProgress(progress); }
|
||||
|
||||
/**
|
||||
* Set the status line text in the scene panel.
|
||||
* @param {string} text
|
||||
*/
|
||||
_updateStatus(text) {
|
||||
if (this._sceneStatus) this._sceneStatus.textContent = text;
|
||||
}
|
||||
_updateStatus(text) { this._controls?.updateStatus(text); }
|
||||
|
||||
/**
|
||||
* Set the runtime label (scene/shot name) and toggle its active class.
|
||||
* @param {string} text - Empty string hides the label
|
||||
*/
|
||||
_updateRuntime(text) {
|
||||
if (!this._sceneRuntime) return;
|
||||
this._sceneRuntime.textContent = text;
|
||||
this._sceneRuntime.classList.toggle('active', !!text);
|
||||
}
|
||||
_updateRuntime(text) { this._controls?.updateRuntime(text); }
|
||||
|
||||
/**
|
||||
* Append a timestamped telemetry event to the active run log.
|
||||
@@ -1429,13 +1267,4 @@ export class SceneDirector {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Global keydown handler registered during a run. Escape cancels the run.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
if (event.key === ESCAPE_KEY && this._running) {
|
||||
this.stopScene('Stopped (Esc)');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user