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 (
{diagnostic.summary}
);
}
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}
/>
);
}