Merge pull request #6 from La-Fabrik-Durable/feat/docs-routing
Feat/docs-routing
This commit is contained in:
@@ -44,12 +44,12 @@ This document describes the code that exists today in the repository.
|
|||||||
## Editor System
|
## Editor System
|
||||||
|
|
||||||
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
||||||
- `src/features/editor/components/EditorControls.tsx` renders the HTML editor control panel.
|
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
|
||||||
- `src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||||
- `src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
- `src/features/editor/controls/FlyController.tsx` provides player-style editor navigation.
|
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
|
||||||
- `src/features/editor/hooks/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||||
- `src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo state.
|
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
|
||||||
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
||||||
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
||||||
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
||||||
@@ -63,9 +63,9 @@ This document describes the code that exists today in the repository.
|
|||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
||||||
- The repository is still a prototype, not the full intended game runtime.
|
- The repository is a prototype, not the full intended game runtime.
|
||||||
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
- `src/world/debug/TestScene.tsx` is part of the active scene composition.
|
||||||
- There is no central gameplay orchestrator such as `GameManager` yet.
|
- There is no central gameplay orchestrator such as `GameManager`.
|
||||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||||
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
||||||
|
|||||||
+24
-23
@@ -10,8 +10,8 @@ The editor is a React route used to inspect and adjust the `public/map.json` sce
|
|||||||
|
|
||||||
- `/` renders the playable La-Fabrik scene.
|
- `/` renders the playable La-Fabrik scene.
|
||||||
- `/editor` renders the map editor.
|
- `/editor` renders the map editor.
|
||||||
- `src/main.tsx` wraps the app with `BrowserRouter`.
|
- `src/App.tsx` mounts TanStack Router through `RouterProvider`.
|
||||||
- `src/App.tsx` defines the route and imports `EditorPage` from `src/pages/editor/EditorPage.tsx`.
|
- `src/router.tsx` defines the `/editor` route and imports `EditorPage` from `src/pages/editor/page.tsx`.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
@@ -19,19 +19,20 @@ The editor is a React route used to inspect and adjust the `public/map.json` sce
|
|||||||
src/
|
src/
|
||||||
├── pages/
|
├── pages/
|
||||||
│ └── editor/
|
│ └── editor/
|
||||||
│ └── EditorPage.tsx
|
│ └── page.tsx
|
||||||
├── features/
|
├── components/
|
||||||
│ └── editor/
|
│ └── editor/
|
||||||
│ ├── components/
|
│ ├── EditorControls.tsx
|
||||||
│ │ └── EditorControls.tsx
|
│ └── scene/
|
||||||
│ ├── controls/
|
│ ├── EditorMap.tsx
|
||||||
│ │ └── FlyController.tsx
|
│ └── EditorScene.tsx
|
||||||
│ ├── hooks/
|
├── controls/
|
||||||
│ │ ├── useEditorHistory.ts
|
│ └── editor/
|
||||||
│ │ └── useEditorSceneData.ts
|
│ └── FlyController.tsx
|
||||||
│ ├── scene/
|
├── hooks/
|
||||||
│ │ ├── EditorMap.tsx
|
│ └── editor/
|
||||||
│ │ └── EditorScene.tsx
|
│ ├── useEditorHistory.ts
|
||||||
|
│ └── useEditorSceneData.ts
|
||||||
├── types/
|
├── types/
|
||||||
│ └── editor.ts
|
│ └── editor.ts
|
||||||
└── utils/
|
└── utils/
|
||||||
@@ -42,19 +43,19 @@ src/
|
|||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
`src/pages/editor/EditorPage.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
||||||
|
|
||||||
`src/features/editor/hooks/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||||
|
|
||||||
`src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo history.
|
`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history.
|
||||||
|
|
||||||
`src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||||
|
|
||||||
`src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
|
|
||||||
`src/features/editor/components/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||||
|
|
||||||
`src/features/editor/controls/FlyController.tsx` provides editor movement controls for player-style navigation.
|
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||||
|
|
||||||
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
||||||
|
|
||||||
@@ -138,6 +139,6 @@ Editor styles are in `src/index.css` under the `/* Editor page */` section. Clas
|
|||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
||||||
- Large `map.json` files may need virtualization, culling, or LOD support later.
|
- Large `map.json` files are not virtualized, culled, or LOD-managed.
|
||||||
- There is no snap-to-grid, duplication, material editing, or object creation workflow yet.
|
- There is no snap-to-grid, duplication, material editing, or object creation workflow.
|
||||||
- Save to Server is a Vite dev-server helper, not a production backend API.
|
- Save to Server is a Vite dev-server helper, not a production backend API.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
## Relationship To The Current Code
|
## Relationship To The Current Code
|
||||||
|
|
||||||
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
||||||
- This document is intentionally aspirational.
|
- This document describes intended direction, not implemented behavior.
|
||||||
- If this document conflicts with the current implementation, the current implementation wins.
|
- If this document conflicts with the current implementation, the current implementation wins.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
@@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- performance overlay
|
- performance overlay
|
||||||
- scene helpers
|
- scene helpers
|
||||||
- free camera and calibration controls
|
- free camera and calibration controls
|
||||||
- temporary test scenes used during development
|
- debug test scenes used during development
|
||||||
|
|
||||||
### UI Layer
|
### UI Layer
|
||||||
|
|
||||||
- `src/components/ui/` should contain player-facing HTML overlays.
|
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||||
- Expected future examples:
|
- Candidate examples:
|
||||||
- crosshair
|
- crosshair
|
||||||
- loading flow
|
- loading flow
|
||||||
- mission HUD
|
- mission HUD
|
||||||
@@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
### Gameplay Layer
|
### Gameplay Layer
|
||||||
|
|
||||||
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||||
- Likely future concerns:
|
- Likely concerns:
|
||||||
- missions
|
- missions
|
||||||
- zones
|
- zones
|
||||||
- cinematics
|
- cinematics
|
||||||
@@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- Prefer direct, working code over speculative scaffolding.
|
- Prefer direct, working code over speculative scaffolding.
|
||||||
- Shared types should stay close to their domain until they have multiple real consumers.
|
- Shared types should stay close to their domain until they have multiple real consumers.
|
||||||
- Avoid creating new managers or service layers without an active runtime need.
|
- Avoid creating new managers or service layers without an active runtime need.
|
||||||
- Debug-only runtime paths should be clearly marked and easy to remove later.
|
- Debug-only runtime paths should be clearly marked and easy to remove when obsolete.
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Editor User Guide
|
||||||
|
|
||||||
|
The map editor is available at `/editor`. It is a browser-based tool for inspecting and adjusting the objects listed in `public/map.json`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Use the editor when you need to move, rotate, or scale existing map objects without editing JSON by hand.
|
||||||
|
|
||||||
|
The editor reads the same map data as the runtime scene:
|
||||||
|
|
||||||
|
- `public/map.json` contains the object list.
|
||||||
|
- `public/models/{name}/model.gltf` contains the matching 3D model for each object name.
|
||||||
|
- Missing models are displayed as gray fallback cubes, so incomplete maps remain editable.
|
||||||
|
|
||||||
|
## Map Node Format
|
||||||
|
|
||||||
|
Each entry in `public/map.json` represents one object:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| ---------- | ------------------------------------------------- |
|
||||||
|
| `name` | Model folder name in `public/models/{name}` |
|
||||||
|
| `type` | Object category |
|
||||||
|
| `position` | Object position as `[x, y, z]` |
|
||||||
|
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
|
||||||
|
| `scale` | Object scale as `[x, y, z]` |
|
||||||
|
|
||||||
|
## Editing Workflow
|
||||||
|
|
||||||
|
1. Open `/editor` in the local app.
|
||||||
|
2. Click an object in the scene to select it.
|
||||||
|
3. Choose a transform mode: translate, rotate, or scale.
|
||||||
|
4. Drag the transform gizmo in the 3D view.
|
||||||
|
5. Check the JSON inspector if you need exact values.
|
||||||
|
6. Use undo or redo if the transform is not correct.
|
||||||
|
7. Export the JSON or save it to the dev server.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Action | Input |
|
||||||
|
| -------------------- | -------------------------- |
|
||||||
|
| Select object | Click object |
|
||||||
|
| Deselect | `Esc` or click empty space |
|
||||||
|
| Translate mode | `T` |
|
||||||
|
| Rotate mode | `R` |
|
||||||
|
| Scale mode | `S` |
|
||||||
|
| Undo | `Ctrl+Z` |
|
||||||
|
| Redo | `Ctrl+Y` |
|
||||||
|
| Locked view movement | `WASD`, `ZQSD`, arrows |
|
||||||
|
| Move up | `Space` |
|
||||||
|
| Move down | `Shift` |
|
||||||
|
|
||||||
|
## View Mode
|
||||||
|
|
||||||
|
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
|
||||||
|
|
||||||
|
## JSON Inspector
|
||||||
|
|
||||||
|
The side panel includes a raw JSON inspector:
|
||||||
|
|
||||||
|
- When no object is selected, it shows the full map node list.
|
||||||
|
- When an object is selected, it highlights the JSON lines for that object.
|
||||||
|
|
||||||
|
This is useful for checking numeric transform values before saving or exporting.
|
||||||
|
|
||||||
|
## Saving Changes
|
||||||
|
|
||||||
|
### Export JSON
|
||||||
|
|
||||||
|
`Export JSON` downloads the current map node list as `map.json`. Use this when you want to manually replace `public/map.json`.
|
||||||
|
|
||||||
|
### Save To Server
|
||||||
|
|
||||||
|
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
||||||
|
|
||||||
|
The button is hidden in production builds because production persistence is not implemented.
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
- The editor only modifies existing nodes.
|
||||||
|
- It does not create or delete objects.
|
||||||
|
- It does not edit model files or textures.
|
||||||
|
- It does not provide production persistence.
|
||||||
|
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
||||||
Generated
+1669
-319
File diff suppressed because it is too large
Load Diff
+10
-3
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
@@ -18,13 +21,15 @@
|
|||||||
"@react-three/fiber": "^9.6.0",
|
"@react-three/fiber": "^9.6.0",
|
||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
|
"@tanstack/react-router": "^1.168.25",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
"lil-gui": "^0.21.0",
|
"lil-gui": "^0.21.0",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
"r3f-perf": "^7.2.3",
|
"r3f-perf": "^7.2.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^7.5.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"three": "^0.183.2"
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -44,7 +49,9 @@
|
|||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"overrides": {
|
||||||
"node": ">=20.19.0 || >=22.12.0"
|
"r3f-perf": {
|
||||||
|
"@react-three/drei": "$@react-three/drei"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-28
@@ -1,33 +1,8 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
import { Suspense } from "react";
|
import { router } from "@/router";
|
||||||
import { Canvas } from "@react-three/fiber";
|
|
||||||
import { Crosshair } from "@/components/ui/Crosshair";
|
|
||||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
|
||||||
import { DebugPerf } from "@/utils/debug/DebugPerf";
|
|
||||||
import { World } from "@/world/World";
|
|
||||||
import { EditorPage } from "@/pages/editor/EditorPage";
|
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return <RouterProvider router={router} />;
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path="/"
|
|
||||||
element={
|
|
||||||
<>
|
|
||||||
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<World />
|
|
||||||
<DebugPerf />
|
|
||||||
</Suspense>
|
|
||||||
</Canvas>
|
|
||||||
<Crosshair />
|
|
||||||
<InteractPrompt />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/editor" element={<EditorPage />} />
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
+5
-4
@@ -3,17 +3,18 @@ import {
|
|||||||
DEBUG_CAMERA_DAMPING_FACTOR,
|
DEBUG_CAMERA_DAMPING_FACTOR,
|
||||||
DEBUG_CAMERA_MAX_DISTANCE,
|
DEBUG_CAMERA_MAX_DISTANCE,
|
||||||
DEBUG_CAMERA_MIN_DISTANCE,
|
DEBUG_CAMERA_MIN_DISTANCE,
|
||||||
} from "@/data/debugConfig";
|
} from "@/data/debug/debugConfig";
|
||||||
import {
|
import {
|
||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
PLAYER_SPAWN_POSITION_GAME,
|
PLAYER_SPAWN_POSITION_GAME,
|
||||||
} from "@/data/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
const DEBUG_CAMERA_TARGET = [
|
const DEBUG_CAMERA_TARGET: Vector3Tuple = [
|
||||||
PLAYER_SPAWN_POSITION_GAME[0],
|
PLAYER_SPAWN_POSITION_GAME[0],
|
||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
PLAYER_SPAWN_POSITION_GAME[2],
|
PLAYER_SPAWN_POSITION_GAME[2],
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export function DebugCameraControls(): React.JSX.Element {
|
export function DebugCameraControls(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
|||||||
DEBUG_GRID_SECONDARY_COLOR,
|
DEBUG_GRID_SECONDARY_COLOR,
|
||||||
DEBUG_GRID_SIZE,
|
DEBUG_GRID_SIZE,
|
||||||
DEBUG_GRID_Y,
|
DEBUG_GRID_Y,
|
||||||
} from "@/data/debugConfig";
|
} from "@/data/debug/debugConfig";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
export function DebugHelpers(): React.JSX.Element | null {
|
export function DebugHelpers(): React.JSX.Element | null {
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { useDocsLanguage } from "@/hooks/docs/useDocsLanguage";
|
||||||
|
|
||||||
|
interface DocsDocumentProps {
|
||||||
|
title: string;
|
||||||
|
meta: string;
|
||||||
|
content: string;
|
||||||
|
frContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsDocument({
|
||||||
|
title,
|
||||||
|
meta,
|
||||||
|
content,
|
||||||
|
frContent,
|
||||||
|
}: DocsDocumentProps): React.JSX.Element {
|
||||||
|
const { language, toggleLanguage } = useDocsLanguage();
|
||||||
|
const translatedContent = language === "fr" ? frContent : content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="docs-content">
|
||||||
|
<header className="docs-content__header">
|
||||||
|
<span>{title}</span>
|
||||||
|
<button
|
||||||
|
className="docs-language-toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLanguage}
|
||||||
|
aria-label="Changer la langue de la documentation"
|
||||||
|
>
|
||||||
|
<span className={language === "fr" ? "is-active" : undefined}>
|
||||||
|
FR
|
||||||
|
</span>
|
||||||
|
<span className={language === "en" ? "is-active" : undefined}>
|
||||||
|
EN
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article className="docs-section">
|
||||||
|
<div className="docs-section__eyebrow">
|
||||||
|
<span>{title}</span>
|
||||||
|
<span>{meta}</span>
|
||||||
|
</div>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{translatedContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
|
import { Home } from "lucide-react";
|
||||||
|
import { docGroups } from "@/data/docs/docsSections";
|
||||||
|
import { DocsLanguageProvider } from "@/providers/docs/DocsLanguageProvider";
|
||||||
|
|
||||||
|
export function DocsLayout(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsLanguageProvider>
|
||||||
|
<main className="docs-page">
|
||||||
|
<aside className="docs-sidebar" aria-label="Documentation">
|
||||||
|
<header className="docs-sidebar__header">
|
||||||
|
<h1>Folders</h1>
|
||||||
|
<Link
|
||||||
|
className="docs-home-link"
|
||||||
|
to="/"
|
||||||
|
aria-label="Retour à l'accueil"
|
||||||
|
>
|
||||||
|
<Home size={18} strokeWidth={2.25} aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{docGroups.map((group) => (
|
||||||
|
<section className="docs-nav-group" key={group.label}>
|
||||||
|
<h2>{group.label}</h2>
|
||||||
|
|
||||||
|
{group.sections.map((section) => (
|
||||||
|
<Link
|
||||||
|
activeProps={{
|
||||||
|
className: "docs-nav-item docs-nav-item--active",
|
||||||
|
}}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
className="docs-nav-item"
|
||||||
|
key={section.path}
|
||||||
|
to={section.path}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<strong>{section.title}</strong>
|
||||||
|
<small>{section.subtitle}</small>
|
||||||
|
</span>
|
||||||
|
<span className="docs-nav-item__meta">{section.meta}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</DocsLanguageProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+28
-43
@@ -31,6 +31,20 @@ interface EditorControlsProps {
|
|||||||
isPlayerMode?: boolean;
|
isPlayerMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TRANSFORM_OPTIONS = [
|
||||||
|
{ mode: "translate", label: "Translate", shortcut: "T", Icon: Move3D },
|
||||||
|
{ mode: "rotate", label: "Rotate", shortcut: "R", Icon: RotateCw },
|
||||||
|
{ mode: "scale", label: "Scale", shortcut: "S", Icon: Expand },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const EDITOR_SHORTCUTS = [
|
||||||
|
["Click", "Select object"],
|
||||||
|
["T / R / S", "Transform mode"],
|
||||||
|
["Ctrl Z / Y", "Undo / redo"],
|
||||||
|
["Esc", "Deselect"],
|
||||||
|
["WASD", "Move when locked"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function EditorControls({
|
export function EditorControls({
|
||||||
transformMode,
|
transformMode,
|
||||||
onTransformModeChange,
|
onTransformModeChange,
|
||||||
@@ -69,33 +83,18 @@ export function EditorControls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-transform-buttons">
|
<div className="editor-transform-buttons">
|
||||||
|
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
|
||||||
<button
|
<button
|
||||||
className={`editor-transform-button ${transformMode === "translate" ? "active" : ""}`}
|
key={mode}
|
||||||
onClick={() => onTransformModeChange("translate")}
|
className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
|
||||||
aria-pressed={transformMode === "translate"}
|
onClick={() => onTransformModeChange(mode)}
|
||||||
|
aria-pressed={transformMode === mode}
|
||||||
>
|
>
|
||||||
<Move3D size={16} aria-hidden="true" />
|
<Icon size={16} aria-hidden="true" />
|
||||||
<span>Translate</span>
|
<span>{label}</span>
|
||||||
<kbd>T</kbd>
|
<kbd>{shortcut}</kbd>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`editor-transform-button ${transformMode === "rotate" ? "active" : ""}`}
|
|
||||||
onClick={() => onTransformModeChange("rotate")}
|
|
||||||
aria-pressed={transformMode === "rotate"}
|
|
||||||
>
|
|
||||||
<RotateCw size={16} aria-hidden="true" />
|
|
||||||
<span>Rotate</span>
|
|
||||||
<kbd>R</kbd>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`editor-transform-button ${transformMode === "scale" ? "active" : ""}`}
|
|
||||||
onClick={() => onTransformModeChange("scale")}
|
|
||||||
aria-pressed={transformMode === "scale"}
|
|
||||||
>
|
|
||||||
<Expand size={16} aria-hidden="true" />
|
|
||||||
<span>Scale</span>
|
|
||||||
<kbd>S</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-history-buttons">
|
<div className="editor-history-buttons">
|
||||||
@@ -203,26 +202,12 @@ export function EditorControls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl className="editor-shortcuts-list">
|
<dl className="editor-shortcuts-list">
|
||||||
<div>
|
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
||||||
<dt>Click</dt>
|
<div key={keys}>
|
||||||
<dd>Select object</dd>
|
<dt>{keys}</dt>
|
||||||
</div>
|
<dd>{description}</dd>
|
||||||
<div>
|
|
||||||
<dt>T / R / S</dt>
|
|
||||||
<dd>Transform mode</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Ctrl Z / Y</dt>
|
|
||||||
<dd>Undo / redo</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Esc</dt>
|
|
||||||
<dd>Deselect</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>WASD</dt>
|
|
||||||
<dd>Move when locked</dd>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
+50
-30
@@ -29,6 +29,12 @@ interface EditorNodeCommonProps {
|
|||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditorNodePointerHandlers {
|
||||||
|
onClick: (event: ThreeEvent<MouseEvent>) => void;
|
||||||
|
onPointerEnter: (event: ThreeEvent<PointerEvent>) => void;
|
||||||
|
onPointerLeave: (event: ThreeEvent<PointerEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
||||||
object.position.set(...node.position);
|
object.position.set(...node.position);
|
||||||
object.rotation.set(...node.rotation);
|
object.rotation.set(...node.rotation);
|
||||||
@@ -88,6 +94,36 @@ function cloneHighlightedMaterial(
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeHighlightColor(
|
||||||
|
isSelected: boolean,
|
||||||
|
isHovered: boolean,
|
||||||
|
): string | null {
|
||||||
|
if (isSelected) return "#ffffff";
|
||||||
|
if (isHovered) return "#b8b8b8";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEditorNodePointerHandlers(
|
||||||
|
index: number,
|
||||||
|
onSelectNode: (index: number | null) => void,
|
||||||
|
onHoverNode: (index: number | null) => void,
|
||||||
|
): EditorNodePointerHandlers {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onSelectNode(index);
|
||||||
|
},
|
||||||
|
onPointerEnter: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onHoverNode(index);
|
||||||
|
},
|
||||||
|
onPointerLeave: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onHoverNode(null);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorMap({
|
export function EditorMap({
|
||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
@@ -224,15 +260,16 @@ function EditorModelNode({
|
|||||||
const { scene } = useGLTF(modelUrl);
|
const { scene } = useGLTF(modelUrl);
|
||||||
|
|
||||||
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||||
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
|
index,
|
||||||
|
onSelectNode,
|
||||||
|
onHoverNode,
|
||||||
|
);
|
||||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
const highlightColor = isSelected
|
const highlightColor = getNodeHighlightColor(isSelected, isHovered);
|
||||||
? "#ffffff"
|
|
||||||
: isHovered
|
|
||||||
? "#b8b8b8"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
groupRef.current.traverse((child) => {
|
groupRef.current.traverse((child) => {
|
||||||
if (!(child instanceof THREE.Mesh)) {
|
if (!(child instanceof THREE.Mesh)) {
|
||||||
@@ -288,18 +325,7 @@ function EditorModelNode({
|
|||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
{...pointerHandlers}
|
||||||
e.stopPropagation();
|
|
||||||
onSelectNode(index);
|
|
||||||
}}
|
|
||||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(index);
|
|
||||||
}}
|
|
||||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -314,9 +340,14 @@ function EditorFallbackNode({
|
|||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
|
index,
|
||||||
|
onSelectNode,
|
||||||
|
onHoverNode,
|
||||||
|
);
|
||||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||||
|
|
||||||
const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f";
|
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -324,18 +355,7 @@ function EditorFallbackNode({
|
|||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
{...pointerHandlers}
|
||||||
e.stopPropagation();
|
|
||||||
onSelectNode(index);
|
|
||||||
}}
|
|
||||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(index);
|
|
||||||
}}
|
|
||||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onHoverNode(null);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
<meshStandardMaterial color={color} />
|
<meshStandardMaterial color={color} />
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { OrbitControls } from "@react-three/drei";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
import { FlyController } from "@/features/editor/controls/FlyController";
|
import { EditorMap } from "@/components/editor/scene/EditorMap";
|
||||||
import { EditorMap } from "@/features/editor/scene/EditorMap";
|
import { FlyController } from "@/controls/editor/FlyController";
|
||||||
import type { MapNode, TransformMode, SceneData } from "@/types/editor";
|
import type { MapNode, TransformMode, SceneData } from "@/types/editor";
|
||||||
|
|
||||||
interface EditorSceneProps {
|
interface EditorSceneProps {
|
||||||
@@ -3,7 +3,7 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||||||
import { RigidBody } from "@react-three/rapier";
|
import { RigidBody } from "@react-three/rapier";
|
||||||
import type { RapierRigidBody } from "@react-three/rapier";
|
import type { RapierRigidBody } from "@react-three/rapier";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { InteractableObject } from "@/components/3d/InteractableObject";
|
import { InteractableObject } from "@/components/three/InteractableObject";
|
||||||
import {
|
import {
|
||||||
GRAB_DEFAULT_COLLIDERS,
|
GRAB_DEFAULT_COLLIDERS,
|
||||||
GRAB_DEFAULT_LABEL,
|
GRAB_DEFAULT_LABEL,
|
||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
GRAB_THROW_BOOST_MAX,
|
GRAB_THROW_BOOST_MAX,
|
||||||
GRAB_THROW_BOOST_MIN,
|
GRAB_THROW_BOOST_MIN,
|
||||||
GRAB_THROW_BOOST_STEP,
|
GRAB_THROW_BOOST_STEP,
|
||||||
} from "@/data/grabConfig";
|
} from "@/data/interaction/grabConfig";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
import type { ColliderShape, Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
interface GrabbableObjectProps {
|
interface GrabbableObjectProps {
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
+47
-26
@@ -8,13 +8,13 @@ import {
|
|||||||
INTERACTION_DEBUG_SPHERE_COLOR,
|
INTERACTION_DEBUG_SPHERE_COLOR,
|
||||||
INTERACTION_DEBUG_SPHERE_OPACITY,
|
INTERACTION_DEBUG_SPHERE_OPACITY,
|
||||||
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
} from "@/data/debugConfig";
|
} from "@/data/debug/debugConfig";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
import { InteractionManager } from "@/managers/InteractionManager";
|
||||||
import { INTERACTION_RADIUS } from "@/data/interactionConfig";
|
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
|
||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
import type { InteractableHandle, InteractableKind } from "@/types/interaction";
|
import type { InteractableHandle } from "@/types/interaction";
|
||||||
|
|
||||||
interface InteractableObjectBaseProps {
|
interface InteractableObjectBaseProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -37,46 +37,67 @@ type InteractableObjectProps =
|
|||||||
| TriggerInteractableObjectProps
|
| TriggerInteractableObjectProps
|
||||||
| GrabInteractableObjectProps;
|
| GrabInteractableObjectProps;
|
||||||
|
|
||||||
type MutableInteractableHandle = {
|
|
||||||
kind: InteractableKind;
|
|
||||||
label: string;
|
|
||||||
onPress: () => void;
|
|
||||||
onRelease?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _cameraPos = new THREE.Vector3();
|
const _cameraPos = new THREE.Vector3();
|
||||||
const _cameraDir = new THREE.Vector3();
|
const _cameraDir = new THREE.Vector3();
|
||||||
const _objectPos = new THREE.Vector3();
|
const _objectPos = new THREE.Vector3();
|
||||||
const _raycaster = new THREE.Raycaster();
|
const _raycaster = new THREE.Raycaster();
|
||||||
|
|
||||||
|
function createInteractableHandle(
|
||||||
|
props: InteractableObjectProps,
|
||||||
|
): InteractableHandle {
|
||||||
|
if (props.kind === "grab") {
|
||||||
|
return {
|
||||||
|
kind: props.kind,
|
||||||
|
label: props.label,
|
||||||
|
onPress: props.onPress,
|
||||||
|
onRelease: props.onRelease,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: props.kind,
|
||||||
|
label: props.label,
|
||||||
|
onPress: props.onPress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function InteractableObject(
|
export function InteractableObject(
|
||||||
props: InteractableObjectProps,
|
props: InteractableObjectProps,
|
||||||
): React.JSX.Element {
|
): React.JSX.Element {
|
||||||
const { kind, label, position, bodyRef, onPress, children } = props;
|
const { kind, label, position, bodyRef, onPress, children } = props;
|
||||||
const onRelease = props.kind === "grab" ? props.onRelease : undefined;
|
const onRelease = props.kind === "grab" ? props.onRelease : null;
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const debugSphereRef = useRef<THREE.Mesh>(null);
|
const debugSphereRef = useRef<THREE.Mesh>(null);
|
||||||
|
|
||||||
const handle = useRef<InteractableHandle>(
|
const handle = useRef<InteractableHandle>(createInteractableHandle(props));
|
||||||
props.kind === "grab"
|
|
||||||
? { kind: props.kind, label, onPress, onRelease: props.onRelease }
|
|
||||||
: { kind: props.kind, label, onPress },
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const current = handle.current as MutableInteractableHandle;
|
const currentHandle = handle.current;
|
||||||
current.kind = kind;
|
|
||||||
current.label = label;
|
if (currentHandle.kind === kind) {
|
||||||
current.onPress = onPress;
|
currentHandle.label = label;
|
||||||
|
currentHandle.onPress = onPress;
|
||||||
|
|
||||||
|
if (currentHandle.kind === "grab") {
|
||||||
|
if (!onRelease) return;
|
||||||
|
currentHandle.onRelease = onRelease;
|
||||||
|
}
|
||||||
|
|
||||||
if (kind === "grab" && onRelease) {
|
|
||||||
current.onRelease = onRelease;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete current.onRelease;
|
if (kind === "grab") {
|
||||||
return undefined;
|
if (!onRelease) return;
|
||||||
|
handle.current = { kind, label, onPress, onRelease };
|
||||||
|
} else {
|
||||||
|
handle.current = { kind, label, onPress };
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = InteractionManager.getInstance();
|
||||||
|
if (manager.getState().focused === currentHandle) {
|
||||||
|
manager.setFocused(handle.current);
|
||||||
|
}
|
||||||
}, [kind, label, onPress, onRelease]);
|
}, [kind, label, onPress, onRelease]);
|
||||||
|
|
||||||
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { RigidBody } from "@react-three/rapier";
|
import { RigidBody } from "@react-three/rapier";
|
||||||
import { InteractableObject } from "@/components/3d/InteractableObject";
|
import { InteractableObject } from "@/components/three/InteractableObject";
|
||||||
import {
|
import {
|
||||||
TRIGGER_DEFAULT_COLLIDERS,
|
TRIGGER_DEFAULT_COLLIDERS,
|
||||||
TRIGGER_DEFAULT_LABEL,
|
TRIGGER_DEFAULT_LABEL,
|
||||||
TRIGGER_DEFAULT_SOUND_VOLUME,
|
TRIGGER_DEFAULT_SOUND_VOLUME,
|
||||||
TRIGGER_DEFAULT_SPAWN_OFFSET,
|
TRIGGER_DEFAULT_SPAWN_OFFSET,
|
||||||
} from "@/data/triggerConfig";
|
} from "@/data/interaction/triggerConfig";
|
||||||
import { AudioManager } from "@/stateManager/AudioManager";
|
import { AudioManager } from "@/managers/AudioManager";
|
||||||
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
import type { ColliderShape, Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
interface SpawnedModel {
|
interface SpawnedModel {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INTERACT_KEY } from "@/data/keybindings";
|
import { INTERACT_KEY } from "@/data/input/keybindings";
|
||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useInteraction } from "@/hooks/useInteraction";
|
import { useInteraction } from "@/hooks/useInteraction";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export type DocsLanguage = "en" | "fr";
|
||||||
|
|
||||||
|
interface DocsLanguageContextValue {
|
||||||
|
language: DocsLanguage;
|
||||||
|
toggleLanguage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocsLanguageContext =
|
||||||
|
createContext<DocsLanguageContextValue | null>(null);
|
||||||
@@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
|
|||||||
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
||||||
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
||||||
|
|
||||||
export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88;
|
|
||||||
|
|
||||||
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
||||||
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
||||||
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
export const TEST_SCENE_FLOOR_POSITION: Vector3Tuple = [0, -0.5, 0];
|
export const TEST_SCENE_FLOOR_POSITION: Vector3Tuple = [0, -0.5, 0];
|
||||||
export const TEST_SCENE_FLOOR_SIZE: Vector3Tuple = [200, 1, 200];
|
export const TEST_SCENE_FLOOR_SIZE: Vector3Tuple = [200, 1, 200];
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
interface DocSection {
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
meta: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocGroup {
|
||||||
|
label: string;
|
||||||
|
sections: DocSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const docGroups: DocGroup[] = [
|
||||||
|
{
|
||||||
|
label: "Technical",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
path: "/docs",
|
||||||
|
title: "README",
|
||||||
|
subtitle: "Project overview",
|
||||||
|
meta: "01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/architecture",
|
||||||
|
title: "Current Architecture",
|
||||||
|
subtitle: "Runtime structure",
|
||||||
|
meta: "02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/target-architecture",
|
||||||
|
title: "Target Architecture",
|
||||||
|
subtitle: "Next direction",
|
||||||
|
meta: "03",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/technical-editor",
|
||||||
|
title: "Editor Technical Notes",
|
||||||
|
subtitle: "Implementation details",
|
||||||
|
meta: "04",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "User",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
path: "/docs/features",
|
||||||
|
title: "Features",
|
||||||
|
subtitle: "Implemented scope",
|
||||||
|
meta: "05",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/editor",
|
||||||
|
title: "Editor User Guide",
|
||||||
|
subtitle: "Editing workflow",
|
||||||
|
meta: "06",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
export const readmeFr = `# La-Fabrik
|
||||||
|
|
||||||
|
Une expérience web 3D interactive pour La Fabrik Durable, un service low-tech de réparation et de transformation situé à Altera, une ville post-capitaliste reconstruite en 2039. Les joueurs incarnent un technicien fraîchement intégré et vivent une journée de service : réparer un vélo électrique, remettre en état un réseau d'énergie et améliorer le système d'irrigation d'une ferme verticale.
|
||||||
|
|
||||||
|
Construit avec React, Three.js et Vite. Fonctionne dans le navigateur, sans installation côté utilisateur.
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
### Build et langage
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| -------------------------------------------------- |
|
||||||
|
| [TypeScript](https://www.typescriptlang.org/docs/) |
|
||||||
|
| [React](https://react.dev/learn) |
|
||||||
|
| [Vite](https://vite.dev/guide/) |
|
||||||
|
| [ESLint](https://eslint.org/docs/latest/) |
|
||||||
|
| [Prettier](https://prettier.io/docs/) |
|
||||||
|
|
||||||
|
### Moteur 3D
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| ----------------------------------------------------------------------------------------- |
|
||||||
|
| [Three.js](https://threejs.org/docs/) |
|
||||||
|
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
||||||
|
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
||||||
|
| [@react-three/rapier](https://rapier.rs/docs/) |
|
||||||
|
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) |
|
||||||
|
| [GSAP](https://gsap.com/docs/v3/Installation/) |
|
||||||
|
|
||||||
|
### Performance et effets
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| --------------------------------------------------------------------------- |
|
||||||
|
| [r3f-perf](https://github.com/utsuboco/r3f-perf) |
|
||||||
|
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
la-fabrik/
|
||||||
|
├── public/
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── map/ # Carte de base, chargée au démarrage
|
||||||
|
│ │ ├── workshop/
|
||||||
|
│ │ ├── powerGrid/
|
||||||
|
│ │ └── farm/
|
||||||
|
│ ├── textures/
|
||||||
|
│ └── sounds/
|
||||||
|
│
|
||||||
|
└── src/
|
||||||
|
├── world/ # Monde 3D persistant
|
||||||
|
│ ├── World.tsx # Composition principale de la scène
|
||||||
|
│ ├── Map.tsx # Carte de base, toujours montée
|
||||||
|
│ ├── Lighting.tsx # Lumières ambiante, directionnelle et ponctuelles
|
||||||
|
│ ├── Environment.tsx # HDRI, brouillard, ciel
|
||||||
|
│ ├── PostFX.tsx # Bloom, SSAO, aberration chromatique
|
||||||
|
│ ├── zones/ # Zones spatiales, LOD par zone
|
||||||
|
│ └── player/ # Contrôleur joueur et caméra
|
||||||
|
│
|
||||||
|
├── components/
|
||||||
|
│ ├── 3d/ # Éléments 3D réutilisables
|
||||||
|
│ └── ui/ # Overlays HTML hors Canvas
|
||||||
|
│
|
||||||
|
├── managers/ # Logique, état et orchestration
|
||||||
|
├── hooks/ # Hooks React autour des managers
|
||||||
|
├── data/ # Configuration statique
|
||||||
|
├── shaders/ # Shaders GLSL
|
||||||
|
└── utils/ # Utilitaires partagés et debug
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Démarrage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git
|
||||||
|
cd La-Fabrik
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- application : \`http://localhost:5173\`
|
||||||
|
- mode debug : \`http://localhost:5173?debug\`
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Voir le fichier [LICENSE](./LICENSE).
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const architectureFr = `# Architecture actuelle
|
||||||
|
|
||||||
|
Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
|
||||||
|
|
||||||
|
## Structure runtime
|
||||||
|
|
||||||
|
- \`src/App.tsx\` monte le \`RouterProvider\`, qui pilote l'affichage des vues de l'application.
|
||||||
|
- \`src/pages/page.tsx\` monte le \`Canvas\`, le \`World\` 3D, l'overlay de performance debug et les overlays HTML.
|
||||||
|
- \`src/world/World.tsx\` compose la scène active avec :
|
||||||
|
- l'environnement et l'éclairage
|
||||||
|
- les helpers debug et le mode caméra debug
|
||||||
|
- soit la carte principale, soit la scène de test physique debug
|
||||||
|
- le rig joueur quand le mode caméra actif est \`player\`
|
||||||
|
- \`src/world/GameMap.tsx\` charge les modèles de carte disponibles et construit l'octree de collision.
|
||||||
|
- \`src/world/debug/TestScene.tsx\` fournit une scène orientée debug pour les interactions et la physique.
|
||||||
|
- \`src/world/player/Player.tsx\` monte la caméra et le contrôleur.
|
||||||
|
- \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut et les inputs d'interaction.
|
||||||
|
|
||||||
|
## Modèle d'interaction
|
||||||
|
|
||||||
|
- \`src/managers/InteractionManager.ts\` est la source d'état actuelle des interactions.
|
||||||
|
- \`src/components/three/InteractableObject.tsx\` gère la détection de focus par distance et raycasting.
|
||||||
|
- \`src/components/three/TriggerObject.tsx\` implémente les interactions de type trigger.
|
||||||
|
- \`src/components/three/GrabbableObject.tsx\` implémente les interactions saisir / relâcher.
|
||||||
|
- \`src/hooks/useInteraction.ts\` expose un snapshot d'interaction à l'UI React.
|
||||||
|
- \`src/components/ui/InteractPrompt.tsx\` affiche le prompt \`E\` pour les interactions trigger.
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
- \`src/managers/AudioManager.ts\` fournit actuellement une lecture de sons one-shot avec pool.
|
||||||
|
- Les interactions trigger peuvent lancer directement un son via \`AudioManager\`.
|
||||||
|
|
||||||
|
## Système debug
|
||||||
|
|
||||||
|
- Le mode debug est activé avec \`?debug\`.
|
||||||
|
- \`src/utils/debug/Debug.ts\` possède l'instance \`lil-gui\` et les contrôles debug.
|
||||||
|
- \`src/hooks/debug/useCameraMode.ts\` et \`src/hooks/debug/useSceneMode.ts\` s'abonnent à l'état debug.
|
||||||
|
- \`src/components/debug/DebugPerf.tsx\` monte \`r3f-perf\` en lazy uniquement en mode debug.
|
||||||
|
- \`src/components/debug/scene/DebugHelpers.tsx\` monte les helpers debug.
|
||||||
|
- \`src/components/debug/scene/DebugCameraControls.tsx\` monte la caméra libre debug.
|
||||||
|
|
||||||
|
## Limites actuelles
|
||||||
|
|
||||||
|
- Le dépôt est encore un prototype, pas le runtime complet du jeu.
|
||||||
|
- \`src/world/debug/TestScene.tsx\` fait encore partie de la composition active.
|
||||||
|
- Il n'existe pas encore d'orchestrateur gameplay central comme \`GameManager\`.
|
||||||
|
- Les systèmes de missions, zones, cinématiques et dialogues ne sont pas implémentés.
|
||||||
|
- Le joueur utilise une collision octree et des règles simples, pas une pile physique gameplay complète.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const targetArchitectureFr = `# Architecture cible
|
||||||
|
|
||||||
|
Ce document décrit l'architecture visée à moyen terme pour le projet.
|
||||||
|
|
||||||
|
## Relation avec le code actuel
|
||||||
|
|
||||||
|
- \`docs/technical/architecture.md\` reste la source de vérité de ce qui existe maintenant.
|
||||||
|
- Ce document est volontairement aspirational.
|
||||||
|
- Si ce document contredit l'implémentation actuelle, l'implémentation actuelle gagne.
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- Garder \`App.tsx\` petit et centré sur l'orchestration.
|
||||||
|
- Séparer le code de production du monde des chemins runtime uniquement debug.
|
||||||
|
- Garder une source de vérité claire par responsabilité.
|
||||||
|
- Faire grandir les systèmes gameplay progressivement, sans préconstruire une architecture vide.
|
||||||
|
|
||||||
|
## Couches prévues
|
||||||
|
|
||||||
|
### Couche App
|
||||||
|
|
||||||
|
- \`App.tsx\` monte la scène canvas et les overlays HTML de premier niveau.
|
||||||
|
- Il doit rester fin et éviter la logique gameplay.
|
||||||
|
|
||||||
|
### Couche World
|
||||||
|
|
||||||
|
- \`src/world/\` doit contenir la composition de scène de production et les objets de scène de production.
|
||||||
|
- Responsabilités attendues :
|
||||||
|
- composition du monde
|
||||||
|
- carte, environnement, éclairage
|
||||||
|
- contrôleur joueur
|
||||||
|
- ancres d'interaction de production
|
||||||
|
- post-processing de production si nécessaire
|
||||||
|
|
||||||
|
### Couche Debug
|
||||||
|
|
||||||
|
- Les scènes et outils uniquement debug doivent être isolés du chemin de production.
|
||||||
|
- Responsabilités attendues :
|
||||||
|
- \`lil-gui\`
|
||||||
|
- overlay de performance
|
||||||
|
- helpers de scène
|
||||||
|
- caméra libre et contrôles de calibration
|
||||||
|
- scènes temporaires de test utilisées pendant le développement
|
||||||
|
|
||||||
|
### Couche UI
|
||||||
|
|
||||||
|
- \`src/components/ui/\` doit contenir les overlays HTML visibles par le joueur.
|
||||||
|
- Exemples futurs :
|
||||||
|
- crosshair
|
||||||
|
- flow de chargement
|
||||||
|
- HUD de mission
|
||||||
|
- overlays narratifs
|
||||||
|
|
||||||
|
### Couche Gameplay
|
||||||
|
|
||||||
|
- À mesure que le projet grandit, l'état gameplay peut évoluer vers une couche d'orchestration plus claire.
|
||||||
|
- Sujets probables :
|
||||||
|
- missions
|
||||||
|
- zones
|
||||||
|
- cinématiques
|
||||||
|
- dialogues
|
||||||
|
- audio
|
||||||
|
- interactions
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Préférer du code direct et fonctionnel plutôt qu'un échafaudage spéculatif.
|
||||||
|
- Les types partagés doivent rester proches de leur domaine jusqu'à avoir plusieurs vrais consommateurs.
|
||||||
|
- Éviter de créer de nouveaux managers ou services sans besoin runtime actif.
|
||||||
|
- Les chemins runtime uniquement debug doivent être clairement marqués et faciles à retirer plus tard.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const featuresFr = `# Fonctionnalités implémentées
|
||||||
|
|
||||||
|
Ce document liste les fonctionnalités présentes dans le code actuel.
|
||||||
|
|
||||||
|
## Scène
|
||||||
|
|
||||||
|
- Scène React Three Fiber plein écran
|
||||||
|
- Carte principale chargée depuis \`public/models/map/model.gltf\`
|
||||||
|
- Scène de test physique debug sélectionnable depuis le panneau debug
|
||||||
|
- Éclairage ambiant et directionnel
|
||||||
|
- Configuration de l'environnement de fond
|
||||||
|
|
||||||
|
## Joueur
|
||||||
|
|
||||||
|
- Mode caméra joueur
|
||||||
|
- Orientation souris avec pointer lock
|
||||||
|
- Déplacement avec \`ZQSD\`
|
||||||
|
- Saut
|
||||||
|
- Collision basée sur une octree contre la carte chargée
|
||||||
|
|
||||||
|
## Interactions
|
||||||
|
|
||||||
|
- Détection de focus par distance et raycast
|
||||||
|
- Interactions trigger activées avec \`E\`
|
||||||
|
- Interactions grab activées avec le bouton principal de la souris
|
||||||
|
- Prompt d'interaction affiché pour les interactions trigger
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
- Lecture de sons one-shot pour les interactions trigger
|
||||||
|
- Pool simple par son via \`AudioManager\`
|
||||||
|
|
||||||
|
## Outils debug
|
||||||
|
|
||||||
|
- Le paramètre \`?debug\` active le panneau debug
|
||||||
|
- Contrôles \`lil-gui\` pour le mode caméra, le mode scène et les sphères d'interaction
|
||||||
|
- Helpers de scène debug
|
||||||
|
- Caméra libre debug
|
||||||
|
- Overlay \`r3f-perf\`
|
||||||
|
|
||||||
|
## Pas encore implémenté
|
||||||
|
|
||||||
|
- système de missions
|
||||||
|
- système de zones
|
||||||
|
- système de cinématiques
|
||||||
|
- système de dialogues
|
||||||
|
- flow de chargement
|
||||||
|
- minimap et HUD de mission
|
||||||
|
- séparation complète production / debug pour les scènes gameplay
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const editorFr = `# Éditeur de carte
|
||||||
|
|
||||||
|
L'éditeur de carte est disponible sur "/editor". Il permet d'inspecter et d'ajuster les objets déclarés dans "/public/map.json" directement depuis le navigateur.
|
||||||
|
|
||||||
|
## Ce qui est édité
|
||||||
|
|
||||||
|
L'éditeur travaille sur la liste de nodes stockée dans "/public/map.json".
|
||||||
|
|
||||||
|
Chaque node décrit un objet de la scène :
|
||||||
|
|
||||||
|
- "name" : nom du dossier modèle dans "/public/models/{name}/model.gltf"
|
||||||
|
- "type" : catégorie de l'objet
|
||||||
|
- "position" : "[x, y, z]"
|
||||||
|
- "rotation" : "[x, y, z]"
|
||||||
|
- "scale" : "[x, y, z]"
|
||||||
|
|
||||||
|
Les modèles sont chargés depuis "/public/models". Si un modèle manque, l'éditeur affiche un cube gris de remplacement pour que le node reste sélectionnable et déplaçable.
|
||||||
|
|
||||||
|
## Workflow de base
|
||||||
|
|
||||||
|
1. Ouvrir "/editor".
|
||||||
|
2. Sélectionner un objet dans la vue 3D.
|
||||||
|
3. Choisir un mode de transformation : translation, rotation ou scale.
|
||||||
|
4. Déplacer la gizmo de transformation.
|
||||||
|
5. Utiliser undo ou redo si nécessaire.
|
||||||
|
6. Exporter le JSON mis à jour ou le sauvegarder sur le serveur de dev.
|
||||||
|
|
||||||
|
## Contrôles
|
||||||
|
|
||||||
|
| Action | Input |
|
||||||
|
| --- | --- |
|
||||||
|
| Sélectionner un objet | Clic sur l'objet |
|
||||||
|
| Désélectionner | "Esc" ou clic dans le vide |
|
||||||
|
| Mode translation | "T" |
|
||||||
|
| Mode rotation | "R" |
|
||||||
|
| Mode scale | "S" |
|
||||||
|
| Undo | "Ctrl+Z" |
|
||||||
|
| Redo | "Ctrl+Y" |
|
||||||
|
| Déplacement en vue verrouillée | "WASD", "ZQSD", flèches |
|
||||||
|
| Monter / descendre | "Space", "Shift" |
|
||||||
|
|
||||||
|
## Actions fichier
|
||||||
|
|
||||||
|
### Export JSON
|
||||||
|
|
||||||
|
"Export JSON" télécharge la liste actuelle des nodes sous le nom "map.json". À utiliser pour remplacer manuellement "/public/map.json".
|
||||||
|
|
||||||
|
### Save to server
|
||||||
|
|
||||||
|
"Save to server" est disponible uniquement en développement local. L'action écrit la carte modifiée dans "/public/map.json" via l'endpoint du serveur de dev Vite.
|
||||||
|
|
||||||
|
Cette action est masquée dans les builds de production car il n'existe pas encore d'API de persistance production.
|
||||||
|
|
||||||
|
## Inspecteur JSON
|
||||||
|
|
||||||
|
Le panneau latéral affiche le JSON brut de la carte :
|
||||||
|
|
||||||
|
- sans sélection, il affiche toute la liste des nodes
|
||||||
|
- avec un objet sélectionné, il met en évidence les lignes du node sélectionné
|
||||||
|
|
||||||
|
Utilise-le pour vérifier les valeurs numériques exactes avant export ou sauvegarde.
|
||||||
|
|
||||||
|
## Limites actuelles
|
||||||
|
|
||||||
|
- L'éditeur modifie uniquement les nodes existants.
|
||||||
|
- Il n'y a pas encore d'interface pour créer ou supprimer des objets.
|
||||||
|
- La sauvegarde production n'est pas implémentée.
|
||||||
|
- Les modèles manquants s'affichent comme cubes de fallback au lieu de bloquer tout l'éditeur.
|
||||||
|
`;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
export const TRIGGER_DEFAULT_COLLIDERS = "ball";
|
export const TRIGGER_DEFAULT_COLLIDERS = "ball";
|
||||||
export const TRIGGER_DEFAULT_LABEL = "Interagir";
|
export const TRIGGER_DEFAULT_LABEL = "Interagir";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
export const PLAYER_EYE_HEIGHT = 1.75;
|
export const PLAYER_EYE_HEIGHT = 1.75;
|
||||||
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { DocsLanguageContext } from "@/contexts/docs/DocsLanguageContext";
|
||||||
|
|
||||||
|
export function useDocsLanguage() {
|
||||||
|
const context = useContext(DocsLanguageContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useDocsLanguage must be used inside DocsLanguageProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
import { InteractionManager } from "@/managers/InteractionManager";
|
||||||
import type { InteractionSnapshot } from "@/types/interaction";
|
import type { InteractionSnapshot } from "@/types/interaction";
|
||||||
|
|
||||||
const manager = InteractionManager.getInstance();
|
const manager = InteractionManager.getInstance();
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import { useEffect, useRef } from "react";
|
|||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import type { Object3D } from "three";
|
import type { Object3D } from "three";
|
||||||
import { Octree } from "three/addons/math/Octree.js";
|
import { Octree } from "three/addons/math/Octree.js";
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
import type { OctreeReadyHandler } from "@/types/three";
|
||||||
|
|
||||||
export function useOctreeGraphNode(
|
export function useOctreeGraphNode(
|
||||||
graphNodeRef: RefObject<Object3D | null>,
|
graphNodeRef: RefObject<Object3D | null>,
|
||||||
onOctreeReady: OctreeReadyHandler,
|
onOctreeReady: OctreeReadyHandler,
|
||||||
|
rebuildKey: string | number = 0,
|
||||||
): void {
|
): void {
|
||||||
const octreeBuilt = useRef(false);
|
const octreeBuilt = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
octreeBuilt.current = false;
|
||||||
|
}, [rebuildKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const graphNode = graphNodeRef.current;
|
const graphNode = graphNodeRef.current;
|
||||||
if (octreeBuilt.current || !graphNode) return;
|
if (octreeBuilt.current || !graphNode) return;
|
||||||
@@ -20,5 +25,5 @@ export function useOctreeGraphNode(
|
|||||||
const octree = new Octree();
|
const octree = new Octree();
|
||||||
octree.fromGraphNode(graphNode);
|
octree.fromGraphNode(graphNode);
|
||||||
onOctreeReady(octree);
|
onOctreeReady(octree);
|
||||||
}, [graphNodeRef, onOctreeReady]);
|
}, [graphNodeRef, onOctreeReady, rebuildKey]);
|
||||||
}
|
}
|
||||||
|
|||||||
+312
-1
@@ -1,6 +1,8 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
font-family: Inter;
|
font-family: "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -27,6 +29,315 @@ canvas {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #050505;
|
||||||
|
color: #f4efe7;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar {
|
||||||
|
border-right: 2px solid #d8d0c4;
|
||||||
|
background: #050505;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar__header,
|
||||||
|
.docs-content__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar__header h1,
|
||||||
|
.docs-content__header span {
|
||||||
|
margin: 0;
|
||||||
|
color: #f4efe7;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar nav {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-group {
|
||||||
|
display: grid;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-group h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 13px 16px 8px;
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar a {
|
||||||
|
color: #f4efe7;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-top: 1px solid rgba(216, 208, 196, 0.35);
|
||||||
|
color: #f4efe7;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-home-link {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item span:first-child {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item small,
|
||||||
|
.docs-nav-item__meta {
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar a:hover,
|
||||||
|
.docs-sidebar a:focus-visible,
|
||||||
|
.docs-nav-item--active {
|
||||||
|
background: #f4efe7;
|
||||||
|
color: #050505;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar a:hover small,
|
||||||
|
.docs-sidebar a:hover .docs-nav-item__meta,
|
||||||
|
.docs-sidebar a:focus-visible small,
|
||||||
|
.docs-sidebar a:focus-visible .docs-nav-item__meta,
|
||||||
|
.docs-nav-item--active small,
|
||||||
|
.docs-nav-item--active .docs-nav-item__meta {
|
||||||
|
color: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-content__header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 2px;
|
||||||
|
border: 2px solid #d8d0c4;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: #f4efe7;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle span {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle .is-active {
|
||||||
|
background: #f4efe7;
|
||||||
|
color: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle:hover,
|
||||||
|
.docs-language-toggle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(216, 208, 196, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section {
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 34px clamp(18px, 4vw, 56px) 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section__eyebrow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h1,
|
||||||
|
.docs-section h2,
|
||||||
|
.docs-section h3 {
|
||||||
|
color: #f4efe7;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: clamp(46px, 7vw, 88px);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h2 {
|
||||||
|
margin-top: 44px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
font-size: clamp(28px, 4vw, 44px);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h3 {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section p,
|
||||||
|
.docs-section li {
|
||||||
|
color: #d8d0c4;
|
||||||
|
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section ul,
|
||||||
|
.docs-section ol {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section a {
|
||||||
|
color: #f4efe7;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section code {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background: rgba(216, 208, 196, 0.22);
|
||||||
|
color: #f4efe7;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section pre {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #0d0d0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section pre code {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #f4efe7;
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section table {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section th,
|
||||||
|
.docs-section td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 2px solid #d8d0c4;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section th {
|
||||||
|
background: #111;
|
||||||
|
color: #f4efe7;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
border-left: 2px solid #d8d0c4;
|
||||||
|
color: #a9a196;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.docs-page {
|
||||||
|
display: block;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-content {
|
||||||
|
overflow: visible;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.crosshair {
|
.crosshair {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import architecture from "../../../../docs/technical/architecture.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
import { architectureFr } from "@/data/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsArchitecturePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={architecture}
|
||||||
|
frContent={architectureFr}
|
||||||
|
meta="02"
|
||||||
|
title="Architecture actuelle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import editor from "../../../../docs/user/editor.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
import { editorFr } from "@/data/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsEditorPage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={editor}
|
||||||
|
frContent={editorFr}
|
||||||
|
meta="06"
|
||||||
|
title="Editor User Guide"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import features from "../../../../docs/user/features.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
import { featuresFr } from "@/data/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsFeaturesPage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={features}
|
||||||
|
frContent={featuresFr}
|
||||||
|
meta="05"
|
||||||
|
title="Features"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import readme from "../../../README.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
import { readmeFr } from "@/data/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsReadmePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={readme}
|
||||||
|
frContent={readmeFr}
|
||||||
|
meta="01"
|
||||||
|
title="README"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import targetArchitecture from "../../../../docs/technical/target-architecture.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
import { targetArchitectureFr } from "@/data/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsTargetArchitecturePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={targetArchitecture}
|
||||||
|
frContent={targetArchitectureFr}
|
||||||
|
meta="03"
|
||||||
|
title="Architecture cible"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import technicalEditor from "../../../../docs/technical/editor.md?raw";
|
||||||
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
|
export function DocsTechnicalEditorPage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={technicalEditor}
|
||||||
|
frContent={technicalEditor}
|
||||||
|
meta="04"
|
||||||
|
title="Editor Technical Notes"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { EditorControls } from "@/features/editor/components/EditorControls";
|
import { EditorControls } from "@/components/editor/EditorControls";
|
||||||
import { useEditorHistory } from "@/features/editor/hooks/useEditorHistory";
|
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||||
import { useEditorSceneData } from "@/features/editor/hooks/useEditorSceneData";
|
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||||
import { EditorScene } from "@/features/editor/scene/EditorScene";
|
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||||
import type { MapNode, TransformMode } from "@/types/editor";
|
import type { MapNode, SceneData, TransformMode } from "@/types/editor";
|
||||||
|
|
||||||
|
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
|
||||||
|
|
||||||
|
function serializeMapNodes(sceneData: SceneData): string {
|
||||||
|
return JSON.stringify(sceneData.mapNodes, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorPage(): React.JSX.Element {
|
export function EditorPage(): React.JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -46,7 +52,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
|
|
||||||
const handleSaveToServer = useCallback(async () => {
|
const handleSaveToServer = useCallback(async () => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return;
|
||||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
const json = serializeMapNodes(sceneData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/save-map", {
|
const response = await fetch("/api/save-map", {
|
||||||
@@ -58,17 +64,17 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert("Map enregistrée avec succès!");
|
alert("Map enregistrée avec succès!");
|
||||||
} else {
|
} else {
|
||||||
alert("Erreur lors de l'enregistrement");
|
alert(SAVE_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error saving map:", err);
|
console.error("Error saving map:", err);
|
||||||
alert("Erreur lors de l'enregistrement");
|
alert(SAVE_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
}, [sceneData]);
|
}, [sceneData]);
|
||||||
|
|
||||||
const handleExportJson = useCallback(() => {
|
const handleExportJson = useCallback(() => {
|
||||||
if (!sceneData) return;
|
if (!sceneData) return;
|
||||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
const json = serializeMapNodes(sceneData);
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { Crosshair } from "@/components/ui/Crosshair";
|
||||||
|
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||||
|
import { DebugPerf } from "@/components/debug/DebugPerf";
|
||||||
|
import { World } from "@/world/World";
|
||||||
|
|
||||||
|
export function HomePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<World />
|
||||||
|
<DebugPerf />
|
||||||
|
</Suspense>
|
||||||
|
</Canvas>
|
||||||
|
<Crosshair />
|
||||||
|
<InteractPrompt />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
DocsLanguageContext,
|
||||||
|
type DocsLanguage,
|
||||||
|
} from "@/contexts/docs/DocsLanguageContext";
|
||||||
|
|
||||||
|
interface DocsLanguageProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsLanguageProvider({
|
||||||
|
children,
|
||||||
|
}: DocsLanguageProviderProps): React.JSX.Element {
|
||||||
|
const [language, setLanguage] = useState<DocsLanguage>("en");
|
||||||
|
|
||||||
|
function toggleLanguage(): void {
|
||||||
|
setLanguage((currentLanguage) => (currentLanguage === "en" ? "fr" : "en"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocsLanguageContext.Provider value={{ language, toggleLanguage }}>
|
||||||
|
{children}
|
||||||
|
</DocsLanguageContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Outlet,
|
||||||
|
createRootRoute,
|
||||||
|
createRoute,
|
||||||
|
createRouter,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
|
import { HomePage } from "@/pages/page";
|
||||||
|
import { EditorPage } from "@/pages/editor/page";
|
||||||
|
import {
|
||||||
|
DocsArchitectureRoute,
|
||||||
|
DocsEditorRoute,
|
||||||
|
DocsFeaturesRoute,
|
||||||
|
DocsLayoutRoute,
|
||||||
|
DocsReadmeRoute,
|
||||||
|
DocsTargetArchitectureRoute,
|
||||||
|
DocsTechnicalEditorRoute,
|
||||||
|
} from "@/routes/docs/DocsRouteComponents";
|
||||||
|
|
||||||
|
const rootRoute = createRootRoute({
|
||||||
|
component: Outlet,
|
||||||
|
});
|
||||||
|
|
||||||
|
const indexRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/",
|
||||||
|
component: HomePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/editor",
|
||||||
|
component: EditorPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/docs",
|
||||||
|
component: DocsLayoutRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsChildRoutes = [
|
||||||
|
{ path: "/", component: DocsReadmeRoute },
|
||||||
|
{ path: "architecture", component: DocsArchitectureRoute },
|
||||||
|
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
|
||||||
|
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
||||||
|
{ path: "features", component: DocsFeaturesRoute },
|
||||||
|
{ path: "editor", component: DocsEditorRoute },
|
||||||
|
].map(({ path, component }) =>
|
||||||
|
createRoute({
|
||||||
|
getParentRoute: () => docsRoute,
|
||||||
|
path,
|
||||||
|
component,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([
|
||||||
|
indexRoute,
|
||||||
|
editorRoute,
|
||||||
|
docsRoute.addChildren(docsChildRoutes),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const router = createRouter({ routeTree });
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
|
||||||
|
const LazyDocsLayout = lazy(() =>
|
||||||
|
import("@/components/docs/DocsLayout").then((module) => ({
|
||||||
|
default: module.DocsLayout,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsReadmePage = lazy(() =>
|
||||||
|
import("@/pages/docs/page").then((module) => ({
|
||||||
|
default: module.DocsReadmePage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsArchitecturePage = lazy(() =>
|
||||||
|
import("@/pages/docs/architecture/page").then((module) => ({
|
||||||
|
default: module.DocsArchitecturePage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsTargetArchitecturePage = lazy(() =>
|
||||||
|
import("@/pages/docs/target-architecture/page").then((module) => ({
|
||||||
|
default: module.DocsTargetArchitecturePage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsTechnicalEditorPage = lazy(() =>
|
||||||
|
import("@/pages/docs/technical-editor/page").then((module) => ({
|
||||||
|
default: module.DocsTechnicalEditorPage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsFeaturesPage = lazy(() =>
|
||||||
|
import("@/pages/docs/features/page").then((module) => ({
|
||||||
|
default: module.DocsFeaturesPage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyDocsEditorPage = lazy(() =>
|
||||||
|
import("@/pages/docs/editor/page").then((module) => ({
|
||||||
|
default: module.DocsEditorPage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function DocsLayoutRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsLayout />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsReadmeRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsReadmePage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsArchitectureRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsArchitecturePage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsTargetArchitectureRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsTargetArchitecturePage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsTechnicalEditorRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsTechnicalEditorPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsFeaturesRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsFeaturesPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsEditorRoute(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyDocsEditorPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "./three";
|
||||||
|
|
||||||
export interface MapNode {
|
export interface MapNode {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { MapNode, SceneData } from "@/types/editor";
|
import type { SceneData } from "@/types/editor";
|
||||||
|
import { parseMapNodes } from "@/utils/mapNodeValidation";
|
||||||
|
|
||||||
const MAP_JSON_PATH = "/map.json";
|
const MAP_JSON_PATH = "/map.json";
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ export async function createSceneDataFromFiles(
|
|||||||
throw new Error("Fichier map.json manquant à la racine du dossier");
|
throw new Error("Fichier map.json manquant à la racine du dossier");
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapNodes: MapNode[] = JSON.parse(await mapFile.text());
|
const mapNodes = parseMapNodes(JSON.parse(await mapFile.text()));
|
||||||
const models = new Map<string, string>();
|
const models = new Map<string, string>();
|
||||||
|
|
||||||
for (const [path, file] of fileMap.entries()) {
|
for (const [path, file] of fileMap.entries()) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { MapNode, SceneData } from "@/types/editor";
|
import type { MapNode, SceneData } from "@/types/editor";
|
||||||
|
import { parseMapNodes } from "@/utils/mapNodeValidation";
|
||||||
|
|
||||||
const MAP_JSON_PATH = "/map.json";
|
const MAP_JSON_PATH = "/map.json";
|
||||||
const MODEL_FILE_NAME = "model.gltf";
|
const MODEL_FILE_NAME = "model.gltf";
|
||||||
|
type ModelEntry = [modelName: string, modelUrl: string];
|
||||||
|
|
||||||
export async function loadMapSceneData(): Promise<SceneData | null> {
|
export async function loadMapSceneData(): Promise<SceneData | null> {
|
||||||
const response = await fetch(MAP_JSON_PATH);
|
const response = await fetch(MAP_JSON_PATH);
|
||||||
@@ -10,7 +12,7 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapNodes: MapNode[] = await response.json();
|
const mapNodes = parseMapNodes(await response.json());
|
||||||
return createSceneData(mapNodes);
|
return createSceneData(mapNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +31,8 @@ async function loadMapModelUrls(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(modelUrl, { method: "HEAD" });
|
const response = await fetch(modelUrl, { method: "HEAD" });
|
||||||
return response.ok ? ([modelName, modelUrl] as const) : null;
|
const modelEntry: ModelEntry = [modelName, modelUrl];
|
||||||
|
return response.ok ? modelEntry : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MapNode } from "../types/editor";
|
||||||
|
|
||||||
|
function isVector3Tuple(value: unknown): value is [number, number, number] {
|
||||||
|
return (
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.length === 3 &&
|
||||||
|
value.every((item) => typeof item === "number" && Number.isFinite(item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMapNode(value: unknown): value is MapNode {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = value as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof node.name === "string" &&
|
||||||
|
typeof node.type === "string" &&
|
||||||
|
isVector3Tuple(node.position) &&
|
||||||
|
isVector3Tuple(node.rotation) &&
|
||||||
|
isVector3Tuple(node.scale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMapNodes(value: unknown): MapNode[] {
|
||||||
|
if (!Array.isArray(value) || !value.every(isMapNode)) {
|
||||||
|
throw new Error("Invalid map node data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Environment as DreiEnvironment } from "@react-three/drei";
|
|||||||
import {
|
import {
|
||||||
GAME_SCENE_SKYBOX_PATH,
|
GAME_SCENE_SKYBOX_PATH,
|
||||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||||
} from "@/data/environmentConfig";
|
} from "@/data/world/environmentConfig";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
|
|
||||||
export function Environment(): React.JSX.Element {
|
export function Environment(): React.JSX.Element {
|
||||||
|
|||||||
+16
-9
@@ -3,7 +3,7 @@ import { useGLTF } from "@react-three/drei";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
import { loadMapSceneData } from "@/utils/loadMapSceneData";
|
import { loadMapSceneData } from "@/utils/loadMapSceneData";
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
import type { OctreeReadyHandler } from "@/types/three";
|
||||||
import type { MapNode } from "@/types/editor";
|
import type { MapNode } from "@/types/editor";
|
||||||
|
|
||||||
interface GameMapProps {
|
interface GameMapProps {
|
||||||
@@ -15,7 +15,7 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
useOctreeGraphNode(groupRef, onOctreeReady);
|
useOctreeGraphNode(groupRef, onOctreeReady, mapNodes.length);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMap = async () => {
|
const loadMap = async () => {
|
||||||
@@ -27,9 +27,19 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapNodes(
|
const loadedMapNodes = sceneData.mapNodes.filter((node) =>
|
||||||
sceneData.mapNodes.filter((node) => sceneData.models.has(node.name)),
|
sceneData.models.has(node.name),
|
||||||
);
|
);
|
||||||
|
const missingModelCount =
|
||||||
|
sceneData.mapNodes.length - loadedMapNodes.length;
|
||||||
|
|
||||||
|
if (missingModelCount > 0) {
|
||||||
|
console.warn(
|
||||||
|
`${missingModelCount} map nodes were skipped because their model files are missing.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapNodes(loadedMapNodes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading map:", error);
|
console.error("Error loading map:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -40,13 +50,10 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
loadMap();
|
loadMap();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
{mapNodes.map((node, index) => (
|
{!isLoading &&
|
||||||
|
mapNodes.map((node, index) => (
|
||||||
<ModelInstance key={index} node={node} />
|
<ModelInstance key={index} node={node} />
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
SUN_Z_MAX,
|
SUN_Z_MAX,
|
||||||
SUN_Z_MIN,
|
SUN_Z_MIN,
|
||||||
SUN_Z_STEP,
|
SUN_Z_STEP,
|
||||||
} from "@/data/lightingConfig";
|
} from "@/data/world/lightingConfig";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
|
||||||
type LightingState = {
|
type LightingState = {
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { useThree } from "@react-three/fiber";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig";
|
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
|
||||||
|
|
||||||
const MAP_PATH = "/models/map/model.gltf";
|
|
||||||
|
|
||||||
interface MapProps {
|
|
||||||
onOctreeReady: OctreeReadyHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
|
||||||
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
|
||||||
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
|
||||||
const { scene } = useThree();
|
|
||||||
|
|
||||||
useOctreeGraphNode(groupRef, onOctreeReady);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const debug = Debug.getInstance();
|
|
||||||
if (!debug.active || !groupRef.current) return;
|
|
||||||
|
|
||||||
const helpers: THREE.BoxHelper[] = [];
|
|
||||||
|
|
||||||
groupRef.current.traverse((child) => {
|
|
||||||
if (!(child instanceof THREE.Mesh)) return;
|
|
||||||
const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR);
|
|
||||||
scene.add(helper);
|
|
||||||
helpers.push(helper);
|
|
||||||
});
|
|
||||||
|
|
||||||
boxHelpersRef.current = helpers;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
helpers.forEach((h) => {
|
|
||||||
scene.remove(h);
|
|
||||||
h.dispose();
|
|
||||||
});
|
|
||||||
boxHelpersRef.current = [];
|
|
||||||
};
|
|
||||||
}, [scene]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group ref={groupRef}>
|
|
||||||
<primitive object={gltfScene} />
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useGLTF.preload(MAP_PATH);
|
|
||||||
+5
-5
@@ -3,15 +3,15 @@ import type { Octree } from "three/addons/math/Octree.js";
|
|||||||
import {
|
import {
|
||||||
PLAYER_SPAWN_POSITION_GAME,
|
PLAYER_SPAWN_POSITION_GAME,
|
||||||
PLAYER_SPAWN_POSITION_PHYSICS,
|
PLAYER_SPAWN_POSITION_PHYSICS,
|
||||||
} from "@/data/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
|
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
|
||||||
import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers";
|
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
|
||||||
import { Environment } from "@/world/Environment";
|
import { Environment } from "@/world/Environment";
|
||||||
import { Lighting } from "@/world/Lighting";
|
import { Lighting } from "@/world/Lighting";
|
||||||
import { GameMap } from "@/world/GameMap";
|
import { GameMap } from "@/world/GameMap";
|
||||||
import { PlayerComponent } from "@/world/player/PlayerComponent";
|
import { Player } from "@/world/player/Player";
|
||||||
import { TestScene } from "@/world/debug/TestScene";
|
import { TestScene } from "@/world/debug/TestScene";
|
||||||
|
|
||||||
export function World(): React.JSX.Element {
|
export function World(): React.JSX.Element {
|
||||||
@@ -37,7 +37,7 @@ export function World(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{cameraMode !== "debug" ? (
|
{cameraMode !== "debug" ? (
|
||||||
<PlayerComponent octree={octree} spawnPosition={playerSpawnPosition} />
|
<Player octree={octree} spawnPosition={playerSpawnPosition} />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { GrabbableObject } from "@/components/3d/GrabbableObject";
|
import { GrabbableObject } from "@/components/three/GrabbableObject";
|
||||||
import { TriggerObject } from "@/components/3d/TriggerObject";
|
import { TriggerObject } from "@/components/three/TriggerObject";
|
||||||
import {
|
import {
|
||||||
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
||||||
TEST_SCENE_FLOOR_POSITION,
|
TEST_SCENE_FLOOR_POSITION,
|
||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
TEST_SCENE_TRIGGER_ROUGHNESS,
|
TEST_SCENE_TRIGGER_ROUGHNESS,
|
||||||
TEST_SCENE_TRIGGER_SEGMENTS,
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
TEST_SCENE_TRIGGER_SOUND_PATH,
|
TEST_SCENE_TRIGGER_SOUND_PATH,
|
||||||
} from "@/data/testSceneConfig";
|
} from "@/data/debug/testSceneConfig";
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
import type { OctreeReadyHandler } from "@/types/3d";
|
import type { OctreeReadyHandler } from "@/types/three";
|
||||||
|
|
||||||
interface TestSceneProps {
|
interface TestSceneProps {
|
||||||
onOctreeReady: OctreeReadyHandler;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import type { Octree } from "three/addons/math/Octree.js";
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
import { PlayerCamera } from "@/world/player/PlayerCamera";
|
import { PlayerCamera } from "@/world/player/PlayerCamera";
|
||||||
import { PlayerController } from "@/world/player/PlayerController";
|
import { PlayerController } from "@/world/player/PlayerController";
|
||||||
|
|
||||||
interface PlayerComponentProps {
|
interface PlayerProps {
|
||||||
octree: Octree | null;
|
octree: Octree | null;
|
||||||
spawnPosition: Vector3Tuple;
|
spawnPosition: Vector3Tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlayerComponent({
|
export function Player({
|
||||||
spawnPosition,
|
spawnPosition,
|
||||||
octree,
|
octree,
|
||||||
}: PlayerComponentProps): React.JSX.Element {
|
}: PlayerProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
MOVE_LEFT_KEY,
|
MOVE_LEFT_KEY,
|
||||||
MOVE_RIGHT_KEY,
|
MOVE_RIGHT_KEY,
|
||||||
PRIMARY_INTERACT_MOUSE_BUTTON,
|
PRIMARY_INTERACT_MOUSE_BUTTON,
|
||||||
} from "@/data/keybindings";
|
} from "@/data/input/keybindings";
|
||||||
import {
|
import {
|
||||||
PLAYER_ACCELERATION_MULTIPLIER,
|
PLAYER_ACCELERATION_MULTIPLIER,
|
||||||
PLAYER_AIR_CONTROL_FACTOR,
|
PLAYER_AIR_CONTROL_FACTOR,
|
||||||
@@ -22,9 +22,9 @@ import {
|
|||||||
PLAYER_MAX_DELTA,
|
PLAYER_MAX_DELTA,
|
||||||
PLAYER_WALK_SPEED,
|
PLAYER_WALK_SPEED,
|
||||||
PLAYER_XZ_DAMPING_FACTOR,
|
PLAYER_XZ_DAMPING_FACTOR,
|
||||||
} from "@/data/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
import { InteractionManager } from "@/managers/InteractionManager";
|
||||||
import type { Vector3Tuple } from "@/types/3d";
|
import type { Vector3Tuple } from "@/types/three";
|
||||||
|
|
||||||
type Keys = {
|
type Keys = {
|
||||||
forward: boolean;
|
forward: boolean;
|
||||||
@@ -54,6 +54,25 @@ const _up = new THREE.Vector3(0, 1, 0);
|
|||||||
const _translateVec = new THREE.Vector3();
|
const _translateVec = new THREE.Vector3();
|
||||||
const _collisionCorrection = new THREE.Vector3();
|
const _collisionCorrection = new THREE.Vector3();
|
||||||
|
|
||||||
|
function setMovementKey(keys: Keys, key: string, pressed: boolean): boolean {
|
||||||
|
switch (key.toLowerCase()) {
|
||||||
|
case MOVE_FORWARD_KEY:
|
||||||
|
keys.forward = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_BACKWARD_KEY:
|
||||||
|
keys.backward = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_LEFT_KEY:
|
||||||
|
keys.left = pressed;
|
||||||
|
return true;
|
||||||
|
case MOVE_RIGHT_KEY:
|
||||||
|
keys.right = pressed;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function PlayerController({
|
export function PlayerController({
|
||||||
octree,
|
octree,
|
||||||
spawnPosition,
|
spawnPosition,
|
||||||
@@ -89,51 +108,29 @@ export function PlayerController({
|
|||||||
const interaction = InteractionManager.getInstance();
|
const interaction = InteractionManager.getInstance();
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
if (setMovementKey(keys.current, event.key, true)) {
|
||||||
case MOVE_FORWARD_KEY:
|
event.preventDefault();
|
||||||
keys.current.forward = true;
|
return;
|
||||||
break;
|
}
|
||||||
case MOVE_BACKWARD_KEY:
|
|
||||||
keys.current.backward = true;
|
if (event.key === JUMP_KEY) {
|
||||||
break;
|
|
||||||
case MOVE_LEFT_KEY:
|
|
||||||
keys.current.left = true;
|
|
||||||
break;
|
|
||||||
case MOVE_RIGHT_KEY:
|
|
||||||
keys.current.right = true;
|
|
||||||
break;
|
|
||||||
case JUMP_KEY:
|
|
||||||
wantsJump.current = true;
|
wantsJump.current = true;
|
||||||
break;
|
event.preventDefault();
|
||||||
case INTERACT_KEY:
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.toLowerCase() === INTERACT_KEY) {
|
||||||
if (interaction.getState().focused?.kind === "trigger") {
|
if (interaction.getState().focused?.kind === "trigger") {
|
||||||
interaction.pressInteract();
|
interaction.pressInteract();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent): void => {
|
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
if (setMovementKey(keys.current, event.key, false)) {
|
||||||
case MOVE_FORWARD_KEY:
|
|
||||||
keys.current.forward = false;
|
|
||||||
break;
|
|
||||||
case MOVE_BACKWARD_KEY:
|
|
||||||
keys.current.backward = false;
|
|
||||||
break;
|
|
||||||
case MOVE_LEFT_KEY:
|
|
||||||
keys.current.left = false;
|
|
||||||
break;
|
|
||||||
case MOVE_RIGHT_KEY:
|
|
||||||
keys.current.right = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent): void => {
|
const handleMouseDown = (event: MouseEvent): void => {
|
||||||
|
|||||||
+4
-29
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { ServerResponse } from "node:http";
|
import type { ServerResponse } from "node:http";
|
||||||
import type { Plugin } from "vite";
|
import type { Plugin } from "vite";
|
||||||
|
import { parseMapNodes } from "./src/utils/mapNodeValidation";
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
|
||||||
@@ -22,34 +23,6 @@ function sendJson(
|
|||||||
.end(JSON.stringify(body));
|
.end(JSON.stringify(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVector3(value: unknown): value is [number, number, number] {
|
|
||||||
return (
|
|
||||||
Array.isArray(value) &&
|
|
||||||
value.length === 3 &&
|
|
||||||
value.every((item) => typeof item === "number" && Number.isFinite(item))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMapNode(value: unknown): value is Record<string, unknown> {
|
|
||||||
if (typeof value !== "object" || value === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = value as Record<string, unknown>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
typeof node.name === "string" &&
|
|
||||||
typeof node.type === "string" &&
|
|
||||||
isVector3(node.position) &&
|
|
||||||
isVector3(node.rotation) &&
|
|
||||||
isVector3(node.scale)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMapPayload(value: unknown): boolean {
|
|
||||||
return Array.isArray(value) && value.every(isMapNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMapPlugin = (): Plugin => ({
|
const saveMapPlugin = (): Plugin => ({
|
||||||
name: "save-map-api",
|
name: "save-map-api",
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
@@ -75,7 +48,9 @@ const saveMapPlugin = (): Plugin => ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(Buffer.concat(chunks).toString());
|
const data = JSON.parse(Buffer.concat(chunks).toString());
|
||||||
if (!isMapPayload(data)) {
|
try {
|
||||||
|
parseMapNodes(data);
|
||||||
|
} catch {
|
||||||
sendJson(res, 400, { error: "Invalid map payload" });
|
sendJson(res, 400, { error: "Invalid map payload" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user