From e4ee2d768b86070331d1e56b00fd1d491274b214 Mon Sep 17 00:00:00 2001 From: tom-boullay Date: Thu, 21 May 2026 15:38:23 +0200 Subject: [PATCH] feat(debug): add map performance visibility controls --- src/hooks/debug/useMapPerformanceDebug.ts | 58 +++++++ src/managers/stores/useMapPerformanceStore.ts | 145 ++++++++++++++++++ src/world/Environment.tsx | 11 +- src/world/GameMap.tsx | 32 +++- src/world/World.tsx | 3 + .../map-instancing/MapInstancingSystem.tsx | 9 +- src/world/vegetation/VegetationSystem.tsx | 9 +- 7 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 src/hooks/debug/useMapPerformanceDebug.ts create mode 100644 src/managers/stores/useMapPerformanceStore.ts diff --git a/src/hooks/debug/useMapPerformanceDebug.ts b/src/hooks/debug/useMapPerformanceDebug.ts new file mode 100644 index 0000000..74cfc78 --- /dev/null +++ b/src/hooks/debug/useMapPerformanceDebug.ts @@ -0,0 +1,58 @@ +import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; +import { + MAP_PERFORMANCE_GROUP_NAMES, + MAP_PERFORMANCE_MODEL_NAMES, + useMapPerformanceStore, +} from "@/managers/stores/useMapPerformanceStore"; + +function toLabel(value: string): string { + return value + .split(/[-_\s]+/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +export function useMapPerformanceDebug(): void { + useDebugFolder("Performance / Map", (folder) => { + const { + groups, + models, + setGroupVisible, + setModelVisible, + resetVisibility, + } = useMapPerformanceStore.getState(); + const controls = { + ...groups, + ...models, + reset: () => { + resetVisibility(); + for (const key of [ + ...MAP_PERFORMANCE_GROUP_NAMES, + ...MAP_PERFORMANCE_MODEL_NAMES, + ]) { + controls[key] = true; + } + folder.controllersRecursive().forEach((controller) => { + controller.updateDisplay(); + }); + }, + }; + + for (const group of MAP_PERFORMANCE_GROUP_NAMES) { + folder + .add(controls, group) + .name(toLabel(group)) + .onChange((visible: boolean) => setGroupVisible(group, visible)); + } + + for (const model of MAP_PERFORMANCE_MODEL_NAMES) { + folder + .add(controls, model) + .name(toLabel(model)) + .onChange((visible: boolean) => setModelVisible(model, visible)); + } + + folder.add(controls, "reset").name("Reset visibility"); + }); +} diff --git a/src/managers/stores/useMapPerformanceStore.ts b/src/managers/stores/useMapPerformanceStore.ts new file mode 100644 index 0000000..a0797bb --- /dev/null +++ b/src/managers/stores/useMapPerformanceStore.ts @@ -0,0 +1,145 @@ +import { create } from "zustand"; + +export type MapPerformanceGroupName = + | "vegetation" + | "crops" + | "trees" + | "buildings" + | "landmarks" + | "props" + | "terrain" + | "sky"; + +export type MapPerformanceModelName = + | "buisson" + | "arbre" + | "sapin" + | "champdeble" + | "champdesoja" + | "champsdetournesol" + | "ecole" + | "generateur" + | "fermeverticale" + | "lafabrik" + | "immeuble1" + | "eolienne" + | "pylone" + | "boiteauxlettres" + | "maison1" + | "parcebike" + | "terrain" + | "sky"; + +export interface MapPerformanceVisibility { + groups: Record; + models: Record; +} + +interface MapPerformanceActions { + setGroupVisible: (group: MapPerformanceGroupName, visible: boolean) => void; + setModelVisible: (model: MapPerformanceModelName, visible: boolean) => void; + resetVisibility: () => void; +} + +type MapPerformanceStore = MapPerformanceVisibility & MapPerformanceActions; + +export const MAP_PERFORMANCE_GROUP_NAMES: readonly MapPerformanceGroupName[] = [ + "vegetation", + "crops", + "trees", + "buildings", + "landmarks", + "props", + "terrain", + "sky", +]; + +export const MAP_PERFORMANCE_MODEL_NAMES: readonly MapPerformanceModelName[] = [ + "buisson", + "arbre", + "sapin", + "champdeble", + "champdesoja", + "champsdetournesol", + "ecole", + "generateur", + "fermeverticale", + "lafabrik", + "immeuble1", + "eolienne", + "pylone", + "boiteauxlettres", + "maison1", + "parcebike", + "terrain", + "sky", +]; + +const MODEL_GROUPS: Record< + MapPerformanceModelName, + readonly MapPerformanceGroupName[] +> = { + buisson: ["vegetation"], + arbre: ["vegetation", "trees"], + sapin: ["vegetation", "trees"], + champdeble: ["vegetation", "crops"], + champdesoja: ["vegetation", "crops"], + champsdetournesol: ["vegetation", "crops"], + ecole: ["buildings", "landmarks"], + generateur: ["landmarks"], + fermeverticale: ["buildings", "landmarks"], + lafabrik: ["buildings", "landmarks"], + immeuble1: ["buildings"], + eolienne: ["props"], + pylone: ["props"], + boiteauxlettres: ["props"], + maison1: ["buildings"], + parcebike: ["props"], + terrain: ["terrain"], + sky: ["sky"], +}; + +function createVisibleRecord( + keys: readonly T[], +): Record { + return Object.fromEntries(keys.map((key) => [key, true])) as Record< + T, + boolean + >; +} + +function createDefaultVisibility(): MapPerformanceVisibility { + return { + groups: createVisibleRecord(MAP_PERFORMANCE_GROUP_NAMES), + models: createVisibleRecord(MAP_PERFORMANCE_MODEL_NAMES), + }; +} + +export function isMapPerformanceModelName( + name: string, +): name is MapPerformanceModelName { + return MAP_PERFORMANCE_MODEL_NAMES.includes(name as MapPerformanceModelName); +} + +export function isMapModelVisible( + name: string, + visibility: MapPerformanceVisibility, +): boolean { + if (!isMapPerformanceModelName(name)) return true; + if (!visibility.models[name]) return false; + + return MODEL_GROUPS[name].every((group) => visibility.groups[group]); +} + +export const useMapPerformanceStore = create()((set) => ({ + ...createDefaultVisibility(), + setGroupVisible: (group, visible) => + set((state) => ({ + groups: { ...state.groups, [group]: visible }, + })), + setModelVisible: (model, visible) => + set((state) => ({ + models: { ...state.models, [model]: visible }, + })), + resetVisibility: () => set(createDefaultVisibility()), +})); diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index ab9d53d..d1ffeb1 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -7,10 +7,17 @@ import { PHYSICS_SCENE_BACKGROUND_COLOR, } from "@/data/world/environmentConfig"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; +import { + isMapModelVisible, + useMapPerformanceStore, +} from "@/managers/stores/useMapPerformanceStore"; import { SkyModel } from "@/components/three/world/SkyModel"; export function Environment(): React.JSX.Element { const sceneMode = useSceneMode(); + const groups = useMapPerformanceStore((state) => state.groups); + const models = useMapPerformanceStore((state) => state.models); + const showSky = isMapModelVisible("sky", { groups, models }); if (sceneMode === "physics") { return ( @@ -18,7 +25,7 @@ export function Environment(): React.JSX.Element { ); } - return ( + return showSky ? ( + ) : ( + ); } diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index 21335a5..03c842c 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -11,6 +11,10 @@ import * as THREE from "three"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { TerrainModel } from "@/components/three/world/TerrainModel"; +import { + isMapModelVisible, + useMapPerformanceStore, +} from "@/managers/stores/useMapPerformanceStore"; import { GameMapCollision } from "@/world/GameMapCollision"; import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance"; import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig"; @@ -101,6 +105,8 @@ export function GameMap({ onOctreeReady, }: GameMapProps): React.JSX.Element { const settledMapNodesRef = useRef(new Set()); + const groups = useMapPerformanceStore((state) => state.groups); + const models = useMapPerformanceStore((state) => state.models); const [mapNodes, setMapNodes] = useState([]); const [mapLoaded, setMapLoaded] = useState(false); const [settledMapNodeCount, setSettledMapNodeCount] = useState(0); @@ -221,17 +227,23 @@ export function GameMap({ node={mapNode.node} onSettled={() => handleMapNodeSettled(index)} > - handleMapNodeSettled(index)} - /> + {isMapModelVisible(mapNode.node.name, { groups, models }) ? ( + handleMapNodeSettled(index)} + /> + ) : ( + handleMapNodeSettled(index)} /> + )} ))} - + {isMapModelVisible("terrain", { groups, models }) ? ( + + ) : null} void }): null { + useEffect(() => { + onSettled(); + }, [onSettled]); + + return null; +} + /** * Temporary development-only map reducer. * diff --git a/src/world/World.tsx b/src/world/World.tsx index bad4fc2..7067784 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -5,6 +5,7 @@ import { PLAYER_SPAWN_POSITION_PHYSICS, } from "@/data/player/playerConfig"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; +import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; @@ -35,6 +36,8 @@ interface WorldProps { } export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { + useMapPerformanceDebug(); + const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); const mainState = useGameStore((state) => state.mainState); diff --git a/src/world/map-instancing/MapInstancingSystem.tsx b/src/world/map-instancing/MapInstancingSystem.tsx index f0c4329..b7f710f 100644 --- a/src/world/map-instancing/MapInstancingSystem.tsx +++ b/src/world/map-instancing/MapInstancingSystem.tsx @@ -1,4 +1,8 @@ import { Suspense } from "react"; +import { + isMapModelVisible, + useMapPerformanceStore, +} from "@/managers/stores/useMapPerformanceStore"; import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset"; import { MAP_INSTANCING_ASSETS, @@ -7,6 +11,8 @@ import { import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData"; export function MapInstancingSystem(): React.JSX.Element | null { + const groups = useMapPerformanceStore((state) => state.groups); + const models = useMapPerformanceStore((state) => state.models); const { data, isLoading } = useMapInstancingData(); if (isLoading || !data) { @@ -14,7 +20,8 @@ export function MapInstancingSystem(): React.JSX.Element | null { } const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter( - ([, config]) => config.enabled, + ([, config]) => + config.enabled && isMapModelVisible(config.mapName, { groups, models }), ); return ( diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx index f5bb962..ebe158c 100644 --- a/src/world/vegetation/VegetationSystem.tsx +++ b/src/world/vegetation/VegetationSystem.tsx @@ -1,4 +1,8 @@ import { Suspense } from "react"; +import { + isMapModelVisible, + useMapPerformanceStore, +} from "@/managers/stores/useMapPerformanceStore"; import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation"; import { useVegetationData } from "@/world/vegetation/useVegetationData"; import { @@ -7,6 +11,8 @@ import { } from "@/world/vegetation/vegetationConfig"; export function VegetationSystem(): React.JSX.Element | null { + const groups = useMapPerformanceStore((state) => state.groups); + const models = useMapPerformanceStore((state) => state.models); const { data, isLoading } = useVegetationData(); if (isLoading || !data) { @@ -14,7 +20,8 @@ export function VegetationSystem(): React.JSX.Element | null { } const enabledTypes = Object.entries(VEGETATION_TYPES).filter( - ([, config]) => config.enabled, + ([, config]) => + config.enabled && isMapModelVisible(config.mapName, { groups, models }), ); return (