diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index 61ec7b2..f502ff1 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -57,7 +57,7 @@ This document describes the code that exists today in the repository. ## Map Data - `public/map.json` is expected to be a `MapNode[]`. -- Each map node `name` maps to `public/models/{name}/model.gltf`. +- Each map node `name` maps to `public/models/{name}/model.glb` when available, with `public/models/{name}/model.gltf` kept as fallback. - The editor renders a fallback cube for missing models. - The game scene filters out nodes whose model cannot be resolved. diff --git a/docs/technical/editor.md b/docs/technical/editor.md index fd0e8cb..c927047 100644 --- a/docs/technical/editor.md +++ b/docs/technical/editor.md @@ -57,7 +57,7 @@ src/ `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.glb` files first, then falls back to `public/models/{name}/model.gltf`. `src/utils/editor/loadEditorScene.ts` contains editor-only upload handling for user-selected folders. @@ -96,10 +96,10 @@ public/ ├── map.json └── models/ └── pylone/ - └── model.gltf + └── model.glb ``` -If a model is missing, the editor renders a fallback cube so the node can still be selected and transformed. +If `model.glb` and `model.gltf` are both missing, the editor renders a fallback cube so the node can still be selected and transformed. ## Editor Flow diff --git a/docs/user/editor.md b/docs/user/editor.md index 1c92b74..d98ba2a 100644 --- a/docs/user/editor.md +++ b/docs/user/editor.md @@ -9,7 +9,7 @@ Use the editor when you need to move, rotate, or scale existing map objects with 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. +- `public/models/{name}/model.glb` contains the matching 3D model for each object name. `model.gltf` is still supported as a fallback during migration. - Missing models are displayed as gray fallback cubes, so incomplete maps remain editable. ## Map Node Format diff --git a/docs/user/features.md b/docs/user/features.md index a04fb80..ddf5212 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -5,7 +5,7 @@ This document lists features that are implemented in the current codebase. ## Scene - Fullscreen React Three Fiber scene -- Main map scene loaded from `public/map.json` and matching `public/models/{name}/model.gltf` assets +- Main map scene loaded from `public/map.json` and matching `public/models/{name}/model.glb` or `model.gltf` assets - Debug physics test scene selectable from the debug panel - Ambient and directional lighting - Environment background setup @@ -43,7 +43,7 @@ This document lists features that are implemented in the current codebase. - `/editor` route for inspecting and editing `public/map.json` - Automatic loading of `public/map.json` when available - Folder upload fallback when `map.json` is missing -- Rendering of available `public/models/{name}/model.gltf` assets +- Rendering of available `public/models/{name}/model.glb` or `model.gltf` assets - Fallback cubes for nodes whose model is missing - Object selection by click - Transform modes for translate, rotate, and scale diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts index 82d45ec..91a3f4f 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -214,7 +214,7 @@ 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\` +- Carte principale chargée depuis \`public/models/{name}/model.glb\`, avec fallback vers \`model.gltf\` - Scène de test physique debug sélectionnable depuis le panneau debug - Éclairage ambiant et directionnel - Configuration de l'environnement de fond @@ -268,7 +268,7 @@ 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" +- "name" : nom du dossier modèle dans "/public/models/{name}/model.glb", avec fallback vers "model.gltf" - "type" : catégorie de l'objet - "position" : "[x, y, z]" - "rotation" : "[x, y, z]" diff --git a/src/pages/editor/page.tsx b/src/pages/editor/page.tsx index e06cc51..c60e93b 100644 --- a/src/pages/editor/page.tsx +++ b/src/pages/editor/page.tsx @@ -138,7 +138,7 @@ export function EditorPage(): React.JSX.Element {

Structure requise :

                 public/ ├── map.json (à la racine) └── models/
-                ├── arbre/ │ └── model.gltf ├── building/ │ └── model.gltf └──
+                ├── arbre/ │ └── model.glb ├── building/ │ └── model.gltf └──
                 ...
               
