diff --git a/package.json b/package.json
index 5e4de86..c0ab5f5 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"npm": "^11.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
+ "react-router-dom": "^7.16.0",
"three": "^0.184.0",
"uuid": "^14.0.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 55f7fe8..230f488 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
react-dom:
specifier: ^19.2.6
version: 19.2.6(react@19.2.6)
+ react-router-dom:
+ specifier: ^7.16.0
+ version: 7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
three:
specifier: ^0.184.0
version: 0.184.0
@@ -679,6 +682,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -1180,6 +1187,23 @@ packages:
peerDependencies:
react: ^19.2.6
+ react-router-dom@7.16.0:
+ resolution: {integrity: sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.16.0:
+ resolution: {integrity: sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-use-measure@2.1.7:
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
@@ -1223,6 +1247,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -2023,6 +2050,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.1.1: {}
+
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@@ -2387,6 +2416,20 @@ snapshots:
react: 19.2.6
scheduler: 0.27.0
+ react-router-dom@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ react-router: 7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+
+ react-router@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.6
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.6(react@19.2.6)
+
react-use-measure@2.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
react: 19.2.6
@@ -2434,6 +2477,8 @@ snapshots:
semver@7.8.1: {}
+ set-cookie-parser@2.7.2: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
diff --git a/src/App.tsx b/src/App.tsx
index 497805e..280361d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,12 +1,81 @@
+import { BrowserRouter, Link, Outlet, Route, Routes, useNavigate, useParams } from 'react-router-dom';
+import { useEffect } from 'react';
+import { reaction } from 'mobx';
import { ThreeView } from './components/ThreeView';
-import './App.scss';
import { Toolbar } from './components/Toolbar';
+import { state } from './state';
+import './App.scss';
+
+function StateToUrlSync() {
+ const navigate = useNavigate();
+ useEffect(() => reaction(
+ () => ({
+ isGame: state.isGamePlaying,
+ selectedId: state.worldEditor.selectedObjectId,
+ }),
+ ({ isGame, selectedId }) => {
+ if (isGame) navigate('/game');
+ else if (selectedId) navigate(`/editor/object/${selectedId}`);
+ else navigate('/editor');
+ },
+ ), []);
+ return null;
+}
+
+function EditorRoute() {
+ useEffect(() => {
+ if (state.isGamePlaying) state.stopGame();
+ state.worldEditor.resetSelectedObject();
+ }, []);
+ return null;
+}
+
+function EditorObjectRoute() {
+ const { id } = useParams<{ id: string }>();
+ useEffect(() => {
+ if (!id) return;
+ if (state.isGamePlaying) state.stopGame();
+ const mode = state.worldEditor.selectedObjectId === id
+ ? state.worldEditor.selectedObjectMode ?? 'translate'
+ : 'translate';
+ state.worldEditor.setSelectedObject(id, mode);
+ }, [id]);
+ return null;
+}
+
+function GameRoute() {
+ useEffect(() => {
+ if (!state.isGamePlaying) state.startGame();
+ }, []);
+ return null;
+}
+
+function HomePage() {
+ return Open Editor;
+}
+
+function EditorLayout() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
export const App = function () {
- return (
- <>
-
-
- >
- )
-}
+ return (
+
+
+
+ } />
+ }>
+ } />
+ } />
+ } />
+
+
+
+ );
+};