mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Merge branch 'main' into fix/director-scene-results-971
# Conflicts: # src/scenes/director.js
This commit is contained in:
commit
75b1c66006
700 files changed
+66498
-6002
No files matched your search
+124
-42
@@ -23,9 +23,12 @@ import * as Cesium from 'cesium';
|
||||
import {
|
||||
SCENE_APPEND_RECIPES,
|
||||
SCENE_RECIPES,
|
||||
displaySceneTitle,
|
||||
displayShotTitle,
|
||||
getSceneAppendRecipeById,
|
||||
} from './recipes.js';
|
||||
import { sceneLayerPlan, sceneRequiresContextModeExit } from './scenePolicy.js';
|
||||
import { t, tn, msg } from '../i18n/index.js';
|
||||
import { createSceneDataPacks } from './dataPacks/controller.js';
|
||||
import { createSceneInteractions } from './interactions.js';
|
||||
import { createSceneSharing } from './sharing.js';
|
||||
@@ -60,6 +63,7 @@ import {
|
||||
stringifySceneDocument,
|
||||
SceneDocumentError,
|
||||
} from '../director/document.js';
|
||||
import { sceneSaveFailureReason } from './sceneSaveError.js';
|
||||
|
||||
/** @constant {string} localStorage key for the serialized project */
|
||||
const STORAGE_KEY = 'godsEyeView.sceneProject.v2';
|
||||
@@ -122,7 +126,7 @@ export class SceneDirector {
|
||||
)
|
||||
return;
|
||||
if (this._usesAuthoredCamera && !this._claimingCamera)
|
||||
this.stopScene('Camera ownership changed');
|
||||
this.stopScene(msg('Camera ownership changed'));
|
||||
};
|
||||
this._cameraHandoffUnsubscribe =
|
||||
styleManager.subscribeCameraHandoff?.(yieldCamera);
|
||||
@@ -171,8 +175,10 @@ export class SceneDirector {
|
||||
|
||||
this._presentation = {
|
||||
status: this._storageReadError
|
||||
? 'Saved project could not be read; storage preserved. Import a valid file to resume saving.'
|
||||
: 'Ready',
|
||||
? t(
|
||||
'Saved project could not be read; storage preserved. Import a valid file to resume saving.',
|
||||
)
|
||||
: t('Ready'),
|
||||
progress: 0,
|
||||
runtime: '',
|
||||
playbackActive: false,
|
||||
@@ -212,7 +218,7 @@ export class SceneDirector {
|
||||
return;
|
||||
const scene = this._getSelectedScene();
|
||||
if (!scene?.releaseLayerIds?.includes(change.layerId)) return;
|
||||
this.stopScene('Scene layer turned off');
|
||||
this.stopScene(msg('Scene layer turned off'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,7 +246,7 @@ export class SceneDirector {
|
||||
this._controls?.destroy();
|
||||
this._state.destroy();
|
||||
this._destroyPromise = Promise.resolve().then(async () => {
|
||||
this.stopScene('Stopped');
|
||||
this.stopScene(msg('Stopped'));
|
||||
this._loadAbort?.abort();
|
||||
this._loadGeneration++;
|
||||
this.viewer.camera.cancelFlight();
|
||||
@@ -316,7 +322,9 @@ export class SceneDirector {
|
||||
_saveProject() {
|
||||
if (this._storageReadError) {
|
||||
this._toastStorageError(
|
||||
'Scene not saved — existing saved project could not be read. Export edits or import a valid file.',
|
||||
t(
|
||||
'Scene not saved — existing saved project could not be read. Export edits or import a valid file.',
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -330,13 +338,20 @@ export class SceneDirector {
|
||||
// Private browsing / block-all-cookies / quota-exceeded throws here. The
|
||||
// in-memory project stays usable this session, but persistence failed —
|
||||
// tell the user instead of crashing the caller (M11).
|
||||
// A validation failure is logged with its technical path and check;
|
||||
// the toast says it in words (sceneSaveError.js).
|
||||
const invalid = e instanceof SceneDocumentError;
|
||||
console.warn(
|
||||
'[Scenes] Could not persist project (storage unavailable):',
|
||||
invalid
|
||||
? '[Scenes] Could not persist project (invalid project data):'
|
||||
: '[Scenes] Could not persist project (storage unavailable):',
|
||||
e,
|
||||
);
|
||||
this._toastStorageError(
|
||||
e instanceof SceneDocumentError
|
||||
? `Scene not saved — ${e.message}`
|
||||
invalid
|
||||
? t('Scene not saved — {reason}', {
|
||||
reason: sceneSaveFailureReason(e),
|
||||
})
|
||||
: undefined,
|
||||
);
|
||||
return false;
|
||||
@@ -345,7 +360,7 @@ export class SceneDirector {
|
||||
|
||||
/** Surface a "scene not saved" notice via the global toast + scene status line. */
|
||||
_toastStorageError(
|
||||
message = 'Scene not saved — browser storage unavailable',
|
||||
message = t('Scene not saved — browser storage unavailable'),
|
||||
) {
|
||||
this._updateStatus(message);
|
||||
try {
|
||||
@@ -374,7 +389,10 @@ export class SceneDirector {
|
||||
|
||||
_shotOutcome(type, scene, shot, index = scene.shots.indexOf(shot)) {
|
||||
if (type === 'shot-loaded')
|
||||
this._presentation.status = `Loaded: ${scene.title} / ${shot.title}`;
|
||||
this._presentation.status = t('Loaded: {scene} / {shot}', {
|
||||
scene: displaySceneTitle(scene),
|
||||
shot: displayShotTitle(shot.title, shot, scene),
|
||||
});
|
||||
this._publish({
|
||||
type,
|
||||
sceneId: scene.id,
|
||||
@@ -393,7 +411,16 @@ export class SceneDirector {
|
||||
this._controls = new SceneControls({
|
||||
subscribe: (listener) => this.subscribe(listener),
|
||||
read: () => ({
|
||||
scenes: this._project.scenes,
|
||||
// The panel shows display titles; shots keep the stored title for
|
||||
// editing and matching.
|
||||
scenes: this._project.scenes.map((scene) => ({
|
||||
...scene,
|
||||
title: displaySceneTitle(scene),
|
||||
shots: scene.shots.map((shot) => ({
|
||||
...shot,
|
||||
displayTitle: displayShotTitle(shot.title, shot, scene),
|
||||
})),
|
||||
})),
|
||||
selectedSceneId: this._selectedSceneId,
|
||||
selectedShotId: this._selectedShotId,
|
||||
running: this._running,
|
||||
@@ -489,6 +516,7 @@ export class SceneDirector {
|
||||
|
||||
const scene = {
|
||||
id: uid('scene'),
|
||||
// i18n-ignore: stored English, like normalizeProject; displaySceneTitle translates it.
|
||||
title: sceneName.trim() || `Scene ${this._project.scenes.length + 1}`,
|
||||
shots: [],
|
||||
};
|
||||
@@ -767,7 +795,9 @@ export class SceneDirector {
|
||||
));
|
||||
if (!inventoryMatches) {
|
||||
this._updateStatus(
|
||||
`Cannot update ${recipe.title}: shot inventory changed`,
|
||||
t('Cannot update {scene}: shot inventory changed', {
|
||||
scene: t(recipe.title),
|
||||
}),
|
||||
);
|
||||
return {
|
||||
appended: false,
|
||||
@@ -798,7 +828,9 @@ export class SceneDirector {
|
||||
);
|
||||
if (!sourcePackMatches) {
|
||||
this._updateStatus(
|
||||
`Cannot update ${recipe.title}: evidence beats changed`,
|
||||
t('Cannot update {scene}: evidence beats changed', {
|
||||
scene: t(recipe.title),
|
||||
}),
|
||||
);
|
||||
return {
|
||||
appended: false,
|
||||
@@ -821,7 +853,9 @@ export class SceneDirector {
|
||||
}));
|
||||
if (resolvedPatches.some(({ shot }) => !shot)) {
|
||||
this._updateStatus(
|
||||
`Cannot update ${recipe.title}: shot bindings are incomplete`,
|
||||
t('Cannot update {scene}: shot bindings are incomplete', {
|
||||
scene: t(recipe.title),
|
||||
}),
|
||||
);
|
||||
return {
|
||||
appended: false,
|
||||
@@ -901,8 +935,18 @@ export class SceneDirector {
|
||||
if (announce && saved) {
|
||||
this._updateStatus(
|
||||
marker
|
||||
? `Updated ${patchedShotCount} ${patchedShotCount === 1 ? 'shot' : 'shots'}: ${recipe.title}`
|
||||
: `Appended ${appendedShots.length} shots: ${recipe.title}`,
|
||||
? tn(
|
||||
patchedShotCount,
|
||||
'Updated {count} shot: {scene}',
|
||||
'Updated {count} shots: {scene}',
|
||||
{ scene: t(recipe.title) },
|
||||
)
|
||||
: tn(
|
||||
appendedShots.length,
|
||||
'Appended {count} shot: {scene}',
|
||||
'Appended {count} shots: {scene}',
|
||||
{ scene: t(recipe.title) },
|
||||
),
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -924,13 +968,14 @@ export class SceneDirector {
|
||||
|
||||
const camera = this.styleManager.getCameraState();
|
||||
if (!camera) {
|
||||
this._updateStatus('Cannot capture shot: camera not ready');
|
||||
this._updateStatus(t('Cannot capture shot: camera not ready'));
|
||||
return;
|
||||
}
|
||||
|
||||
const shot = normalizeShot(
|
||||
{
|
||||
id: uid('shot'),
|
||||
// i18n-ignore: stored English; displayShotTitle translates it when shown.
|
||||
title: `Shot ${scene.shots.length + 1}`,
|
||||
durationSec: DEFAULT_SHOT_DURATION_SEC,
|
||||
holdSec: DEFAULT_HOLD_SEC,
|
||||
@@ -945,7 +990,14 @@ export class SceneDirector {
|
||||
this._selectedShotId = shot.id;
|
||||
const saved = this._saveProject();
|
||||
this._shotOutcome('shot-captured', scene, shot);
|
||||
if (saved) this._updateStatus(`Captured: ${scene.title} / ${shot.title}`);
|
||||
if (saved) {
|
||||
this._updateStatus(
|
||||
t('Captured: {scene} / {shot}', {
|
||||
scene: displaySceneTitle(scene),
|
||||
shot: displayShotTitle(shot.title, shot, scene),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -958,7 +1010,7 @@ export class SceneDirector {
|
||||
|
||||
const shot = scene.shots.find((item) => item.id === this._selectedShotId);
|
||||
if (!shot) {
|
||||
this._updateStatus('Select a shot first');
|
||||
this._updateStatus(t('Select a shot first'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -973,7 +1025,14 @@ export class SceneDirector {
|
||||
|
||||
const saved = this._saveProject();
|
||||
this._shotOutcome('shot-updated', scene, shot);
|
||||
if (saved) this._updateStatus(`Updated: ${scene.title} / ${shot.title}`);
|
||||
if (saved) {
|
||||
this._updateStatus(
|
||||
t('Updated: {scene} / {shot}', {
|
||||
scene: displaySceneTitle(scene),
|
||||
shot: displayShotTitle(shot.title, shot, scene),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1079,7 +1138,11 @@ export class SceneDirector {
|
||||
const released = await this._releaseSceneLayers(previousScene, token);
|
||||
if (!released || token.cancelled) {
|
||||
if (!token.cancelled)
|
||||
this._updateStatus(`Could not leave scene: ${previousScene.title}`);
|
||||
this._updateStatus(
|
||||
t('Could not leave scene: {scene}', {
|
||||
scene: displaySceneTitle(previousScene),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._loadedSceneId = null;
|
||||
@@ -1129,7 +1192,12 @@ export class SceneDirector {
|
||||
seeking: true,
|
||||
});
|
||||
if (this._loadAbort === controller) this._loadAbort = null;
|
||||
this._updateStatus(`Seeked: ${scene.title} / ${shot.title}`);
|
||||
this._updateStatus(
|
||||
t('Seeked: {scene} / {shot}', {
|
||||
scene: displaySceneTitle(scene),
|
||||
shot: displayShotTitle(shot.title, shot, scene),
|
||||
}),
|
||||
);
|
||||
this._activateInteractions(scene, shot);
|
||||
return { started: true, shotId };
|
||||
}
|
||||
@@ -1491,14 +1559,19 @@ export class SceneDirector {
|
||||
running: false,
|
||||
seeking: true,
|
||||
});
|
||||
this._updateStatus(`Seeked: ${scene.title} / ${shot.title}`);
|
||||
this._updateStatus(
|
||||
t('Seeked: {scene} / {shot}', {
|
||||
scene: displaySceneTitle(scene),
|
||||
shot: displayShotTitle(shot.title, shot, scene),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Seek the complete authored scene state to an exact clock position. */
|
||||
seekScene(sceneId, progress) {
|
||||
if (this._destroyed) return Promise.resolve(false);
|
||||
this.stopScene('Seeking scene clock');
|
||||
this.stopScene(msg('Seeking scene clock'));
|
||||
const generation = ++this._sceneSeekGeneration;
|
||||
return this._trackWork(this._seekScene(sceneId, progress, generation));
|
||||
}
|
||||
@@ -1568,7 +1641,7 @@ export class SceneDirector {
|
||||
this._claimingCamera = false;
|
||||
}
|
||||
if (claimed === false) {
|
||||
this._updateStatus('Camera unavailable — exit cockpit first');
|
||||
this._updateStatus(t('Camera unavailable — exit cockpit first'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1703,7 +1776,7 @@ export class SceneDirector {
|
||||
if (!queue.length) return { started: false, reason: 'scene-complete' };
|
||||
}
|
||||
if (!queue.length) {
|
||||
this._updateStatus('No shots to run');
|
||||
this._updateStatus(t('No shots to run'));
|
||||
return { started: false, reason: 'no-shots' };
|
||||
}
|
||||
|
||||
@@ -1765,6 +1838,7 @@ export class SceneDirector {
|
||||
// Initialize telemetry accumulator for this run
|
||||
this._activeRun = {
|
||||
recipeId: `project-${PROJECT_VERSION}`,
|
||||
// i18n-ignore: run telemetry record, downloaded as JSON
|
||||
title: 'Editable Scene Run',
|
||||
startedAt: new Date().toISOString(),
|
||||
estimatedDurationSec,
|
||||
@@ -1787,7 +1861,11 @@ export class SceneDirector {
|
||||
releaseOnFinish: preview,
|
||||
});
|
||||
} catch (error) {
|
||||
this._updateStatus(`Error: ${error.message || 'run failed'}`);
|
||||
this._updateStatus(
|
||||
t('Error: {message}', {
|
||||
message: error.message || 'run failed',
|
||||
}),
|
||||
);
|
||||
this._logEvent('scene_run_error', {
|
||||
message: error.message || 'unknown error',
|
||||
});
|
||||
@@ -1833,9 +1911,11 @@ export class SceneDirector {
|
||||
* enabled while the post-await check skips its params. The manager rolls an
|
||||
* aborted enable back through the module's own disable().
|
||||
*
|
||||
* @param {string} [reason='Stopped'] - Human-readable cancellation reason
|
||||
* @param {string} [reason='Stopped'] - Cancellation reason, in English: it
|
||||
* is logged as data and translated only for the status line (callers mark
|
||||
* theirs with msg()).
|
||||
*/
|
||||
stopScene(reason = 'Stopped') {
|
||||
stopScene(reason = msg('Stopped')) {
|
||||
this._setSceneMediaPlayback();
|
||||
this._interactions?.clear();
|
||||
this._dataPacks?.clear();
|
||||
@@ -1850,7 +1930,7 @@ export class SceneDirector {
|
||||
if (!this._running || !this._runToken) return;
|
||||
this._runToken.cancelled = true;
|
||||
this._runAbort?.abort();
|
||||
this._updateStatus(reason);
|
||||
this._updateStatus(t(reason));
|
||||
this._logEvent('scene_stopped', { reason });
|
||||
}
|
||||
|
||||
@@ -1907,7 +1987,7 @@ export class SceneDirector {
|
||||
if (action.type === 'shot') {
|
||||
if (this._interactionTransitions >= 64) {
|
||||
this._updateStatus(
|
||||
'Scene transition limit reached — load a shot to reset',
|
||||
t('Scene transition limit reached — load a shot to reset'),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -1950,7 +2030,7 @@ export class SceneDirector {
|
||||
try {
|
||||
payload = stringifySceneDocument(this._project);
|
||||
} catch (error) {
|
||||
this._updateStatus(`Export failed: ${error.message}`);
|
||||
this._updateStatus(t('Export failed: {error}', { error: error.message }));
|
||||
return;
|
||||
}
|
||||
// Trigger a browser download via a temporary anchor element
|
||||
@@ -1963,7 +2043,7 @@ export class SceneDirector {
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
this._presentation.status = 'Project exported';
|
||||
this._presentation.status = t('Project exported');
|
||||
this._publish({ type: 'project-exported', project: this._project });
|
||||
}
|
||||
|
||||
@@ -1993,7 +2073,7 @@ export class SceneDirector {
|
||||
)
|
||||
return false;
|
||||
// Validate before touching playback, selection or the saved project.
|
||||
this.stopScene('Importing project');
|
||||
this.stopScene(msg('Importing project'));
|
||||
await Promise.allSettled([...(this._pendingWork || [])]);
|
||||
if (this._destroyed || generation !== this._importGeneration) return;
|
||||
if (
|
||||
@@ -2023,7 +2103,7 @@ export class SceneDirector {
|
||||
this._loadedSceneId = null;
|
||||
const saved = this._saveProject();
|
||||
this._publish({ type: 'project-imported', project });
|
||||
if (saved) this._updateStatus(`Imported ${file.name}`);
|
||||
if (saved) this._updateStatus(t('Imported {file}', { file: file.name }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -2034,8 +2114,8 @@ export class SceneDirector {
|
||||
return false;
|
||||
this._updateStatus(
|
||||
error instanceof SceneDocumentError
|
||||
? `Import failed: ${error.message}`
|
||||
: 'Import failed (could not read JSON file)',
|
||||
? t('Import failed: {error}', { error: error.message })
|
||||
: t('Import failed (could not read JSON file)'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2135,7 +2215,9 @@ export class SceneDirector {
|
||||
}
|
||||
|
||||
if (refused.length) {
|
||||
this._updateStatus(`Layers refused: ${refused.join(', ')}`);
|
||||
this._updateStatus(
|
||||
t('Layers refused: {layers}', { layers: refused.join(', ') }),
|
||||
);
|
||||
this._logEvent('shot_layers_refused', { layerIds: [...refused] });
|
||||
}
|
||||
return { applied, refused, cancelled: false };
|
||||
@@ -2226,7 +2308,7 @@ export class SceneDirector {
|
||||
result.error || 'unknown reason',
|
||||
);
|
||||
this._updateStatus(
|
||||
`Could not exit ${mode} — scene layers may be refused`,
|
||||
t('Could not exit {mode} — scene layers may be refused', { mode }),
|
||||
);
|
||||
this._logEvent('context_mode_exit_failed', {
|
||||
mode,
|
||||
@@ -2252,7 +2334,7 @@ export class SceneDirector {
|
||||
token,
|
||||
);
|
||||
if (!completed && !token.cancelled && !this._destroyed)
|
||||
this.stopScene('Camera move interrupted');
|
||||
this.stopScene(msg('Camera move interrupted'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2341,7 +2423,7 @@ export class SceneDirector {
|
||||
const pending = readers.filter(({ read }) => read()?.pending === true);
|
||||
if (!pending.length) return;
|
||||
if (pending.some(({ maxWaitMs }) => Date.now() - began >= maxWaitMs)) {
|
||||
this.stopScene('Scene media timed out');
|
||||
this.stopScene(msg('Scene media timed out'));
|
||||
throw new Error(
|
||||
'Scene media did not finish within its bounded playback window',
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user