refactor: nettoie l'architecture monde et les docs
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
|
||||
export function useActivityCity(): boolean {
|
||||
return useGameStore((state) => state.missionFlow.activityCity);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={animation}
|
||||
frContent={animation}
|
||||
meta="15"
|
||||
title="Animation & 3D Model System"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={architecture}
|
||||
frContent={architecture}
|
||||
meta="02"
|
||||
title="Current Architecture"
|
||||
/>
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={handTracking}
|
||||
frContent={handTracking}
|
||||
meta="09"
|
||||
title="Hand Tracking Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={technicalEditor}
|
||||
frContent={technicalEditor}
|
||||
meta="07"
|
||||
title="Editor Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user