'use client' import { Component, useEffect, useState } from 'react' import type { ComponentType, ReactNode } from 'react' import type { ModelHierarchyNode, ModelStats, SceneViewerProps } from '@/lib/client-types' interface ModelViewerProps { url: string assetUrls: Record filename: string size: string } const VIEWER_FRAME_CLASS = 'w-full h-[450px] bg-black-800 border border-white/20 rounded-xl overflow-hidden' const CENTERED_VIEWER_FRAME_CLASS = `${VIEWER_FRAME_CLASS} flex items-center justify-center` const MODEL_STAT_ROWS = [ { label: 'Draw calls', getValue: (stats: ModelStats) => stats.drawCalls }, { label: 'Children', getValue: (stats: ModelStats) => stats.childObjects }, { label: 'Meshes', getValue: (stats: ModelStats) => stats.meshes }, { label: 'Triangles', getValue: (stats: ModelStats) => stats.triangles.toLocaleString('fr-FR') }, { label: 'Materials', getValue: (stats: ModelStats) => stats.materials }, { label: 'Textures', getValue: (stats: ModelStats) => stats.textures }, ] satisfies Array<{ label: string getValue: (stats: ModelStats) => number | string }> function getPreviewErrorMessage(error: unknown) { return error instanceof Error ? error.message : 'Erreur preview inconnue' } function PreviewFallback({ message }: { message?: string }) { return (

Preview 3D indisponible pour ce modele.

L'upload reste possible. {message ? `Detail technique : ${message}` : ''}

) } function formatTransformValue(value: number) { return Number.isInteger(value) ? String(value) : value.toFixed(4).replace(/0+$/, '').replace(/\.$/, '') } function formatTransform(values: [number, number, number]) { return `[${values.map(formatTransformValue).join(', ')}]` } function countHierarchyNodes(node: ModelHierarchyNode): number { return 1 + node.children.reduce((count, child) => count + countHierarchyNodes(child), 0) } function HierarchyTree({ node, depth = 0, }: { node: ModelHierarchyNode depth?: number }) { const hasChildren = node.children.length > 0 return (
{hasChildren ? 'v' : '-'} {node.name} {node.type}
pos {formatTransform(node.position)} rot {formatTransform(node.rotation)}
{node.visible ? 'Visible' : 'Hidden'}
{hasChildren && (
{node.children.map((child) => ( ))}
)}
) } function HierarchyPanel({ hierarchy, onClose, }: { hierarchy: ModelHierarchyNode onClose: () => void }) { const nodeCount = countHierarchyNodes(hierarchy) return (

Hierarchy

{nodeCount} nodes

) } function ModelStatsPanel({ stats }: { stats: ModelStats }) { return (
{MODEL_STAT_ROWS.map(({ label, getValue }) => ( {label} {getValue(stats)} ))}
) } class PreviewErrorBoundary extends Component< { children: ReactNode }, { message: string | null } > { state = { message: null } static getDerivedStateFromError(error: unknown) { return { message: getPreviewErrorMessage(error) } } componentDidCatch(error: unknown) { console.error('[ERROR] Preview 3D indisponible', error) } render() { if (this.state.message) { return } return this.props.children } } export default function ModelViewer({ url, assetUrls, filename, size }: ModelViewerProps) { const canPreview = filename.toLowerCase().endsWith('.gltf') const [stats, setStats] = useState(null) const [hierarchy, setHierarchy] = useState(null) const [hierarchyOpen, setHierarchyOpen] = useState(false) const [sceneError, setSceneError] = useState(null) const [Scene, setScene] = useState | null>(null) useEffect(() => { if (!canPreview) return let cancel = false setSceneError(null) setStats(null) setHierarchy(null) setHierarchyOpen(false) import('./SceneViewer') .then((mod) => { if (!cancel) setScene(() => mod.default) }) .catch((error: unknown) => { if (!cancel) setSceneError(getPreviewErrorMessage(error)) }) return () => { cancel = true } }, [canPreview, url]) if (!canPreview) { return (

La preview 3D locale n'est pas disponible pour les dossiers model.gltf avec fichiers associes.

) } if (!Scene) { return (
) } return (
{filename} {size}
{stats && (
)} {hierarchy && hierarchyOpen && ( setHierarchyOpen(false)} /> )} {sceneError ? ( ) : ( )}
) }