From 5a80facf8d4254079bc7601bd299a8f0ad325897 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Thu, 29 Oct 2026 18:39:53 +0000 Subject: [PATCH] Address review: playable PLAY/NEXT, visible rename editor, accurate copy - PLAY and NEXT continue into the following scenes, so they stay enabled while any scene in the project has a shot, not only the selected one. - Renaming a shot drops the number and ellipsis so the editor is no longer hidden; qa-scene-controls checks a long rename stays visible. - PLAY, NEXT, EDIT DETAILS and SHARE SCENE tooltips describe what the buttons do, translated in every shipped language. - The primary actions and the file row keep a hover state. - Docs: the EXPORT rename in DIRECTOR-SHARING and the playback button states in CURRENT-STATE are accurate. - Test the voice "authoring" alias through set_panel_open. --- docs/CURRENT-STATE.md | 14 +++++++------ docs/DIRECTOR-SHARING.md | 2 +- scripts/qa-scene-controls.mjs | 33 ++++++++++++++++++++++++++++++ src/i18n/locales/es.json | 8 ++++---- src/i18n/locales/ru.json | 8 ++++---- src/i18n/locales/zh-CN.json | 8 ++++---- src/i18n/locales/zh-Hant.json | 8 ++++---- src/ui/sceneControls.js | 6 +++++- src/ui/sceneControls.test.mjs | 9 ++++++++ src/ui/scenePresentation.js | 14 +++++++------ src/ui/sceneSharing.js | 8 ++++++-- src/ui/styles/scenes.css | 18 ++++++++++++++++ src/ui/templates/layer-panels.html | 4 ++-- src/voice/gevActions.test.mjs | 23 +++++++++++++++++++++ 14 files changed, 129 insertions(+), 34 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 42865a09d..330249ec9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1176,12 +1176,14 @@ The panel is titled Authoring and is laid out in four labelled steps: SCENE (picker on its own row, NEW SCENE, DELETE), SHOTS (+ CAPTURE SHOT, UPDATE SHOT, the numbered shot list and a select/rename hint), PREVIEW (PLAY, STOP, NEXT) and SAVE & SHARE (EXPORT, IMPORT, RUN LOG, then the EDIT DETAILS / SHARE SCENE row). -Every button in the panel's step rows has a tooltip, and the status line is a -polite live region. Outside -playback a button that would do nothing is disabled: DELETE and CAPTURE SHOT -without a scene, UPDATE SHOT without a selected shot, PLAY and NEXT on a scene -with no shots. During playback everything but STOP stays disabled as before. -PLAY uses the `scene|PLAY` catalog entry, apart from the radio's PLAY. +Every button in the step rows has a tooltip, and the status line is a polite +live region. Outside playback a button that would do nothing is disabled: +DELETE and CAPTURE SHOT without a scene, UPDATE SHOT without a selected shot, +PLAY and NEXT when no scene in the project has a shot (both continue into the +following scenes). During playback the authoring and file actions stay disabled +and STOP is enabled, as before; RUN LOG follows the last run, and EDIT DETAILS / +SHARE SCENE keep their own checks. PLAY uses the `scene|PLAY` catalog entry, +apart from the radio's PLAY. Director clock listeners, interaction observers and scene release failures no longer leave the director inconsistent, and a phase error is kept when cleanup diff --git a/docs/DIRECTOR-SHARING.md b/docs/DIRECTOR-SHARING.md index 4e3c5a85e..84e9d514a 100644 --- a/docs/DIRECTOR-SHARING.md +++ b/docs/DIRECTOR-SHARING.md @@ -42,7 +42,7 @@ field definitions. This is a basic draft editor, not a node graph or path editor SHARE SCENE exports only the selected scene. Download scene JSON preserves its camera, timing, layers, anchors, manifests, interactions and attribution, but contains no asset bytes. The recipient needs the referenced source registrations -and files. Existing EXPORT PRESETS continues to export the entire project. +and files. EXPORT (formerly EXPORT PRESETS) continues to export the entire project. For a bundle, select the declared pack files or a folder, then Download asset bundle. Folder paths match a manifest path either directly or after removing the diff --git a/scripts/qa-scene-controls.mjs b/scripts/qa-scene-controls.mjs index ab92c128d..223202221 100644 --- a/scripts/qa-scene-controls.mjs +++ b/scripts/qa-scene-controls.mjs @@ -106,6 +106,39 @@ try { ), ), ); + // A long rename keeps its editor visible inside the shot row. + await page.click('.scene-shot-label', { count: 2 }); + await page.waitForSelector('.scene-shot-rename', { visible: true }); + await page.type( + '.scene-shot-rename', + ' with a much longer title that runs well past the shot row', + ); + await page.keyboard.press('End'); + check( + 'A long rename keeps the editor visible inside its shot row', + await page.evaluate(() => { + // The label clips with an ellipsis, which hides an editor that + // overflows it, so its content must fit its width. + const input = document.querySelector('.scene-shot-rename'); + const label = input?.closest('.scene-shot-label'); + const box = input?.getBoundingClientRect(); + const style = input && getComputedStyle(input); + return ( + !!box && + box.width > 120 && + box.height > 10 && + label.scrollWidth <= label.clientWidth + 1 && + style.visibility === 'visible' && + style.borderTopWidth !== '0px' && + document.activeElement === input + ); + }), + ); + await page.keyboard.press('Escape'); + await page.waitForFunction( + () => !document.querySelector('.scene-shot-rename'), + ); + await page.click('#scene-capture-btn'); await page.waitForFunction( () => document.querySelectorAll('.scene-shot-row').length === 2, diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index e9ec5a1c6..759db7117 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -3013,13 +3013,13 @@ "Replace the selected shot with the current view": "Sustituye el plano seleccionado por la vista actual", "Click a shot to select it, double-click to rename.": "Haz clic en un plano para seleccionarlo y doble clic para cambiarle el nombre.", "scene|PLAY": "REPROD.", - "Play the whole scene from its first shot": "Reproduce toda la escena desde su primer plano", "Stop playback (Esc)": "Detén la reproducción (Esc)", - "Play only the shot after the selected one": "Reproduce solo el plano posterior al seleccionado", "SAVE & SHARE": "GUARDAR Y COMPARTIR", "Download every scene as a JSON file you can import later": "Descarga todas las escenas como un archivo JSON que puedes importar más tarde", "Load scenes from an exported JSON file": "Carga escenas desde un archivo JSON exportado", "Download the timing log of the last play": "Descarga el registro de tiempos de la última reproducción", - "Edit the scene title and description": "Edita el título y la descripción de la escena", - "Share this scene as a link or file": "Comparte esta escena como enlace o archivo" + "Play from this scene through the rest of the project": "Reproduce desde esta escena hasta el resto del proyecto", + "Play the shot after the selected one, moving on to the next scene when needed": "Reproduce el plano posterior al seleccionado y pasa a la siguiente escena si hace falta", + "Edit anchors, data packs and shot details": "Edita anclas, paquetes de datos y detalles de los planos", + "Download this scene as JSON or an asset bundle": "Descarga esta escena como JSON o como paquete de recursos" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 8d0ed1f24..c65265d5e 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -3082,13 +3082,13 @@ "Replace the selected shot with the current view": "Заменить выбранный кадр текущим видом", "Click a shot to select it, double-click to rename.": "Щёлкните кадр, чтобы выбрать его, дважды — чтобы переименовать.", "scene|PLAY": "ПУСК", - "Play the whole scene from its first shot": "Воспроизвести всю сцену с первого кадра", "Stop playback (Esc)": "Остановить воспроизведение (Esc)", - "Play only the shot after the selected one": "Воспроизвести только кадр после выбранного", "SAVE & SHARE": "СОХРАНИТЬ И ПОДЕЛИТЬСЯ", "Download every scene as a JSON file you can import later": "Скачать все сцены файлом JSON для последующего импорта", "Load scenes from an exported JSON file": "Загрузить сцены из экспортированного файла JSON", "Download the timing log of the last play": "Скачать журнал таймингов последнего воспроизведения", - "Edit the scene title and description": "Изменить название и описание сцены", - "Share this scene as a link or file": "Поделиться сценой ссылкой или файлом" + "Play from this scene through the rest of the project": "Воспроизвести с этой сцены и далее по всему проекту", + "Play the shot after the selected one, moving on to the next scene when needed": "Воспроизвести кадр после выбранного, при необходимости перейдя к следующей сцене", + "Edit anchors, data packs and shot details": "Изменить якоря, пакеты данных и параметры кадров", + "Download this scene as JSON or an asset bundle": "Скачать эту сцену в формате JSON или архивом ресурсов" } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 21632d398..63f4b6b05 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2875,13 +2875,13 @@ "Replace the selected shot with the current view": "用当前视角替换所选镜头", "Click a shot to select it, double-click to rename.": "单击镜头即可选中,双击可重命名。", "scene|PLAY": "播放", - "Play the whole scene from its first shot": "从第一个镜头开始播放整个场景", "Stop playback (Esc)": "停止播放(Esc)", - "Play only the shot after the selected one": "仅播放所选镜头的下一个镜头", "SAVE & SHARE": "保存与分享", "Download every scene as a JSON file you can import later": "将所有场景下载为 JSON 文件,以便日后导入", "Load scenes from an exported JSON file": "从导出的 JSON 文件加载场景", "Download the timing log of the last play": "下载上次播放的计时日志", - "Edit the scene title and description": "编辑场景标题和描述", - "Share this scene as a link or file": "以链接或文件形式分享此场景" + "Play from this scene through the rest of the project": "从此场景开始播放项目中的其余场景", + "Play the shot after the selected one, moving on to the next scene when needed": "播放所选镜头的下一个镜头,必要时进入下一个场景", + "Edit anchors, data packs and shot details": "编辑锚点、数据包和镜头详情", + "Download this scene as JSON or an asset bundle": "将此场景下载为 JSON 或资源包" } diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 33316d6ed..d42742659 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -2875,13 +2875,13 @@ "Replace the selected shot with the current view": "用當前視角替換所選鏡頭", "Click a shot to select it, double-click to rename.": "單擊鏡頭即可選中,雙擊可重命名。", "scene|PLAY": "播放", - "Play the whole scene from its first shot": "從第一個鏡頭開始播放整個場景", "Stop playback (Esc)": "停止播放(Esc)", - "Play only the shot after the selected one": "僅播放所選鏡頭的下一個鏡頭", "SAVE & SHARE": "保存與分享", "Download every scene as a JSON file you can import later": "將所有場景下載為 JSON 文件,以便日後導入", "Load scenes from an exported JSON file": "從導出的 JSON 文件加載場景", "Download the timing log of the last play": "下載上次播放的計時日誌", - "Edit the scene title and description": "編輯場景標題和描述", - "Share this scene as a link or file": "以鏈接或文件形式分享此場景" + "Play from this scene through the rest of the project": "從此場景開始播放項目中的其餘場景", + "Play the shot after the selected one, moving on to the next scene when needed": "播放所選鏡頭的下一個鏡頭,必要時進入下一個場景", + "Edit anchors, data packs and shot details": "編輯錨點、數據包和鏡頭詳情", + "Download this scene as JSON or an asset bundle": "將此場景下載為 JSON 或資源包" } diff --git a/src/ui/sceneControls.js b/src/ui/sceneControls.js index 5976c2c05..c7e023f10 100644 --- a/src/ui/sceneControls.js +++ b/src/ui/sceneControls.js @@ -220,7 +220,11 @@ export class SceneControls { const shots = scene?.shots ?? []; presentSceneButtons(this.elements, running, state.hasRun, { hasScene: !!scene, - shotCount: shots.length, + // PLAY and NEXT run on into the following scenes. + playableShots: (state.scenes ?? []).reduce( + (total, item) => total + (item.shots?.length ?? 0), + 0, + ), hasSelectedShot: shots.some((shot) => shot.id === state.selectedShotId), }); this.elements.panel?.classList.toggle('has-shots', shots.length > 0); diff --git a/src/ui/sceneControls.test.mjs b/src/ui/sceneControls.test.mjs index b31a1a681..f53612a66 100644 --- a/src/ui/sceneControls.test.mjs +++ b/src/ui/sceneControls.test.mjs @@ -184,7 +184,16 @@ test('Authoring disables idle actions that would do nothing', () => { assert.equal(f.elements.update.disabled, true); assert.equal(f.elements.start.disabled, false); + // PLAY and NEXT run on into the following scenes, so an empty selected + // scene beside one with shots keeps them available. f.state.scenes[0].shots = []; + f.state.scenes.push({ id: 'scene-b', title: 'B', shots: [{ id: 'b1' }] }); + f.owner.setButtons(false); + assert.equal(f.elements.start.disabled, false); + assert.equal(f.elements.next.disabled, false); + assert.equal(f.elements.update.disabled, true); + + f.state.scenes.pop(); f.owner.setButtons(false); assert.equal(f.elements.start.disabled, true); assert.equal(f.elements.next.disabled, true); diff --git a/src/ui/scenePresentation.js b/src/ui/scenePresentation.js index 4f5711226..e77c832f0 100644 --- a/src/ui/scenePresentation.js +++ b/src/ui/scenePresentation.js @@ -173,22 +173,24 @@ export function presentSceneSelection(element, selectedShotId) { } /** - * Everything but STOP is off while a scene plays. Otherwise a button is off - * when it would do nothing: DELETE and CAPTURE SHOT without a scene, UPDATE - * SHOT without a selected shot, PLAY and NEXT on a scene with no shots. + * While a scene plays, the authoring and file actions here are off and STOP + * is on. Otherwise a button is off when it would do nothing: DELETE and + * CAPTURE SHOT without a scene, UPDATE SHOT without a selected shot, PLAY and + * NEXT when no scene in the project has a shot (both continue into the + * following scenes). */ export function presentSceneButtons( elements, running, hasRun, - { hasScene = true, shotCount = 1, hasSelectedShot = true } = {}, + { hasScene = true, playableShots = 1, hasSelectedShot = true } = {}, ) { const idleOff = { delete: !hasScene, capture: !hasScene, update: !hasSelectedShot, - start: shotCount === 0, - next: shotCount === 0, + start: playableShots === 0, + next: playableShots === 0, }; for (const name of [ 'start', diff --git a/src/ui/sceneSharing.js b/src/ui/sceneSharing.js index 30fbe9732..e8ebf9110 100644 --- a/src/ui/sceneSharing.js +++ b/src/ui/sceneSharing.js @@ -181,8 +181,12 @@ export function mountSceneSharing( bar.dataset.directorAuthoring = ''; bar.className = 'scene-controls'; const entries = [ - [t('EDIT DETAILS'), t('Edit the scene title and description'), edit], - [t('SHARE SCENE'), t('Share this scene as a link or file'), share], + [t('EDIT DETAILS'), t('Edit anchors, data packs and shot details'), edit], + [ + t('SHARE SCENE'), + t('Download this scene as JSON or an asset bundle'), + share, + ], ]; const removers = []; for (const [text, title, fn] of entries) { diff --git a/src/ui/styles/scenes.css b/src/ui/styles/scenes.css index 9893061e1..441bbbf65 100644 --- a/src/ui/styles/scenes.css +++ b/src/ui/styles/scenes.css @@ -223,6 +223,14 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn { background: transparent; } +/* Keep the shared hover on the restyled actions. */ +#scene-panel .scene-btn-primary:hover:not(:disabled), +#scene-panel .scene-file-controls .scene-btn:hover:not(:disabled) { + color: var(--accent); + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 16%, transparent); +} + .scene-shot-hint { margin: 0 0 8px; font-size: 11px; @@ -311,6 +319,16 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn { color: var(--text-primary); } +/* While renaming, the label holds only the editor at full width. */ +.scene-shot-label:has(.scene-shot-rename) { + overflow: visible; + white-space: normal; +} + +.scene-shot-label:has(.scene-shot-rename)::before { + content: none; +} + /* Shots are numbered 01, 02… in playback order. */ .scene-shot-label::before { content: counter(scene-shot, decimal-leading-zero); diff --git a/src/ui/templates/layer-panels.html b/src/ui/templates/layer-panels.html index 02b02d05a..5f5d11b4d 100644 --- a/src/ui/templates/layer-panels.html +++ b/src/ui/templates/layer-panels.html @@ -166,9 +166,9 @@

