mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat: add declarative Director feature actions
This commit is contained in:
1 parent
a30c42322c
commit
80027756ef
22 files changed
+1150
-17
No files matched your search
+90
-1
@@ -26,6 +26,7 @@ import {
|
||||
} from './recipes.js';
|
||||
import { sceneLayerPlan, sceneRequiresContextModeExit } from './scenePolicy.js';
|
||||
import { createSceneDataPacks } from './dataPacks/controller.js';
|
||||
import { createSceneInteractions } from './interactions.js';
|
||||
import { createDefaultScenePacks } from './packs/defaults.js';
|
||||
import {
|
||||
layerStatesForShot,
|
||||
@@ -89,10 +90,23 @@ export class SceneDirector {
|
||||
this._isMapStackAvailable = isMapStackAvailable;
|
||||
this._scenePacks = scenePacks;
|
||||
this._dataPacks = createSceneDataPacks(viewer, dataPacks);
|
||||
this._interactionTransitions = 0;
|
||||
this._interactions = createSceneInteractions(viewer, {
|
||||
available: () =>
|
||||
!this._destroyed && !this._running && !this.viewer.trackedEntity,
|
||||
execute: (action, signal) =>
|
||||
this._trackWork(this._executeInteraction(action, signal)),
|
||||
});
|
||||
this._cameraMotion = createCameraMotion({
|
||||
applyPose: (pose) => this._setCameraView(pose),
|
||||
});
|
||||
const yieldCamera = () => {
|
||||
const yieldCamera = (event) => {
|
||||
// A settled feature click belongs to the action picker, not a new camera gesture.
|
||||
if (
|
||||
event?.type === 'pointerdown' &&
|
||||
this._interactions?.getState().active
|
||||
)
|
||||
return;
|
||||
if (this._usesAuthoredCamera && !this._claimingCamera)
|
||||
this.stopScene('Camera ownership changed');
|
||||
};
|
||||
@@ -203,6 +217,7 @@ export class SceneDirector {
|
||||
this._cameraHandoffUnsubscribe?.();
|
||||
this._removeCameraInput?.();
|
||||
this._cameraMotion?.destroy();
|
||||
this._interactions?.destroy();
|
||||
this._dataPacks?.destroy();
|
||||
this._controls?.destroy();
|
||||
this._state.destroy();
|
||||
@@ -976,6 +991,7 @@ export class SceneDirector {
|
||||
if (this._destroyed)
|
||||
return Promise.resolve({ started: false, reason: 'destroyed' });
|
||||
this._sceneSeekGeneration++;
|
||||
this._interactionTransitions = 0;
|
||||
return this._trackWork(this._loadShot(sceneId, shotId, options));
|
||||
}
|
||||
|
||||
@@ -1001,6 +1017,7 @@ export class SceneDirector {
|
||||
// manager) rather than merely ignored once it has already committed.
|
||||
this._cancelActiveSceneTravel();
|
||||
this._usesAuthoredCamera = !!shot.move;
|
||||
this._interactions?.clear();
|
||||
this._dataPacks?.clear();
|
||||
this._loadAbort?.abort();
|
||||
const controller = new AbortController();
|
||||
@@ -1060,6 +1077,7 @@ export class SceneDirector {
|
||||
});
|
||||
if (this._loadAbort === controller) this._loadAbort = null;
|
||||
this._updateStatus(`Seeked: ${scene.title} / ${shot.title}`);
|
||||
this._activateInteractions(scene, shot);
|
||||
return { started: true, shotId };
|
||||
}
|
||||
const holdSec = this._effectiveShotHoldSec(scene, shot);
|
||||
@@ -1109,6 +1127,7 @@ export class SceneDirector {
|
||||
);
|
||||
|
||||
if (this._loadAbort === controller) this._loadAbort = null;
|
||||
this._activateInteractions(scene, shot);
|
||||
this._shotOutcome('shot-loaded', scene, shot);
|
||||
this._updateRuntime('');
|
||||
return { started: true, shotId };
|
||||
@@ -1359,6 +1378,8 @@ export class SceneDirector {
|
||||
if (
|
||||
!scene ||
|
||||
!shot ||
|
||||
shot.dataPackIds?.length ||
|
||||
shot.interactions?.length ||
|
||||
this._loadedSceneId !== scene.id ||
|
||||
this._selectedShotId !== shot.id
|
||||
)
|
||||
@@ -1621,6 +1642,7 @@ export class SceneDirector {
|
||||
// Aborting cancels a layer transition already in flight; bumping the
|
||||
// generation disowns everything the load has not yet started.
|
||||
this._cancelActiveSceneTravel();
|
||||
this._interactions?.clear();
|
||||
this._dataPacks?.clear();
|
||||
this._loadAbort?.abort();
|
||||
this._loadAbort = null;
|
||||
@@ -1732,6 +1754,7 @@ export class SceneDirector {
|
||||
* @param {string} [reason='Stopped'] - Human-readable cancellation reason
|
||||
*/
|
||||
stopScene(reason = 'Stopped') {
|
||||
this._interactions?.clear();
|
||||
this._dataPacks?.clear();
|
||||
this._sceneSeekGeneration++;
|
||||
this._loadAbort?.abort();
|
||||
@@ -1763,6 +1786,71 @@ export class SceneDirector {
|
||||
return this._dataPacks?.getState() || { status: 'idle', count: 0 };
|
||||
}
|
||||
|
||||
/** Activate only after LOAD/seek settles. Running timelines never branch automatically. */
|
||||
_activateInteractions(scene, shot) {
|
||||
try {
|
||||
this._interactions?.activate(shot, this._dataPacks.getTargets());
|
||||
} catch (error) {
|
||||
this._updateStatus(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute a validated action through the existing camera and layer admission paths. */
|
||||
async _executeInteraction(action, signal) {
|
||||
if (signal.aborted || this._running || this._destroyed) return false;
|
||||
const { scene, shot } = this._getShot(
|
||||
this._selectedSceneId,
|
||||
this._selectedShotId,
|
||||
);
|
||||
if (!scene || !shot) return false;
|
||||
if (action.type === 'focus') {
|
||||
if (!this._claimCameraOwnership()) return false;
|
||||
this._cancelActiveSceneTravel();
|
||||
this._clock.stopShot();
|
||||
return this._setCameraView(
|
||||
resolveCameraPose(scene, { anchorId: action.anchorId, pitch: -90 }),
|
||||
);
|
||||
}
|
||||
if (action.type === 'layer') {
|
||||
if (
|
||||
!this.dataManager.getAll().some((layer) => layer.id === action.layerId)
|
||||
)
|
||||
return false;
|
||||
return this.dataManager.setEnabled(action.layerId, action.enabled, {
|
||||
signal,
|
||||
origin: 'scene',
|
||||
});
|
||||
}
|
||||
if (action.type === 'shot') {
|
||||
if (this._interactionTransitions >= 64) {
|
||||
this._updateStatus(
|
||||
'Scene transition limit reached — load a shot to reset',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
this._interactionTransitions++;
|
||||
// Replacement clears this action's owner; the new LOAD owns its own cancellation.
|
||||
const target = scene.shots.find((item) => item.id === action.shotId);
|
||||
return this.seekScene(
|
||||
scene.id,
|
||||
this._sceneTimingForShot(scene, target).startProgress,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Copied interaction state for lifecycle diagnostics. */
|
||||
getInteractionState() {
|
||||
return (
|
||||
this._interactions?.getState() || {
|
||||
active: false,
|
||||
busy: false,
|
||||
selected: null,
|
||||
count: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Export the entire project as a timestamped JSON file download. */
|
||||
exportProject() {
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
@@ -1937,6 +2025,7 @@ export class SceneDirector {
|
||||
* @returns {Promise<boolean>} True only when every owned layer is released
|
||||
*/
|
||||
async _releaseSceneLayers(scene, token = null) {
|
||||
this._interactions?.clear();
|
||||
this._dataPacks?.clear();
|
||||
const layerIds = Array.isArray(scene?.releaseLayerIds)
|
||||
? scene.releaseLayerIds
|
||||
|
||||
Reference in new issue
Block a user