import PropTypes from 'prop-types' import { useCallback, useEffect, useRef, useState } from 'react' import * as OV from 'online-3d-viewer' import LoadingPlaceholder from './LoadingPlaceholder' function ThreeDPreview(props) { const { src, extension = '.stl', width = 500, height = 500, style = {}, backgroundColor = '#ffffff' } = props const containerRef = useRef(null) const viewer = useRef(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const resizeViewer = useCallback(() => { if (viewer.current && containerRef.current) { // Resize the viewer container const container = containerRef.current if (container.style.width !== width + 'px') { container.style.width = width + 'px' } if (container.style.height !== height + 'px') { container.style.height = height + 'px' } } }, [viewer, width, height]) useEffect(() => { // Variable to track the viewer instance created in this effect let currentViewer = null const initializeViewer = async () => { if (!containerRef.current) return try { setIsLoading(true) setError(null) // Clear any existing viewer if (viewer.current) { if (viewer.current.dispose) { viewer.current.dispose() } containerRef.current.innerHTML = '' } // Wait for the OV global to be available if (typeof OV === 'undefined') { // If OV is not available, try to load it dynamically console.warn( 'OV (Online 3D Viewer) is not available. Make sure the library is loaded.' ) setError('3D Viewer library not loaded') setIsLoading(false) return } // Initialize the online-3d-viewer using OV.EmbeddedViewer const newViewer = new OV.EmbeddedViewer(containerRef.current, { camera: new OV.Camera( new OV.Coord3D(-1.5, 2.0, 3.0), new OV.Coord3D(0.0, 0.0, 0.0), new OV.Coord3D(0.0, 1.0, 0.0), 45.0 ), backgroundColor: new OV.RGBAColor(255, 255, 255, 255), defaultColor: new OV.RGBColor(200, 200, 200), edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1), environmentSettings: new OV.EnvironmentSettings([], false) }) // Store the viewer instance in the ref and local variable viewer.current = newViewer currentViewer = newViewer try { setIsLoading(true) setError(null) const response = await fetch(src) const arrayBuffer = await response.arrayBuffer() // Create a file-like object from the array buffer const fileName = `model${extension}` const file = new File([arrayBuffer], fileName, { type: 'application/octet-stream' }) // Load model from file using LoadModelFromFileList await newViewer.LoadModelFromFileList([file]) setIsLoading(false) } catch (err) { console.error('Failed to load 3D model from src', err) setError('Failed to load 3D model') setIsLoading(false) } } catch (err) { console.error('Failed to initialize 3D viewer', err) setError('Failed to initialize 3D viewer') setIsLoading(false) } } initializeViewer() window.addEventListener('resize', resizeViewer) return () => { window.removeEventListener('resize', resizeViewer) if (currentViewer && currentViewer.dispose) { currentViewer.dispose() } } }, [width, height, src, extension, resizeViewer]) const containerStyle = { width: width + 'px', height: height + 'px', backgroundColor, position: 'relative', ...style } return (
{isLoading && } {error && (
{error}
)}
) } ThreeDPreview.propTypes = { src: PropTypes.string, width: PropTypes.number, height: PropTypes.number, style: PropTypes.object, backgroundColor: PropTypes.string, showGrid: PropTypes.bool, showAxes: PropTypes.bool, enableControls: PropTypes.bool, extension: PropTypes.string.isRequired } export default ThreeDPreview