feat(scenes): add scene darkness reveal and fix project reopen crash
Add darkenScene with Opening brush in presentation, persist reveal strokes per scene during a session, and close projects properly on return to home. Harden dnd asset streaming to avoid Windows main-process crashes when reopening projects. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,9 @@ import type { GraphNodeId, Scene, SceneId } from '../../shared/types';
|
||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||
import { getDndApi } from '../shared/dndApi';
|
||||
import { PixiEffectsOverlay } from '../shared/effects/PxiEffectsOverlay';
|
||||
import { SceneDarknessOverlay } from '../shared/effects/SceneDarknessOverlay';
|
||||
import { useEffectsState } from '../shared/effects/useEffectsState';
|
||||
import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState';
|
||||
import { Button } from '../shared/ui/controls';
|
||||
import { Surface } from '../shared/ui/Surface';
|
||||
|
||||
@@ -49,6 +51,7 @@ export function ControlApp() {
|
||||
const tRef = useRef(t);
|
||||
tRef.current = t;
|
||||
const [fxState, fx] = useEffectsState();
|
||||
const [sdState, sd] = useSceneDarknessState();
|
||||
const [session, setSession] = useState<SessionState | null>(null);
|
||||
const historyRef = useRef<GraphNodeId[]>([]);
|
||||
const [history, setHistory] = useState<GraphNodeId[]>([]);
|
||||
@@ -69,7 +72,18 @@ export function ControlApp() {
|
||||
const previewHostRef = useRef<HTMLDivElement | null>(null);
|
||||
const previewVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const brushRef = useRef<{
|
||||
tool: 'fog' | 'fire' | 'rain' | 'water' | 'darkness' | 'lightning' | 'sunbeam' | 'poisonCloud' | 'freeze' | 'eraser';
|
||||
tool:
|
||||
| 'fog'
|
||||
| 'fire'
|
||||
| 'rain'
|
||||
| 'water'
|
||||
| 'darkness'
|
||||
| 'lightning'
|
||||
| 'sunbeam'
|
||||
| 'poisonCloud'
|
||||
| 'freeze'
|
||||
| 'exploreBrush'
|
||||
| 'eraser';
|
||||
startN?: { x: number; y: number };
|
||||
points?: { x: number; y: number; tMs: number }[];
|
||||
} | null>(null);
|
||||
@@ -145,6 +159,7 @@ export function ControlApp() {
|
||||
const currentScene =
|
||||
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
|
||||
const isVideoPreviewScene = currentScene?.previewAssetType === 'video';
|
||||
const isDarkenScene = Boolean(currentScene?.darkenScene) && !isVideoPreviewScene;
|
||||
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.
|
||||
@@ -609,6 +624,13 @@ export function ControlApp() {
|
||||
draftPaintRafRef.current = requestAnimationFrame(() => {
|
||||
draftPaintRafRef.current = 0;
|
||||
setDraftFxTick((x) => x + 1);
|
||||
const b = brushRef.current;
|
||||
if (b?.tool === 'exploreBrush' && b.points) {
|
||||
void sd.dispatch({
|
||||
kind: 'draft.set',
|
||||
draft: { points: b.points, radiusN: toolRef.current.radiusN },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -711,6 +733,18 @@ export function ControlApp() {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (b.tool === 'exploreBrush' && b.points && b.points.length > 0) {
|
||||
await sd.dispatch({
|
||||
kind: 'stroke.add',
|
||||
stroke: {
|
||||
id: `sd_${String(createdAtMs)}_${String(seed)}`,
|
||||
seed,
|
||||
createdAtMs,
|
||||
points: b.points,
|
||||
radiusN: tool.radiusN,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (b.tool === 'darkness' && b.points && b.points.length > 0) {
|
||||
const last = b.points[b.points.length - 1];
|
||||
if (last === undefined) return;
|
||||
@@ -1137,6 +1171,24 @@ export function ControlApp() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isDarkenScene ? (
|
||||
<div className={styles.effectsGroup}>
|
||||
<div className={styles.subsectionLabel}>{t('control.darknessControl')}</div>
|
||||
<div className={styles.iconRow}>
|
||||
<Button
|
||||
variant={tool.tool === 'exploreBrush' ? 'primary' : 'ghost'}
|
||||
iconOnly
|
||||
title={t('control.explorerBrush')}
|
||||
ariaLabel={t('control.explorerBrush')}
|
||||
onClick={() =>
|
||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'exploreBrush' } })
|
||||
}
|
||||
>
|
||||
<span className={styles.iconGlyph}>🔦</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.radiusRow}>
|
||||
<div className={styles.radiusLabel}>{t('control.brushRadius')}</div>
|
||||
<input
|
||||
@@ -1235,6 +1287,13 @@ export function ControlApp() {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{previewContentRect ? (
|
||||
<SceneDarknessOverlay
|
||||
state={sdState}
|
||||
overlayAlpha={0.5}
|
||||
viewport={previewContentRect}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
ref={brushCursorElRef}
|
||||
className={styles.brushCursor}
|
||||
@@ -1269,6 +1328,15 @@ export function ControlApp() {
|
||||
startN: p,
|
||||
points: [{ x: p.x, y: p.y, tMs: Date.now() }],
|
||||
};
|
||||
if (tool.tool === 'exploreBrush') {
|
||||
void sd.dispatch({
|
||||
kind: 'draft.set',
|
||||
draft: {
|
||||
points: [{ x: p.x, y: p.y, tMs: Date.now() }],
|
||||
radiusN: tool.radiusN,
|
||||
},
|
||||
});
|
||||
}
|
||||
setDraftFxTick((x) => x + 1);
|
||||
}}
|
||||
onPointerMove={(e) => {
|
||||
@@ -1296,6 +1364,9 @@ export function ControlApp() {
|
||||
void commitStroke();
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
if (brushRef.current?.tool === 'exploreBrush') {
|
||||
void sd.dispatch({ kind: 'draft.set', draft: null });
|
||||
}
|
||||
brushRef.current = null;
|
||||
setDraftFxTick((x) => x + 1);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user