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:
Ivan Fontosh
2026-07-02 19:41:26 +08:00
parent d54e9ed02d
commit 4631a1bece
19 changed files with 526 additions and 11 deletions
+72 -1
View File
@@ -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);
}}