import { Bounds, Center, OrbitControls, useAnimations, useGLTF, } from "@react-three/drei"; import { Canvas } from "@react-three/fiber"; import { Component, Suspense, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { ArrowLeft, ArrowRight, CheckCircle2, SlidersHorizontal, TriangleAlert, } from "lucide-react"; import * as THREE from "three"; import { SkyModel } from "@/components/three/world/SkyModel"; import { galleryModels, type GalleryModel } from "@/data/galleryModels"; import { AMBIENT_LIGHT_COLOR, LIGHTING_DEFAULTS, SUN_LIGHT_COLOR, } from "@/data/world/lightingConfig"; import { GAME_SCENE_FALLBACK_SKY_MODEL_PATH, GAME_SCENE_FALLBACK_SKY_MODEL_SCALE, GAME_SCENE_SKY_MODEL_PATH, GAME_SCENE_SKY_MODEL_SCALE, } from "@/data/world/environmentConfig"; interface GalleryModelProps { model: GalleryModel; } interface GallerySceneProps extends GalleryModelProps { lighting: GalleryLightingConfig; onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void; } interface GalleryModelPreviewProps extends GalleryModelProps { onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void; } interface GalleryLightingConfig { ambientIntensity: number; sunIntensity: number; sunX: number; sunY: number; sunZ: number; } interface GalleryLightControl { key: keyof GalleryLightingConfig; label: string; min: number; max: number; step: number; } interface TextureDiagnostic { modelId: string | null; status: "loading" | "ok" | "warning"; summary: string; } interface GalleryViewerErrorBoundaryProps { children: ReactNode; resetKey: string; } interface GalleryViewerErrorBoundaryState { hasError: boolean; } const TEXTURE_SLOTS = [ "map", "normalMap", "roughnessMap", "metalnessMap", "aoMap", "emissiveMap", "alphaMap", ] as const; const LOADING_TEXTURE_DIAGNOSTIC: TextureDiagnostic = { modelId: null, status: "loading", summary: "Analyse des textures...", }; const GALLERY_LIGHT_CONTROLS: GalleryLightControl[] = [ { key: "ambientIntensity", label: "Ambiance", min: 0, max: 5, step: 0.1 }, { key: "sunIntensity", label: "Soleil", min: 0, max: 8, step: 0.1 }, { key: "sunX", label: "Soleil X", min: -100, max: 100, step: 1 }, { key: "sunY", label: "Soleil Y", min: -100, max: 150, step: 1 }, { key: "sunZ", label: "Soleil Z", min: -100, max: 100, step: 1 }, ]; class GalleryViewerErrorBoundary extends Component< GalleryViewerErrorBoundaryProps, GalleryViewerErrorBoundaryState > { constructor(props: GalleryViewerErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): GalleryViewerErrorBoundaryState { return { hasError: true }; } componentDidUpdate(previousProps: GalleryViewerErrorBoundaryProps): void { if (previousProps.resetKey !== this.props.resetKey && this.state.hasError) { this.setState({ hasError: false }); } } render(): ReactNode { if (this.state.hasError) { return (
Ce modèle ne peut pas être affiché pour le moment.
); } return this.props.children; } } function GalleryModelPreview({ model, onTextureDiagnosticReady, }: GalleryModelPreviewProps): React.JSX.Element { const groupRef = useRef(null); const { animations, scene } = useGLTF(model.path); const modelScene = useMemo(() => createGalleryModelScene(scene), [scene]); const { actions } = useAnimations(animations, groupRef); useEffect(() => { return () => { disposeGalleryModelMaterials(modelScene); }; }, [modelScene]); useEffect(() => { onTextureDiagnosticReady(getTextureDiagnostic(model.id, modelScene)); }, [model.id, modelScene, onTextureDiagnosticReady]); useEffect(() => { const animationActions = Object.values(actions).filter( (action): action is THREE.AnimationAction => Boolean(action), ); for (const action of animationActions) { action.reset().play(); } return () => { for (const action of animationActions) { action.stop(); } }; }, [actions]); return ( ); } function createGalleryModelScene(scene: THREE.Object3D): THREE.Object3D { const modelScene = scene.clone(true); modelScene.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; object.material = Array.isArray(object.material) ? object.material.map(createGalleryMaterial) : createGalleryMaterial(object.material); }); return modelScene; } function createGalleryMaterial(material: THREE.Material): THREE.Material { const galleryMaterial = material.clone(); const materialWithNormalMap = galleryMaterial as THREE.Material & { normalMap?: THREE.Texture | null; }; galleryMaterial.side = THREE.DoubleSide; if (materialWithNormalMap.normalMap) { materialWithNormalMap.normalMap = null; galleryMaterial.needsUpdate = true; } return galleryMaterial; } function disposeGalleryModelMaterials(modelScene: THREE.Object3D): void { modelScene.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; if (Array.isArray(object.material)) { for (const material of object.material) { material.dispose(); } return; } object.material.dispose(); }); } function GalleryScene({ lighting, model, onTextureDiagnosticReady, }: GallerySceneProps): React.JSX.Element { return ( <>
); } function GalleryLighting({ lighting, }: { lighting: GalleryLightingConfig; }): React.JSX.Element { return ( <> ); } function TextureStatusBadge({ diagnostic, }: { diagnostic: TextureDiagnostic; }): React.JSX.Element { const hasWarning = diagnostic.status === "warning"; const Icon = hasWarning ? TriangleAlert : CheckCircle2; return (
); } function GalleryLightingPanel({ lighting, onChange, onReset, onToggle, open, }: { lighting: GalleryLightingConfig; onChange: (key: keyof GalleryLightingConfig, value: number) => void; onReset: () => void; onToggle: () => void; open: boolean; }): React.JSX.Element { return ( ); } function getTextureDiagnostic( modelId: string, modelScene: THREE.Object3D, ): TextureDiagnostic { let textureCount = 0; let missingTextureImageCount = 0; modelScene.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; const materials = Array.isArray(object.material) ? object.material : [object.material]; for (const material of materials) { const materialRecord = material as unknown as Record; for (const textureSlot of TEXTURE_SLOTS) { const texture = materialRecord[textureSlot]; if (!(texture instanceof THREE.Texture)) continue; textureCount += 1; if (!texture.image) { missingTextureImageCount += 1; } } } }); if (missingTextureImageCount > 0) { return { modelId, status: "warning", summary: `${missingTextureImageCount} texture(s) à vérifier`, }; } if (textureCount === 0) { return { modelId, status: "warning", summary: "Aucune texture détectée", }; } return { modelId, status: "ok", summary: `${textureCount} texture(s) OK`, }; } export function GalleryPage(): React.JSX.Element { const [activeModelIndex, setActiveModelIndex] = useState(0); const [lightPanelOpen, setLightPanelOpen] = useState(false); const [lighting, setLighting] = useState({ ...LIGHTING_DEFAULTS, }); const [textureDiagnostic, setTextureDiagnostic] = useState( LOADING_TEXTURE_DIAGNOSTIC, ); const activeModel = galleryModels[activeModelIndex] ?? galleryModels[0]!; const modelCount = galleryModels.length; const activeTextureDiagnostic = textureDiagnostic.modelId === activeModel.id ? textureDiagnostic : LOADING_TEXTURE_DIAGNOSTIC; const goToPreviousModel = (): void => { setActiveModelIndex((currentIndex) => currentIndex === 0 ? modelCount - 1 : currentIndex - 1, ); }; const goToNextModel = (): void => { setActiveModelIndex((currentIndex) => currentIndex === modelCount - 1 ? 0 : currentIndex + 1, ); }; const handleLightChange = ( key: keyof GalleryLightingConfig, value: number, ): void => { setLighting((currentLighting) => ({ ...currentLighting, [key]: value, })); }; const resetLighting = (): void => { setLighting({ ...LIGHTING_DEFAULTS }); }; return (

GALERIE

setLightPanelOpen((open) => !open)} open={lightPanelOpen} />
); }