refactor: nettoie l'architecture monde et les docs

This commit is contained in:
tom-boullay
2026-05-28 15:47:16 +02:00
parent 1a91b1d7ae
commit ba50224e6e
45 changed files with 89 additions and 726 deletions
+2 -2
View File
@@ -6,14 +6,14 @@ interface DocsDocumentProps {
title: string;
meta: string;
content: string;
frContent: string;
frContent?: string;
}
export function DocsDocument({
title,
meta,
content,
frContent,
frContent = content,
}: DocsDocumentProps): React.JSX.Element {
const { language, toggleLanguage } = useDocsLanguage();
const hasAlternateContent = frContent !== content;
+8 -2
View File
@@ -496,10 +496,16 @@ export function EditorSrtPanel(): React.JSX.Element {
setContent(await response.text());
setStatus(`Charge depuis ${srtPath}`);
})
.catch(() => {
.catch((error: unknown) => {
if (!mounted) return;
setContent(srtTemplate);
setStatus("Erreur de chargement, template local cree");
setStatus(
`Erreur de chargement, template local cree: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
);
logger.warn("EditorSrt", "Falling back to local SRT template", {
srtPath,
error: error instanceof Error ? error : String(error),
});
});
return () => {
+5 -10
View File
@@ -1,17 +1,12 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
import {
MergedStaticMapModel,
type MergedStaticMapModelProps,
} from "@/components/three/world/MergedStaticMapModel";
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
interface EcoleModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
type EcoleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
@@ -1,17 +1,12 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
import {
MergedStaticMapModel,
type MergedStaticMapModelProps,
} from "@/components/three/world/MergedStaticMapModel";
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
interface FermeVerticaleModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
type FermeVerticaleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
export function FermeVerticaleModel(
props: FermeVerticaleModelProps,
+5 -10
View File
@@ -1,17 +1,12 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
import {
MergedStaticMapModel,
type MergedStaticMapModelProps,
} from "@/components/three/world/MergedStaticMapModel";
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
interface GenerateurModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
type GenerateurModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
export function GenerateurModel(
props: GenerateurModelProps,
+5 -10
View File
@@ -1,17 +1,12 @@
import { useGLTF } from "@react-three/drei";
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
import type { Vector3Tuple } from "@/types/three/three";
import {
MergedStaticMapModel,
type MergedStaticMapModelProps,
} from "@/components/three/world/MergedStaticMapModel";
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
interface LafabrikModelProps {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
castShadow?: boolean;
receiveShadow?: boolean;
onLoaded?: () => void;
}
type LafabrikModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
return <MergedStaticMapModel modelPath={LAFABRIK_MODEL_PATH} {...props} />;
@@ -6,7 +6,7 @@ import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import type { Vector3Tuple } from "@/types/three/three";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
interface MergedStaticMapModelProps {
export interface MergedStaticMapModelProps {
modelPath: string;
position: Vector3Tuple;
rotation: Vector3Tuple;
+1 -14
View File
@@ -7,8 +7,6 @@ import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
interface SkyModelProps {
modelPath: string;
fallbackColor?: string | undefined;
fallbackModelPath?: string | undefined;
fallbackScale?: number | undefined;
scale?: number | undefined;
}
@@ -29,7 +27,6 @@ interface SkyModelErrorBoundaryState {
const SKY_MODEL_SCALE = 1;
const SKY_MODEL_RENDER_ORDER = -1000;
const SKYBOX_MODEL_PATH = "/models/skybox/model.gltf";
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
class SkyModelErrorBoundary extends Component<
SkyModelErrorBoundaryProps,
@@ -55,21 +52,12 @@ class SkyModelErrorBoundary extends Component<
export function SkyModel({
fallbackColor,
fallbackModelPath,
fallbackScale = SKY_MODEL_SCALE,
modelPath,
scale = SKY_MODEL_SCALE,
}: SkyModelProps): React.JSX.Element {
const colorFallback = fallbackColor ? (
const fallback = fallbackColor ? (
<color attach="background" args={[fallbackColor]} />
) : null;
const fallback = fallbackModelPath ? (
<SkyModelErrorBoundary key={fallbackModelPath} fallback={colorFallback}>
<SkyModelContent modelPath={fallbackModelPath} scale={fallbackScale} />
</SkyModelErrorBoundary>
) : (
colorFallback
);
return (
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
@@ -154,4 +142,3 @@ function disposeSkyModelMaterials(model: THREE.Object3D): void {
}
useGLTF.preload(SKYBOX_MODEL_PATH);
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
+2 -2
View File
@@ -1,5 +1,3 @@
import type { AudioCategory } from "@/managers/AudioManager";
export const AUDIO_PATHS = {
intro: "/sounds/effect/fa.mp3",
bienvenue: "/sounds/effect/fa.mp3",
@@ -8,6 +6,8 @@ export const AUDIO_PATHS = {
helped: "/sounds/effect/fa.mp3",
} as const;
export type AudioCategory = "music" | "sfx" | "dialogue";
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
music: 1,
sfx: 1,
-2
View File
@@ -1,6 +1,4 @@
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
export const GAME_SCENE_FALLBACK_BACKGROUND_COLOR = "#0b1018";
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
-12
View File
@@ -1,12 +0,0 @@
import { TERRAIN_COLORS, TERRAIN_TILE_SIZE } from "@/data/world/terrainConfig";
export const PATH_SURFACE_KEY = "chemin";
export const PATH_DEBUG_PREVIEW_ENABLED = false;
export const PATH_TILE_RENDER_ENABLED = false;
export const PATH_TILE_MODEL_PATH = TERRAIN_COLORS.chemin.modelPath;
export const PATH_TILE_SIZE =
TERRAIN_COLORS.chemin.tileSize ?? TERRAIN_TILE_SIZE;
export const PATH_TILE_SAMPLE_STEP = 2;
export const PATH_TILE_MAX_COUNT = 1500;
export const PATH_TILE_ROTATION = [0, 0, 0] as const;
export const PATH_TILE_SCALE = [1, 1, 1] as const;
-5
View File
@@ -1,5 +0,0 @@
import { useGameStore } from "@/managers/stores/useGameStore";
export function useActivityCity(): boolean {
return useGameStore((state) => state.missionFlow.activityCity);
}
-64
View File
@@ -1,64 +0,0 @@
import { useMemo } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
import type {
TerrainSurfaceBounds,
TerrainSurfaceData,
} from "@/types/world/terrainSurface";
import { createTerrainSurfaceImageData } from "@/utils/world/terrainSurfaceSampler";
function findTerrainBaseColorTexture(
scene: THREE.Object3D,
): THREE.Texture | null {
let texture: THREE.Texture | null = null;
scene.traverse((child) => {
if (texture || !(child instanceof THREE.Mesh)) return;
const materials = Array.isArray(child.material)
? child.material
: [child.material];
for (const material of materials) {
if (material instanceof THREE.MeshStandardMaterial && material.map) {
texture = material.map;
return;
}
}
});
return texture;
}
function createTerrainSurfaceBounds(
scene: THREE.Object3D,
): TerrainSurfaceBounds {
scene.updateWorldMatrix(true, true);
const box = new THREE.Box3().setFromObject(scene);
return {
minX: box.min.x,
maxX: box.max.x,
minZ: box.min.z,
maxZ: box.max.z,
};
}
export function useTerrainSurfaceData(): TerrainSurfaceData | null {
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
return useMemo(() => {
const texture = findTerrainBaseColorTexture(scene);
if (!texture) return null;
const imageData = createTerrainSurfaceImageData(texture);
if (!imageData) return null;
return {
bounds: createTerrainSurfaceBounds(scene),
imageData,
raycastTarget: scene,
};
}, [scene]);
}
+5 -2
View File
@@ -1,7 +1,10 @@
import { DEFAULT_CATEGORY_VOLUMES } from "@/data/audioConfig";
import {
DEFAULT_CATEGORY_VOLUMES,
type AudioCategory,
} from "@/data/audioConfig";
import { logger } from "@/utils/core/Logger";
export type AudioCategory = "music" | "sfx" | "dialogue";
export type { AudioCategory } from "@/data/audioConfig";
export type OneShotAudioCategory = Exclude<AudioCategory, "music">;
interface AudioContextWindow extends Window {
-1
View File
@@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element {
return (
<DocsDocument
content={animation}
frContent={animation}
meta="15"
title="Animation & 3D Model System"
/>
-1
View File
@@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element {
return (
<DocsDocument
content={architecture}
frContent={architecture}
meta="02"
title="Current Architecture"
/>
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsAudioPage(): React.JSX.Element {
return (
<DocsDocument
content={audio}
frContent={audio}
meta="08"
title="Audio Technical Notes"
/>
<DocsDocument content={audio} meta="08" title="Audio Technical Notes" />
);
}
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsCodeReviewPage(): React.JSX.Element {
return (
<DocsDocument
content={codeReview}
frContent={codeReview}
meta="16"
title="Code Review Prep"
/>
<DocsDocument content={codeReview} meta="16" title="Code Review Prep" />
);
}
+1 -8
View File
@@ -2,12 +2,5 @@ import features from "../../../../docs/user/features.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsFeaturesPage(): React.JSX.Element {
return (
<DocsDocument
content={features}
frContent={features}
meta="12"
title="Features"
/>
);
return <DocsDocument content={features} meta="12" title="Features" />;
}
-1
View File
@@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element {
return (
<DocsDocument
content={handTracking}
frContent={handTracking}
meta="09"
title="Hand Tracking Technical Notes"
/>
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsInteractionPage(): React.JSX.Element {
return (
<DocsDocument
content={interaction}
frContent={interaction}
meta="05"
title="Interaction System"
/>
<DocsDocument content={interaction} meta="05" title="Interaction System" />
);
}
+1 -8
View File
@@ -2,12 +2,5 @@ import mainFeature from "../../../../docs/user/main-feature.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsMainFeaturePage(): React.JSX.Element {
return (
<DocsDocument
content={mainFeature}
frContent={mainFeature}
meta="13"
title="Main Feature"
/>
);
return <DocsDocument content={mainFeature} meta="13" title="Main Feature" />;
}
+1 -8
View File
@@ -2,12 +2,5 @@ import readme from "../../../README.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsReadmePage(): React.JSX.Element {
return (
<DocsDocument
content={readme}
frContent={readme}
meta="01"
title="README"
/>
);
return <DocsDocument content={readme} meta="01" title="README" />;
}
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsSceneRuntimePage(): React.JSX.Element {
return (
<DocsDocument
content={sceneRuntime}
frContent={sceneRuntime}
meta="03"
title="Scene Runtime"
/>
<DocsDocument content={sceneRuntime} meta="03" title="Scene Runtime" />
);
}
@@ -5,7 +5,6 @@ export function DocsTargetArchitecturePage(): React.JSX.Element {
return (
<DocsDocument
content={targetArchitecture}
frContent={targetArchitecture}
meta="06"
title="Target Architecture"
/>
-1
View File
@@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element {
return (
<DocsDocument
content={technicalEditor}
frContent={technicalEditor}
meta="07"
title="Editor Technical Notes"
/>
+1 -6
View File
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsThreeDebuggingPage(): React.JSX.Element {
return (
<DocsDocument
content={threeDebugging}
frContent={threeDebugging}
meta="11"
title="Three Debugging"
/>
<DocsDocument content={threeDebugging} meta="11" title="Three Debugging" />
);
}
+1 -8
View File
@@ -2,12 +2,5 @@ import zustand from "../../../../docs/technical/zustand.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsZustandPage(): React.JSX.Element {
return (
<DocsDocument
content={zustand}
frContent={zustand}
meta="10"
title="Zustand Stores"
/>
);
return <DocsDocument content={zustand} meta="10" title="Zustand Stores" />;
}
-26
View File
@@ -1,5 +1,3 @@
import type * as THREE from "three";
export type TerrainSurfaceKind =
| "grass"
| "path"
@@ -10,18 +8,6 @@ export type TerrainSurfaceKind =
export type TerrainSurfaceRgb = readonly [number, number, number];
export interface TerrainSurfaceUv {
u: number;
v: number;
}
export interface TerrainSurfaceProjectionConfig {
flipX?: boolean;
flipZ?: boolean;
offsetX?: number;
offsetZ?: number;
}
export interface TerrainSurfaceBounds {
minX: number;
maxX: number;
@@ -37,15 +23,3 @@ export interface TerrainSurfaceColorConfig {
modelPath?: string;
tileSize?: number;
}
export interface TerrainSurfaceSample {
rgb: TerrainSurfaceRgb;
key: string | null;
config: TerrainSurfaceColorConfig | null;
}
export interface TerrainSurfaceData {
bounds: TerrainSurfaceBounds;
imageData: ImageData;
raycastTarget: THREE.Object3D;
}
-66
View File
@@ -1,66 +0,0 @@
import * as THREE from "three";
type TextureMaterialKey = Extract<
| keyof THREE.MeshBasicMaterial
| keyof THREE.MeshStandardMaterial
| keyof THREE.MeshPhysicalMaterial
| keyof THREE.MeshToonMaterial,
string
>;
type MaterialWithTextureSlots = THREE.Material &
Partial<Record<TextureMaterialKey, THREE.Texture | null>>;
const MATERIAL_TEXTURE_KEYS = [
"alphaMap",
"aoMap",
"bumpMap",
"clearcoatMap",
"clearcoatNormalMap",
"clearcoatRoughnessMap",
"displacementMap",
"emissiveMap",
"envMap",
"gradientMap",
"lightMap",
"map",
"metalnessMap",
"normalMap",
"roughnessMap",
"sheenColorMap",
"sheenRoughnessMap",
"specularColorMap",
"specularIntensityMap",
"specularMap",
"thicknessMap",
"transmissionMap",
] as const satisfies readonly TextureMaterialKey[];
export function disposeObject3D(object: THREE.Object3D): void {
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry?.dispose();
if (Array.isArray(child.material)) {
for (const material of child.material) {
disposeMaterial(material);
}
} else if (child.material) {
disposeMaterial(child.material);
}
}
});
}
function disposeMaterial(material: THREE.Material): void {
material.dispose();
const materialWithTextures = material as MaterialWithTextureSlots;
for (const key of MATERIAL_TEXTURE_KEYS) {
const value = materialWithTextures[key];
if (value instanceof THREE.Texture) {
value.dispose();
}
}
}
-51
View File
@@ -1,51 +0,0 @@
import {
TERRAIN_COLORS,
TERRAIN_SURFACE_COLOR_TOLERANCE,
type TerrainColorKey,
} from "@/data/world/terrainConfig";
import type { TerrainSurfaceRgb } from "@/types/world/terrainSurface";
export function colorMatchesTerrainSurface(
r: number,
g: number,
b: number,
targetRgb: TerrainSurfaceRgb,
tolerance: number = TERRAIN_SURFACE_COLOR_TOLERANCE,
): boolean {
return (
Math.abs(r - targetRgb[0]) <= tolerance &&
Math.abs(g - targetRgb[1]) <= tolerance &&
Math.abs(b - targetRgb[2]) <= tolerance
);
}
export function getTerrainColorKeyFromRgb(
r: number,
g: number,
b: number,
): TerrainColorKey | null {
for (const [key, config] of Object.entries(TERRAIN_COLORS)) {
if (colorMatchesTerrainSurface(r, g, b, config.rgb)) {
return key as TerrainColorKey;
}
}
return null;
}
export function isGrassTerrainColor(r: number, g: number, b: number): boolean {
const key = getTerrainColorKeyFromRgb(r, g, b);
return key !== null && TERRAIN_COLORS[key].kind === "grass";
}
export function getGrassTipColorFromRgb(
r: number,
g: number,
b: number,
): string | null {
const key = getTerrainColorKeyFromRgb(r, g, b);
if (key === null) return null;
const terrainColor = TERRAIN_COLORS[key];
return "grassTipColor" in terrainColor ? terrainColor.grassTipColor : null;
}
-125
View File
@@ -1,125 +0,0 @@
import * as THREE from "three";
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
import type {
TerrainSurfaceBounds,
TerrainSurfaceProjectionConfig,
TerrainSurfaceRgb,
TerrainSurfaceSample,
TerrainSurfaceUv,
} from "@/types/world/terrainSurface";
import { getTerrainColorKeyFromRgb } from "@/utils/world/terrainSurfaceColor";
type TerrainSurfaceImageSource =
| HTMLImageElement
| HTMLCanvasElement
| ImageBitmap;
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
function clamp01(value: number): number {
return Math.min(Math.max(value, 0), 1);
}
function wrap01(value: number): number {
return ((value % 1) + 1) % 1;
}
function isTerrainSurfaceImageSource(
value: unknown,
): value is TerrainSurfaceImageSource {
return (
value instanceof HTMLImageElement ||
value instanceof HTMLCanvasElement ||
(typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap)
);
}
export function createTerrainSurfaceImageData(
texture: THREE.Texture,
): ImageData | null {
if (typeof document === "undefined") return null;
const image = texture.image as unknown;
if (!isTerrainSurfaceImageSource(image)) return null;
const cachedImageData = imageDataCache.get(image);
if (cachedImageData) return cachedImageData;
const width = image.width;
const height = image.height;
if (width <= 0 || height <= 0) return null;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) return null;
canvas.width = width;
canvas.height = height;
context.drawImage(image, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
imageDataCache.set(image, imageData);
return imageData;
}
export function sampleTerrainSurfaceAtUv(
imageData: ImageData,
uv: TerrainSurfaceUv,
): TerrainSurfaceSample {
const x = Math.round(clamp01(uv.u) * (imageData.width - 1));
const y = Math.round((1 - clamp01(uv.v)) * (imageData.height - 1));
const index = (y * imageData.width + x) * 4;
const rgb: TerrainSurfaceRgb = [
imageData.data[index] ?? 0,
imageData.data[index + 1] ?? 0,
imageData.data[index + 2] ?? 0,
];
const key = getTerrainColorKeyFromRgb(rgb[0], rgb[1], rgb[2]);
return {
rgb,
key,
config: key === null ? null : TERRAIN_COLORS[key],
};
}
export function terrainSurfaceUvFromXZ(
x: number,
z: number,
bounds: TerrainSurfaceBounds,
projection?: TerrainSurfaceProjectionConfig,
): TerrainSurfaceUv {
const width = bounds.maxX - bounds.minX;
const depth = bounds.maxZ - bounds.minZ;
let u = width === 0 ? 0 : x / width + 0.5;
let v = depth === 0 ? 0 : z / depth + 0.5;
if (projection?.flipX) {
u = 1 - u;
}
if (projection?.flipZ) {
v = 1 - v;
}
u = wrap01(u + (projection?.offsetX ?? 0));
v = wrap01(v + (projection?.offsetZ ?? 0));
return {
u,
v,
};
}
export function sampleTerrainSurfaceAtXZ(
imageData: ImageData,
x: number,
z: number,
bounds: TerrainSurfaceBounds,
projection?: TerrainSurfaceProjectionConfig,
): TerrainSurfaceSample {
return sampleTerrainSurfaceAtUv(
imageData,
terrainSurfaceUvFromXZ(x, z, bounds, projection),
);
}
-4
View File
@@ -1,7 +1,5 @@
import {
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
GAME_SCENE_FALLBACK_SKY_MODEL_SCALE,
GAME_SCENE_SKY_MODEL_PATH,
GAME_SCENE_SKY_MODEL_SCALE,
PHYSICS_SCENE_BACKGROUND_COLOR,
@@ -37,8 +35,6 @@ export function Environment(): React.JSX.Element {
{showSky ? (
<SkyModel
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
modelPath={GAME_SCENE_SKY_MODEL_PATH}
scale={GAME_SCENE_SKY_MODEL_SCALE}
/>
-87
View File
@@ -1,87 +0,0 @@
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
import {
PATH_DEBUG_PREVIEW_ENABLED,
PATH_TILE_RENDER_ENABLED,
PATH_TILE_MODEL_PATH,
} from "@/data/world/pathConfig";
import { usePathTileData } from "@/world/paths/usePathTileData";
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
export function PathSystem(): React.JSX.Element | null {
if (!PATH_DEBUG_PREVIEW_ENABLED && !PATH_TILE_RENDER_ENABLED) {
return null;
}
return <PathTiles />;
}
function PathTiles(): React.JSX.Element | null {
const pathTiles = usePathTileData();
if (pathTiles.length === 0) {
return null;
}
if (PATH_DEBUG_PREVIEW_ENABLED) {
return <PathDebugPreview instances={pathTiles} />;
}
if (!PATH_TILE_RENDER_ENABLED) {
return null;
}
return (
<InstancedMapAsset
castShadow={false}
instances={pathTiles}
modelPath={PATH_TILE_MODEL_PATH}
receiveShadow
/>
);
}
function PathDebugPreview({
instances,
}: {
instances: MapAssetInstance[];
}): React.JSX.Element {
const instancedMeshRef = useRef<THREE.InstancedMesh>(null);
useEffect(() => {
const instancedMesh = instancedMeshRef.current;
if (!instancedMesh) return;
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
for (let i = 0; i < instances.length; i++) {
const instance = instances[i];
if (!instance) continue;
position.set(
instance.position[0],
instance.position[1] + 0.08,
instance.position[2],
);
matrix.compose(position, quaternion, scale);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.computeBoundingSphere();
}, [instances]);
return (
<instancedMesh
ref={instancedMeshRef}
args={[undefined, undefined, instances.length]}
>
<boxGeometry args={[0.35, 0.08, 0.35]} />
<meshBasicMaterial color="#ff00ff" />
</instancedMesh>
);
}
-72
View File
@@ -1,72 +0,0 @@
import { useMemo } from "react";
import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig";
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
import type { Vector3Tuple } from "@/types/three/three";
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
import {
PATH_TILE_MAX_COUNT,
PATH_SURFACE_KEY,
PATH_TILE_ROTATION,
PATH_TILE_SAMPLE_STEP,
PATH_TILE_SCALE,
} from "@/data/world/pathConfig";
function createSampleCenters(min: number, max: number, step: number): number[] {
const start = Math.ceil(min / step) * step + step * 0.5;
const centers: number[] = [];
for (let value = start; value <= max; value += step) {
centers.push(value);
}
return centers;
}
export function usePathTileData(): MapAssetInstance[] {
const terrainSurfaceData = useTerrainSurfaceData();
const terrainHeight = useTerrainHeightSampler();
return useMemo(() => {
if (!terrainSurfaceData) return [];
const instances: MapAssetInstance[] = [];
const xCenters = createSampleCenters(
terrainSurfaceData.bounds.minX,
terrainSurfaceData.bounds.maxX,
PATH_TILE_SAMPLE_STEP,
);
const zCenters = createSampleCenters(
terrainSurfaceData.bounds.minZ,
terrainSurfaceData.bounds.maxZ,
PATH_TILE_SAMPLE_STEP,
);
for (const x of xCenters) {
for (const z of zCenters) {
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
const sample = sampleTerrainSurfaceAtXZ(
terrainSurfaceData.imageData,
x,
z,
terrainSurfaceData.bounds,
TERRAIN_SURFACE_PROJECTION,
);
if (sample.key !== PATH_SURFACE_KEY) continue;
const height = terrainHeight.getHeight(x, z) ?? 0;
instances.push({
position: [x, height, z],
rotation: [...PATH_TILE_ROTATION] as Vector3Tuple,
scale: [...PATH_TILE_SCALE] as Vector3Tuple,
});
}
}
return instances;
}, [terrainHeight, terrainSurfaceData]);
}