mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
176 lines
6.1 KiB
JavaScript
176 lines
6.1 KiB
JavaScript
import { displaySceneTitle, displayShotTitle } from './recipes.js';
|
|
|
|
const DEFAULT_SHOT_DURATION_SEC = 4;
|
|
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
const deepClone = (value) => JSON.parse(JSON.stringify(value));
|
|
|
|
/** Prepare scene-owned layer state, timing context and seek/travel controls. */
|
|
export function layerStatesForShot(
|
|
scene,
|
|
shot,
|
|
packs,
|
|
{ cameraSettled = false, cameraTravel = null, sceneSeek = null } = {},
|
|
) {
|
|
const states = deepClone(shot?.layers || {});
|
|
const shotIndex = scene?.shots?.findIndex(({ id }) => id === shot?.id) ?? -1;
|
|
const sceneShotDurations =
|
|
scene?.shots?.map((sceneShot) =>
|
|
shotRuntimeDurationSec(scene, sceneShot, packs),
|
|
) || [];
|
|
const sceneDurationSec = sceneShotDurations.reduce(
|
|
(sum, duration) => sum + duration,
|
|
0,
|
|
);
|
|
const authoredSceneElapsedSec = sceneShotDurations
|
|
.slice(0, Math.max(0, shotIndex))
|
|
.reduce((sum, duration) => sum + duration, 0);
|
|
const sceneElapsedSec = sceneSeek
|
|
? Math.max(
|
|
0,
|
|
Math.min(sceneDurationSec, Number(sceneSeek.sceneElapsedSec) || 0),
|
|
)
|
|
: authoredSceneElapsedSec;
|
|
const sourceRecipe = packs.recipeForShot(shot);
|
|
for (const state of Object.values(states)) {
|
|
if (state?.params?.presentation !== 'scene-beat') continue;
|
|
state.params.sceneSurface ||= shot.sourcePackId
|
|
? 'evidence-beat'
|
|
: 'panel-only';
|
|
const runtimeControls =
|
|
sourceRecipe?.runtimeControlsByBeat?.[state.params.beatId];
|
|
if (runtimeControls) {
|
|
state.params.sceneControls = {
|
|
...state.params.sceneControls,
|
|
...runtimeControls,
|
|
...(runtimeControls.deferEvidenceUntilCameraSettled
|
|
? { cameraSettled }
|
|
: {}),
|
|
...(sceneSeek
|
|
? {
|
|
timelineSeek: {
|
|
shotElapsedSec: Number(sceneSeek.shotElapsedSec) || 0,
|
|
flightDurationSec: Number(sceneSeek.flightDurationSec) || 0,
|
|
holdDurationSec: Number(sceneSeek.holdDurationSec) || 0,
|
|
holdElapsedSec: Number(sceneSeek.holdElapsedSec) || 0,
|
|
cameraProgress: clamp01(Number(sceneSeek.cameraProgress) || 0),
|
|
holdProgress: clamp01(Number(sceneSeek.holdProgress) || 0),
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
if (runtimeControls.evidencePathDuringCamera && cameraTravel) {
|
|
state.params.sceneControls.cameraTravel = { ...cameraTravel };
|
|
}
|
|
}
|
|
state.params.sceneContext = {
|
|
sceneId: scene.id,
|
|
sceneTitle: scene.title,
|
|
sceneDisplayTitle: displaySceneTitle(scene),
|
|
shotId: shot.id,
|
|
shotTitle: shot.title,
|
|
shotDisplayTitle: displayShotTitle(shot.title, shot, scene),
|
|
shotIndex,
|
|
shotCount: scene.shots.length,
|
|
durationSec: shot.durationSec || DEFAULT_SHOT_DURATION_SEC,
|
|
holdSec: effectiveShotHoldSec(scene, shot, packs, states),
|
|
sceneElapsedSec,
|
|
sceneDurationSec,
|
|
sceneProgress:
|
|
sceneDurationSec > 0 ? sceneElapsedSec / sceneDurationSec : 0,
|
|
...(sceneSeek
|
|
? {
|
|
shotElapsedSec: Number(sceneSeek.shotElapsedSec) || 0,
|
|
shotProgress: clamp01(Number(sceneSeek.shotProgress) || 0),
|
|
seeking: true,
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
for (const layerId of scene?.releaseLayerIds || []) {
|
|
if (!Object.hasOwn(states, layerId)) states[layerId] = { enabled: false };
|
|
}
|
|
return states;
|
|
}
|
|
|
|
/** Resolve recipe map preferences and registered presentation fallbacks. */
|
|
export function visualStateForShot(shot, packs, isMapStackAvailable) {
|
|
const sourceRecipe = packs.recipeForShot(shot);
|
|
const sceneBeatState = Object.values(shot?.layers || {}).find(
|
|
(state) => state?.params?.presentation === 'scene-beat',
|
|
);
|
|
const controls = {
|
|
...sceneBeatState?.params?.sceneControls,
|
|
...sourceRecipe?.runtimeControlsByBeat?.[sceneBeatState?.params?.beatId],
|
|
};
|
|
if (controls.imageryComparison === true) {
|
|
return { ...(shot.visual || {}), mapStack: 'esri-imagery' };
|
|
}
|
|
if (
|
|
sourceRecipe?.standaloneSurfaceBeatIds?.includes(
|
|
sceneBeatState?.params?.beatId,
|
|
)
|
|
) {
|
|
return { ...(shot.visual || {}), mapStack: 'esri-imagery' };
|
|
}
|
|
const visual = sourceRecipe?.photorealSurfaceBeatIds?.includes(
|
|
sceneBeatState?.params?.beatId,
|
|
)
|
|
? { ...(shot.visual || {}), mapStack: 'photoreal' }
|
|
: shot?.visual || {};
|
|
return packs.resolveVisual(shot, visual, isMapStackAvailable);
|
|
}
|
|
|
|
/** Combine authored holds, registered pack overrides and layer reveal requirements. */
|
|
export function effectiveShotHoldSec(
|
|
scene,
|
|
shot,
|
|
packs,
|
|
resolvedStates = null,
|
|
) {
|
|
const states = resolvedStates || layerStatesForShot(scene, shot, packs);
|
|
const recipe = packs.recipeForShot(shot);
|
|
const overrides = Object.values(states)
|
|
.filter(
|
|
(state) => state?.enabled && state.params?.presentation === 'scene-beat',
|
|
)
|
|
.map((state) => recipe?.runtimeHoldSecByBeat?.[state.params.beatId])
|
|
.filter((value) => Number.isFinite(value) && value >= 0);
|
|
const authoredHoldSec = overrides.length
|
|
? Math.max(...overrides)
|
|
: Number(shot?.holdSec) || 0;
|
|
return Object.values(states).reduce(
|
|
(holdSec, state) => {
|
|
const controls = state?.params?.sceneControls;
|
|
return Math.max(
|
|
holdSec,
|
|
Number(controls?.minimumHoldSec) || 0,
|
|
controls?.evidenceSequence === true
|
|
? Number(controls.evidenceSequenceDurationSec) || 0
|
|
: 0,
|
|
);
|
|
},
|
|
Math.max(packs.minimumHoldSec(states), authoredHoldSec),
|
|
);
|
|
}
|
|
|
|
/** Estimate flight plus effective hold without mutating saved layer parameters. */
|
|
export function shotRuntimeDurationSec(scene, shot, packs) {
|
|
const states = deepClone(shot?.layers || {});
|
|
const sourceRecipe = packs.recipeForShot(shot);
|
|
for (const state of Object.values(states)) {
|
|
if (state?.params?.presentation !== 'scene-beat') continue;
|
|
const runtimeControls =
|
|
sourceRecipe?.runtimeControlsByBeat?.[state.params.beatId];
|
|
if (runtimeControls) {
|
|
state.params.sceneControls = {
|
|
...state.params.sceneControls,
|
|
...runtimeControls,
|
|
};
|
|
}
|
|
}
|
|
return (
|
|
(shot?.durationSec || DEFAULT_SHOT_DURATION_SEC) +
|
|
effectiveShotHoldSec(scene, shot, packs, states)
|
|
);
|
|
}
|