fix: game audio persistence and editor perf
- Keep game/campaign audio assets referenced (no prune) - Flush pending project save on quit/switch/export to avoid losing campaignAudios - Control: prevent game music restarts on scene changes; allow always-on controls; handle autoplay-after-scene-audio - Editor: reduce ReactFlow churn with stable scene card map; lazy/async image decode - Add contract/unit tests and update test script Made-with: Cursor
This commit is contained in:
@@ -19,19 +19,29 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { isSceneGraphEdgeRejected } from '../../../shared/graph/sceneGraphEdgeRules';
|
||||
import type {
|
||||
AssetId,
|
||||
GraphNodeId,
|
||||
Scene,
|
||||
SceneGraphEdge,
|
||||
SceneGraphNode,
|
||||
SceneId,
|
||||
} from '../../../shared/types';
|
||||
import type { AssetId, GraphNodeId, SceneGraphEdge, SceneGraphNode, SceneId } from '../../../shared/types';
|
||||
import { RotatedImage } from '../../shared/RotatedImage';
|
||||
import { useAssetUrl } from '../../shared/useAssetImageUrl';
|
||||
|
||||
import styles from './SceneGraph.module.css';
|
||||
|
||||
/** Поля сцены, нужные только для карточки узла графа (без описания и прочего). */
|
||||
export type SceneGraphSceneAudioSummary = {
|
||||
assetId: AssetId;
|
||||
loop: boolean;
|
||||
autoplay: boolean;
|
||||
};
|
||||
|
||||
export type SceneGraphSceneCard = {
|
||||
title: string;
|
||||
previewAssetId: AssetId | null;
|
||||
previewAssetType: 'image' | 'video' | null;
|
||||
previewVideoAutostart: boolean;
|
||||
previewRotationDeg: 0 | 90 | 180 | 270;
|
||||
loopVideo: boolean;
|
||||
audios: readonly SceneGraphSceneAudioSummary[];
|
||||
};
|
||||
|
||||
/** MIME для перетаскивания сцены из списка на граф (см. EditorApp). */
|
||||
export const DND_SCENE_ID_MIME = 'application/x-dnd-scene-id';
|
||||
|
||||
@@ -42,7 +52,7 @@ const SCENE_CARD_H = 248;
|
||||
export type SceneGraphProps = {
|
||||
sceneGraphNodes: SceneGraphNode[];
|
||||
sceneGraphEdges: SceneGraphEdge[];
|
||||
sceneById: Record<SceneId, Scene>;
|
||||
sceneCardById: Record<SceneId, SceneGraphSceneCard>;
|
||||
currentSceneId: SceneId | null;
|
||||
onCurrentSceneChange: (id: SceneId) => void;
|
||||
onConnect: (sourceGraphNodeId: GraphNodeId, targetGraphNodeId: GraphNodeId) => void;
|
||||
@@ -132,12 +142,21 @@ function SceneCardNode({ data }: NodeProps<SceneCardData>) {
|
||||
{url && data.previewAssetType === 'image' ? (
|
||||
<div className={styles.previewFill}>
|
||||
{data.previewRotationDeg === 0 ? (
|
||||
<img src={url} alt="" className={styles.imageCover} draggable={false} />
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
className={styles.imageCover}
|
||||
draggable={false}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<RotatedImage
|
||||
url={url}
|
||||
rotationDeg={data.previewRotationDeg}
|
||||
mode="cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
)}
|
||||
@@ -261,7 +280,7 @@ function GraphZoomToolbar() {
|
||||
function SceneGraphCanvas({
|
||||
sceneGraphNodes,
|
||||
sceneGraphEdges,
|
||||
sceneById,
|
||||
sceneCardById,
|
||||
currentSceneId,
|
||||
onCurrentSceneChange,
|
||||
onConnect,
|
||||
@@ -291,33 +310,33 @@ function SceneGraphCanvas({
|
||||
|
||||
const desiredNodes = useMemo<Node<SceneCardData>[]>(() => {
|
||||
return sceneGraphNodes.map((gn) => {
|
||||
const s = sceneById[gn.sceneId];
|
||||
const c = sceneCardById[gn.sceneId];
|
||||
const active = gn.sceneId === currentSceneId;
|
||||
const audios = s?.media.audios ?? [];
|
||||
const audios = c?.audios ?? [];
|
||||
return {
|
||||
id: gn.id,
|
||||
type: 'sceneCard',
|
||||
position: { x: gn.x, y: gn.y },
|
||||
data: {
|
||||
sceneId: gn.sceneId,
|
||||
title: s?.title ?? '',
|
||||
title: c?.title ?? '',
|
||||
active,
|
||||
previewAssetId: s?.previewAssetId ?? null,
|
||||
previewAssetType: s?.previewAssetType ?? null,
|
||||
previewVideoAutostart: s?.previewVideoAutostart ?? false,
|
||||
previewRotationDeg: s?.previewRotationDeg ?? 0,
|
||||
previewAssetId: c?.previewAssetId ?? null,
|
||||
previewAssetType: c?.previewAssetType ?? null,
|
||||
previewVideoAutostart: c?.previewVideoAutostart ?? false,
|
||||
previewRotationDeg: c?.previewRotationDeg ?? 0,
|
||||
isStartScene: gn.isStartScene,
|
||||
hasSceneAudio: audios.length >= 1,
|
||||
previewIsVideo: s?.previewAssetType === 'video',
|
||||
previewIsVideo: c?.previewAssetType === 'video',
|
||||
hasAnyAudioLoop: audios.some((a) => a.loop),
|
||||
hasAnyAudioAutoplay: audios.some((a) => a.autoplay),
|
||||
showPreviewVideoAutostart: s?.previewAssetType === 'video' ? s.previewVideoAutostart : false,
|
||||
showPreviewVideoLoop: s?.previewAssetType === 'video' ? s.settings.loopVideo : false,
|
||||
showPreviewVideoAutostart: c?.previewAssetType === 'video' ? c.previewVideoAutostart : false,
|
||||
showPreviewVideoLoop: c?.previewAssetType === 'video' ? c.loopVideo : false,
|
||||
},
|
||||
style: { padding: 0, background: 'transparent', border: 'none' },
|
||||
};
|
||||
});
|
||||
}, [currentSceneId, sceneById, sceneGraphNodes]);
|
||||
}, [currentSceneId, sceneCardById, sceneGraphNodes]);
|
||||
|
||||
const desiredEdges = useMemo<Edge[]>(() => {
|
||||
return sceneGraphEdges.map((e) => ({
|
||||
|
||||
Reference in New Issue
Block a user