diff --git a/src/utils/editor/loadEditorScene.ts b/src/utils/editor/loadEditorScene.ts index 62e8e4c..e22b36f 100644 --- a/src/utils/editor/loadEditorScene.ts +++ b/src/utils/editor/loadEditorScene.ts @@ -21,9 +21,12 @@ export async function createSceneDataFromFiles( const models = new Map(); for (const [path, file] of fileMap.entries()) { - const modelMatch = path.match(/^\/models\/(.+)\/model\.gltf$/); - if (modelMatch?.[1]) { - models.set(modelMatch[1], URL.createObjectURL(file)); + const modelMatch = path.match(/^\/models\/(.+)\/model\.(glb|gltf)$/); + const modelName = modelMatch?.[1]; + const modelExtension = modelMatch?.[2]; + + if (modelName && (modelExtension === "glb" || !models.has(modelName))) { + models.set(modelName, URL.createObjectURL(file)); } } diff --git a/src/utils/loadMapSceneData.ts b/src/utils/loadMapSceneData.ts index 27ef6f8..da2464b 100644 --- a/src/utils/loadMapSceneData.ts +++ b/src/utils/loadMapSceneData.ts @@ -2,7 +2,7 @@ import type { MapNode, SceneData } from "@/types/editor"; import { parseMapNodes } from "@/utils/mapNodeValidation"; const MAP_JSON_PATH = "/map.json"; -const MODEL_FILE_NAME = "model.gltf"; +const MODEL_FILE_NAMES = ["model.glb", "model.gltf"]; const HTML_CONTENT_TYPE = "text/html"; type ModelEntry = [modelName: string, modelUrl: string]; @@ -27,21 +27,26 @@ async function loadMapModelUrls( ): Promise> { const uniqueModelNames = [...new Set(mapNodes.map((node) => node.name))]; const modelEntries = await Promise.all( - uniqueModelNames.map(async (modelName) => { - const modelUrl = `/models/${modelName}/${MODEL_FILE_NAME}`; - - try { - const response = await fetch(modelUrl, { method: "HEAD" }); - const contentType = response.headers.get("content-type") ?? ""; - const modelEntry: ModelEntry = [modelName, modelUrl]; - return response.ok && !contentType.includes(HTML_CONTENT_TYPE) - ? modelEntry - : null; - } catch { - return null; - } - }), + uniqueModelNames.map((modelName) => loadModelEntry(modelName)), ); return new Map(modelEntries.filter((entry) => entry !== null)); } + +async function loadModelEntry(modelName: string): Promise { + for (const fileName of MODEL_FILE_NAMES) { + const modelUrl = `/models/${modelName}/${fileName}`; + + try { + const response = await fetch(modelUrl, { method: "HEAD" }); + const contentType = response.headers.get("content-type") ?? ""; + if (response.ok && !contentType.includes(HTML_CONTENT_TYPE)) { + return [modelName, modelUrl]; + } + } catch { + continue; + } + } + + return null; +} diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index dd7faaa..fe90bd4 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -6,12 +6,17 @@ import { loadMapSceneData } from "@/utils/loadMapSceneData"; import type { OctreeReadyHandler } from "@/types/three"; import type { MapNode } from "@/types/editor"; +interface LoadedMapNode { + node: MapNode; + modelUrl: string; +} + interface GameMapProps { onOctreeReady: OctreeReadyHandler; } export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element { - const [mapNodes, setMapNodes] = useState([]); + const [mapNodes, setMapNodes] = useState([]); const [isLoading, setIsLoading] = useState(true); const groupRef = useRef(null); @@ -27,9 +32,10 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element { return; } - const loadedMapNodes = sceneData.mapNodes.filter((node) => - sceneData.models.has(node.name), - ); + const loadedMapNodes = sceneData.mapNodes.flatMap((node) => { + const modelUrl = sceneData.models.get(node.name); + return modelUrl ? [{ node, modelUrl }] : []; + }); const missingModelCount = sceneData.mapNodes.length - loadedMapNodes.length; @@ -54,16 +60,25 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element { {!isLoading && mapNodes.map((node, index) => ( - + ))} ); } -function ModelInstance({ node }: { node: MapNode }): React.JSX.Element { - const modelPath = `/models/${node.name}/model.gltf`; +function ModelInstance({ + node, + modelUrl, +}: { + node: MapNode; + modelUrl: string; +}): React.JSX.Element { const groupRef = useRef(null); - const { scene } = useGLTF(modelPath); + const { scene } = useGLTF(modelUrl); const sceneInstance = useMemo(() => scene.clone(true), [scene]); const { position, rotation, scale } = node;