From 2dd1c3000180f55bde1b4f73ad8500cc9799a454 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 04:36:34 +0200 Subject: [PATCH] Add scene transition and exit handling logic in App.tsx --- src/app/App.tsx | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/app/App.tsx b/src/app/App.tsx index 6681775b..9d78ddfd 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3454,6 +3454,137 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; + const resolveDefaultSceneExitDestination = ( + preferredSceneId = editorState.activeSceneId + ): { + targetSceneId: string; + targetEntryEntityId: string; + } | null => { + const preferredOrder = [ + ...sceneTargetOptions + .map(({ id }) => id) + .filter((sceneId) => sceneId !== preferredSceneId), + preferredSceneId + ]; + + for (const sceneId of preferredOrder) { + const firstSceneEntry = sceneEntryOptionsBySceneId[sceneId]?.[0]?.entity; + + if (firstSceneEntry !== undefined) { + return { + targetSceneId: sceneId, + targetEntryEntityId: firstSceneEntry.id + }; + } + } + + return null; + }; + + const buildRuntimeSceneForProjectScene = ( + sceneId: string, + options: { sceneEntryId?: string | null } = {} + ): RuntimeSceneDefinition => { + const sceneDocument = createSceneDocumentFromProject( + editorState.projectDocument, + sceneId + ); + + return buildRuntimeSceneFromDocument(sceneDocument, { + navigationMode: preferredNavigationMode, + loadedModelAssets, + sceneEntryId: options.sceneEntryId + }); + }; + + const applyRuntimeSceneSession = ( + sceneId: string, + nextRuntimeScene: RuntimeSceneDefinition + ) => { + const projectScene = editorState.projectDocument.scenes[sceneId]; + + if (projectScene === undefined) { + throw new Error(`Project scene ${sceneId} does not exist.`); + } + + setRuntimeScene(nextRuntimeScene); + setRuntimeSceneId(projectScene.id); + setRuntimeSceneName(projectScene.name); + setRuntimeSceneLoadingScreen( + cloneSceneLoadingScreenSettings(projectScene.loadingScreen) + ); + }; + + const handleRunnerSceneExitActivated = ( + request: RuntimeSceneExitTransitionRequest + ) => { + if (runtimeSceneId === null || runtimeSceneName === null) { + setRuntimeMessage("Scene transition failed: run mode is not active."); + return; + } + + const sourceScene = editorState.projectDocument.scenes[runtimeSceneId]; + const targetScene = editorState.projectDocument.scenes[request.targetSceneId]; + + if (sourceScene === undefined) { + setRuntimeMessage( + `Scene transition failed: source scene ${runtimeSceneId} is no longer available.` + ); + return; + } + + if (targetScene === undefined) { + setRuntimeMessage( + `Scene transition failed: target scene ${request.targetSceneId} does not exist.` + ); + return; + } + + const targetEntry = targetScene.entities[request.targetEntryEntityId]; + + if (targetEntry === undefined) { + setRuntimeMessage( + `Scene transition failed: target Scene Entry ${request.targetEntryEntityId} does not exist in ${targetScene.name}.` + ); + return; + } + + if (targetEntry.kind !== "sceneEntry") { + setRuntimeMessage( + `Scene transition failed: target ${request.targetEntryEntityId} in ${targetScene.name} is not a Scene Entry.` + ); + return; + } + + try { + const nextRuntimeScene = buildRuntimeSceneForProjectScene(targetScene.id, { + sceneEntryId: targetEntry.id + }); + + applyRuntimeSceneSession(targetScene.id, nextRuntimeScene); + setRuntimeMessage(null); + setFirstPersonTelemetry(null); + setRuntimeInteractionPrompt(null); + setRuntimeGlobalState((currentState) => ({ + ...currentState, + transitionCount: currentState.transitionCount + 1, + lastSceneTransition: { + fromSceneId: sourceScene.id, + fromSceneName: sourceScene.name, + toSceneId: targetScene.id, + toSceneName: targetScene.name, + viaExitEntityId: request.sourceExitEntityId, + targetEntryEntityId: targetEntry.id + } + })); + setStatusMessage(`Loaded ${targetScene.name}.`); + } catch (error) { + const message = getErrorMessage(error); + setRuntimeMessage(`Scene transition failed: ${message}`); + setStatusMessage(`Scene transition failed: ${message}`); + } + }; + const handleCommitCreation = ( creationPreview: CreationViewportToolPreview ): boolean => { @@ -3555,6 +3686,17 @@ export function App({ store, initialStatusMessage }: AppProps) { ); completeCreation("Placed Player Start."); return true; + case "sceneEntry": + store.executeCommand( + createUpsertEntityCommand({ + entity: createSceneEntryEntity({ + position + }), + label: "Place scene entry" + }) + ); + completeCreation("Placed Scene Entry."); + return true; case "soundEmitter": { const placedAudioAssetId = creationPreview.target.audioAssetId ?? @@ -3610,6 +3752,29 @@ export function App({ store, initialStatusMessage }: AppProps) { ); completeCreation("Placed Interactable."); return true; + case "sceneExit": { + const destination = resolveDefaultSceneExitDestination(); + + if (destination === null) { + setStatusMessage( + "Author a Scene Entry before placing a Scene Exit." + ); + return false; + } + + store.executeCommand( + createUpsertEntityCommand({ + entity: createSceneExitEntity({ + position, + targetSceneId: destination.targetSceneId, + targetEntryEntityId: destination.targetEntryEntityId + }), + label: "Place scene exit" + }) + ); + completeCreation("Placed Scene Exit."); + return true; + } } } catch (error) { setStatusMessage(getErrorMessage(error));