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
+21
View File
@@ -623,6 +623,7 @@ export function EditorApp() {
previewAssetType={sc?.previewAssetType ?? null}
previewVideoAutostart={sc?.previewVideoAutostart ?? false}
previewRotationDeg={sc?.previewRotationDeg ?? 0}
darkenScene={sc?.darkenScene ?? false}
previewBusy={previewBusy}
mediaAssets={sceneMediaAssets}
audioRefs={sceneAudioRefs}
@@ -632,6 +633,9 @@ export function EditorApp() {
onPreviewVideoAutostartChange={(next) =>
void actions.updateScene(sid, { previewVideoAutostart: next })
}
onDarkenSceneChange={(next) =>
void actions.updateScene(sid, { darkenScene: next })
}
onTitleChange={(title) => void actions.updateScene(sid, { title })}
onDescriptionChange={(description) =>
void actions.updateScene(sid, { description })
@@ -1681,11 +1685,13 @@ type SceneInspectorProps = {
previewAssetType: 'image' | 'video' | null;
previewVideoAutostart: boolean;
previewRotationDeg: 0 | 90 | 180 | 270;
darkenScene: boolean;
previewBusy: boolean;
mediaAssets: MediaAsset[];
audioRefs: SceneAudioRef[];
onAudioRefsChange: (next: SceneAudioRef[]) => void;
onPreviewVideoAutostartChange: (next: boolean) => void;
onDarkenSceneChange: (next: boolean) => void;
onTitleChange: (v: string) => void;
onDescriptionChange: (v: string) => void;
onImportPreview: () => void;
@@ -1788,11 +1794,13 @@ function SceneInspector({
previewAssetType,
previewVideoAutostart,
previewRotationDeg,
darkenScene,
previewBusy,
mediaAssets,
audioRefs,
onAudioRefsChange,
onPreviewVideoAutostartChange,
onDarkenSceneChange,
onTitleChange,
onDescriptionChange,
onImportPreview,
@@ -1877,6 +1885,19 @@ function SceneInspector({
</Button>
) : null}
</div>
{previewAssetId && previewAssetType === 'image' ? (
<>
<div className={styles.spacer6} />
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={darkenScene}
onChange={(e) => onDarkenSceneChange(e.target.checked)}
/>
<span className={styles.spanSm}>{t('scene.darkenScene')}</span>
</label>
</>
) : null}
<div className={styles.spacer6} />
<div className={styles.labelSm}>{t('scene.audio')}</div>
<div className={styles.audioDrop}>
@@ -281,6 +281,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'scene.change': 'Изменить',
'scene.clear': 'Очистить',
'scene.autostart': 'Автостарт',
'scene.darkenScene': 'Затемнить сцену',
'scene.rotate': 'Повернуть',
'scene.audio': 'АУДИО СЦЕНЫ',
'scene.removeTitle': 'Убрать из сцены',
@@ -319,6 +320,8 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'control.fire': 'Огонь',
'control.water': 'Вода',
'control.darkness': 'Тьма',
'control.darknessControl': 'Управление затемнением',
'control.explorerBrush': 'Кисть Открытия',
'control.lightning': 'Молния',
'control.sunbeam': 'Луч света',
'control.freeze': 'Заморозка',
@@ -601,6 +604,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'scene.change': 'Change',
'scene.clear': 'Clear',
'scene.autostart': 'Autostart',
'scene.darkenScene': 'Darken scene',
'scene.rotate': 'Rotate',
'scene.audio': 'SCENE AUDIO',
'scene.removeTitle': 'Remove from scene',
@@ -639,6 +643,8 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'control.fire': 'Fire',
'control.water': 'Water',
'control.darkness': 'Darkness',
'control.darknessControl': 'Darkness control',
'control.explorerBrush': 'Opening brush',
'control.lightning': 'Lightning',
'control.sunbeam': 'Sunbeam',
'control.freeze': 'Freeze',
@@ -32,6 +32,7 @@ type Actions = {
previewAssetType?: 'image' | 'video' | null;
previewVideoAutostart?: boolean;
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;
layout?: { x: number; y: number };
@@ -140,6 +141,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
};
const closeProject = async () => {
await api.invoke(ipcChannels.project.close, {});
setState((s) => ({ ...s, project: null, selectedSceneId: null }));
await refreshProjects();
};
@@ -157,6 +159,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
previewAssetType: null,
previewVideoAutostart: false,
previewRotationDeg: 0,
darkenScene: false,
media: { videos: [], audios: [] },
settings: { autoplayVideo: false, autoplayAudio: true, loopVideo: true, loopAudio: true },
connections: [],
@@ -212,6 +215,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
previewAssetType?: 'image' | 'video' | null;
previewVideoAutostart?: boolean;
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;
layout?: { x: number; y: number };
@@ -237,6 +241,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
...(patch.previewRotationDeg !== undefined
? { previewRotationDeg: patch.previewRotationDeg }
: null),
...(patch.darkenScene !== undefined ? { darkenScene: patch.darkenScene } : null),
...(patch.settings ? { settings: { ...scene.settings, ...patch.settings } } : null),
...(patch.media ? { media: { ...scene.media, ...patch.media } } : null),
layout: patch.layout ? { ...scene.layout, ...patch.layout } : scene.layout,