Click a shot to select it, double-click to rename.

PREVIEW

- + - +

SAVE & SHARE

diff --git a/src/voice/gevActions.test.mjs b/src/voice/gevActions.test.mjs index 4a3a8ec72..6145a998d 100644 --- a/src/voice/gevActions.test.mjs +++ b/src/voice/gevActions.test.mjs @@ -3931,3 +3931,26 @@ test('analyst-only lookup distinguishes duplicate names and uses canonical aircr assert.equal(plane.ok, true); assert.equal(plane.latitude, 36, 'canonical ID resolves the current position, not old coordinates'); }); + +test('set_panel_open accepts the Authoring name for the scene panel', async () => { + const calls = []; + const runner = createGevActionRunner({ + viewer: { + clock: { onTick: { addEventListener: () => () => {} } }, + scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, + camera: { + moveEnd: { addEventListener() {} }, + positionWC: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 1000), + }, + }, + styleManager: { + setPanelCollapsed: (...args) => calls.push(args), + }, + dataManager: { layers: new Map(), isEnabled: () => false, getAll: () => [] }, + }); + for (const panel of ['authoring', 'Authoring', 'scenes']) { + const result = await runner('set_panel_open', { panelId: panel, open: true }); + assert.equal(result.panelId, 'scene-panel'); + } + assert.deepEqual(calls.at(-1), ['scene-panel', false, { explicit: true }]); +});