Files
gods-eye-view/src/scenes/shotPresentation.js
T
German KhokhlovandClaude Opus 5.5 00704ddc89 i18n: fix review findings and polish ru/es strings
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]>
2026-10-15 23:49:52 +04:00

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)
);
}