import { useEffect, useRef, useState, type ReactNode } from 'react'; import { detectWebGLSupport, getWebGLFailureMessage, type DetectWebGLOptions, type WebGLSupportResult, } from '../../utils/webgl'; export interface WebGLFailureFallbackProps { result?: WebGLSupportResult; title?: string; onRetry?: () => void; compact?: boolean; } export interface WebGLSupportBoundaryProps { children: ReactNode; detectOptions?: DetectWebGLOptions; fallback?: (props: WebGLFailureFallbackProps) => ReactNode; checkingFallback?: ReactNode; className?: string; onUnsupported?: (result: WebGLSupportResult) => void; } type WebGLContextCreationEvent = Event & { statusMessage?: string; }; export function WebGLFailureFallback({ result, title = '3D graphics are unavailable', onRetry, compact = false, }: WebGLFailureFallbackProps): JSX.Element { const message = result ? getWebGLFailureMessage(result) : 'Mechanica could not verify WebGL support in this environment.'; return ( WebGL fallback {title} {message} {result?.details ? ( Renderer {result.renderer} Reason {result.reason ?? 'unknown'} {result.details.userAgent ? ( User agent {result.details.userAgent} ) : null} ) : null} {onRetry ? ( Retry graphics check ) : null} Return to catalogue ); } export function WebGLSupportBoundary({ children, detectOptions, fallback, checkingFallback, className, onUnsupported, }: WebGLSupportBoundaryProps): JSX.Element { const containerRef = useRef(null); const [support, setSupport] = useState(() => typeof window === 'undefined' ? null : detectWebGLSupport(detectOptions), ); const [runtimeFailure, setRuntimeFailure] = useState(null); const runDetection = () => { const result = detectWebGLSupport(detectOptions); setSupport(result); setRuntimeFailure(null); if (!result.supported) { onUnsupported?.(result); } }; useEffect(() => { if (!support) { runDetection(); } // Initial check only. detectOptions should be stable at integration sites. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const container = containerRef.current; if (!container) { return undefined; } const handleContextLost = (event: Event) => { event.preventDefault(); setRuntimeFailure({ supported: false, renderer: 'none', reason: 'context-lost', details: { errorMessage: 'The WebGL context was lost.', userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, devicePixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio : undefined, }, }); }; const handleContextCreationError = (event: Event) => { const webglEvent = event as WebGLContextCreationEvent; setRuntimeFailure({ supported: false, renderer: 'none', reason: 'context-creation-error', details: { errorMessage: webglEvent.statusMessage || 'WebGL context creation failed.', userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, devicePixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio : undefined, }, }); }; container.addEventListener('webglcontextlost', handleContextLost, true); container.addEventListener('webglcontextcreationerror', handleContextCreationError, true); return () => { container.removeEventListener('webglcontextlost', handleContextLost, true); container.removeEventListener('webglcontextcreationerror', handleContextCreationError, true); }; }, []); const failure = runtimeFailure ?? (support && !support.supported ? support : null); useEffect(() => { if (failure) { onUnsupported?.(failure); } }, [failure, onUnsupported]); if (!support) { return ( <> {checkingFallback ?? ( Checking graphics capabilities… )} > ); } if (failure) { return ( <> {fallback ? ( fallback({ result: failure, onRetry: runDetection, }) ) : ( )} > ); } return ( {children} ); }
WebGL fallback
{message}