feat(scenes): rich scene descriptions and control viewer window
Add TipTap editing in the editor and an Electron window on the control panel to read the current scene description. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,9 +3,14 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're
|
||||
import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
|
||||
import { ipcChannels } from '../../shared/ipc/contracts';
|
||||
import type { SessionState } from '../../shared/ipc/contracts';
|
||||
import { isNodeInMainStoryline, isNodeInSideStoryline, listSideStoryStarts } from '../../shared/graph/sceneGraphLineage';
|
||||
import {
|
||||
isNodeInMainStoryline,
|
||||
isNodeInSideStoryline,
|
||||
listSideStoryStarts,
|
||||
} from '../../shared/graph/sceneGraphLineage';
|
||||
import type { GraphNodeId, Scene, SceneId } from '../../shared/types';
|
||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||
import { isSceneDescriptionEmpty } from '../editor/sceneDescriptionHtml';
|
||||
import { getDndApi } from '../shared/dndApi';
|
||||
import { RotatedImage } from '../shared/RotatedImage';
|
||||
import { PixiEffectsOverlay } from '../shared/effects/PxiEffectsOverlay';
|
||||
@@ -48,15 +53,7 @@ function playLightningEffectSound(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function SideStoryTile({
|
||||
scene,
|
||||
title,
|
||||
onClick,
|
||||
}: {
|
||||
scene: Scene;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
function SideStoryTile({ scene, title, onClick }: { scene: Scene; title: string; onClick: () => void }) {
|
||||
const thumbUrl = useAssetUrl(scene.previewThumbAssetId ?? scene.previewAssetId);
|
||||
const previewUrl = useAssetUrl(scene.previewAssetId);
|
||||
const imageUrl = thumbUrl ?? (scene.previewAssetType === 'image' ? previewUrl : null);
|
||||
@@ -213,6 +210,8 @@ export function ControlApp() {
|
||||
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
|
||||
const isVideoPreviewScene = currentScene?.previewAssetType === 'video';
|
||||
const isDarkenScene = Boolean(currentScene?.darkenScene) && !isVideoPreviewScene;
|
||||
const sceneDescription = currentScene?.description ?? '';
|
||||
const hasSceneDescription = !isSceneDescriptionEmpty(sceneDescription);
|
||||
const sceneAudioRefs = useMemo(() => currentScene?.media.audios ?? [], [currentScene]);
|
||||
// Keep this memo as narrow as possible: project changes on scene switch,
|
||||
// but campaign audio list/config often does not.
|
||||
@@ -1149,6 +1148,27 @@ export function ControlApp() {
|
||||
<Surface className={styles.remote}>
|
||||
<div className={styles.remoteTitle}>{t('control.remoteTitle')}</div>
|
||||
<div className={styles.spacer12} />
|
||||
<div className={styles.sectionLabel}>{t('control.instruments')}</div>
|
||||
<div className={styles.spacer8} />
|
||||
<div className={styles.iconRow}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
disabled={!hasSceneDescription}
|
||||
title={hasSceneDescription ? t('control.descriptionTool') : t('control.descriptionMissing')}
|
||||
ariaLabel={hasSceneDescription ? t('control.descriptionTool') : t('control.descriptionMissing')}
|
||||
onClick={() => {
|
||||
void api.invoke(ipcChannels.windows.openSceneDescription, { html: sceneDescription }).catch((err) => {
|
||||
console.error('[control] openSceneDescription failed', err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className={styles.iconGlyph} aria-hidden>
|
||||
📖
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.spacer12} />
|
||||
{!isVideoPreviewScene ? (
|
||||
<>
|
||||
<div className={styles.sectionLabel}>{t('control.effects')}</div>
|
||||
|
||||
Reference in New Issue
Block a user