'use client' import { Component, useEffect, useState } from 'react' import type { ComponentType, ReactNode } from 'react' import type { ModelHierarchyNode, ModelStats } from './SceneViewer' interface ModelViewerProps { url: string assetUrls: Record filename: string size: 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

) } 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 onStatsReady: (stats: ModelStats) => void onHierarchyReady: (hierarchy: ModelHierarchyNode) => void }> | 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 && (
Draw calls {stats.drawCalls} Children {stats.childObjects} Meshes {stats.meshes} Triangles {stats.triangles.toLocaleString('fr-FR')} Materials {stats.materials} Textures {stats.textures}
)} {hierarchy && hierarchyOpen && ( setHierarchyOpen(false)} /> )} {sceneError ? ( ) : ( )}
) }