// @flow import * as React from 'react'; import * as dat from 'dat.gui'; import LoadingScreen from 'component/common/loading-screen'; // ThreeJS import * as THREE from './internal/three'; import detectWebGL from './internal/detector'; import ThreeGrid from './internal/grid'; import ThreeScene from './internal/scene'; import ThreeLoader from './internal/loader'; import ThreeRenderer from './internal/renderer'; type Props = { theme: string, autoRotate: boolean, source: { fileType: string, downloadPath: string, }, }; type State = { error: ?string, isReady: boolean, isLoading: boolean, }; class ThreeViewer extends React.PureComponent { static testWebgl = new Promise((resolve, reject) => { if (detectWebGL()) resolve(); else reject(); }); static fitMeshToCamera(group) { const max = { x: 0, y: 0, z: 0 }; const min = { x: 0, y: 0, z: 0 }; group.traverse(child => { if (child instanceof THREE.Mesh) { const box = new THREE.Box3().setFromObject(child); // Max max.x = box.max.x > max.x ? box.max.x : max.x; max.y = box.max.y > max.y ? box.max.y : max.y; max.z = box.max.z > max.z ? box.max.z : max.z; // Min min.x = box.min.x < min.x ? box.min.x : min.x; min.y = box.min.y < min.y ? box.min.y : min.y; min.z = box.min.z < min.z ? box.min.z : min.z; } }); const meshY = Math.abs(max.y - min.y); const meshX = Math.abs(max.x - min.x); const scaleFactor = 10 / Math.max(meshX, meshY); group.scale.set(scaleFactor, scaleFactor, scaleFactor); group.position.setY((meshY / 2) * scaleFactor); // Reset object position const box = new THREE.Box3().setFromObject(group); box.getCenter(group.position); // Update position group.position.multiplyScalar(-1); group.position.setY(group.position.y + meshY * scaleFactor); } constructor(props: Props) { super(props); const { theme } = this.props; this.viewer = React.createRef(); this.guiContainer = React.createRef(); // Object defualt color this.materialColor = '#44b098'; // Viewer themes this.themes = { dark: { gridColor: '#414e5c', groundColor: '#13233C', backgroundColor: '#13233C', centerLineColor: '#7f8c8d', }, light: { gridColor: '#7f8c8d', groundColor: '#DDD', backgroundColor: '#EEE', centerLineColor: '#2F2F2F', }, }; // Select current theme this.theme = this.themes[theme] || this.themes.light; // State this.state = { error: null, isReady: false, isLoading: false, }; } componentDidMount() { ThreeViewer.testWebgl .then(() => { this.renderScene(); // Update render on resize window window.addEventListener('resize', this.handleResize, false); }) .catch(() => { // No webgl support, handle Error... // TODO: Use a better error message this.setState({ error: "Sorry, your computer doesn't support WebGL." }); }); } componentWillUnmount() { // Remove event listeners window.removeEventListener('resize', this.handleResize, false); // Free memory if (this.renderer && this.mesh) { // Clean up group this.scene.remove(this.mesh); if (this.mesh.geometry) this.mesh.geometry.dispose(); if (this.mesh.material) this.mesh.material.dispose(); // Cleanup shared geometry if (this.geometry) this.geometry.dispose(); if (this.bufferGeometry) this.bufferGeometry.dispose(); // Clean up shared material if (this.material) this.material.dispose(); // Clean up grid if (this.grid) { this.grid.material.dispose(); this.grid.geometry.dispose(); } // Clean up group items this.mesh.traverse(child => { if (child instanceof THREE.Mesh) { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); } }); // It's unclear if we need this: if (this.renderer) { this.renderer.renderLists.dispose(); this.renderer.dispose(); } // Stop animation cancelAnimationFrame(this.frameID); // Destroy GUI Controls if (this.gui) this.gui.destroy(); // Empty objects this.grid = null; this.mesh = null; this.renderer = null; this.material = null; this.geometry = null; this.bufferGeometry = null; } } transformGroup(group) { ThreeViewer.fitMeshToCamera(group); this.updateControlsTarget(group.position); } createInterfaceControls() { if (this.guiContainer && this.mesh) { const config = { color: this.materialColor, wireframe: false, }; this.gui = new dat.GUI({ autoPlace: false }); const colorPicker = this.gui.addColor(config, 'color'); colorPicker.onChange(color => { this.material.color.set(color); }); this.gui.add(this.material, 'wireframe').listen(); this.guiContainer.current.appendChild(this.gui.domElement); } } createOrbitControls(camera, canvas) { const { autoRotate } = this.props; const controls = new THREE.OrbitControls(camera, canvas); // Controls configuration controls.enableDamping = true; controls.dampingFactor = 0.75; controls.enableZoom = true; controls.minDistance = 1; controls.maxDistance = 50; controls.autoRotate = autoRotate; controls.enablePan = false; return controls; } createGeometry(data) { this.bufferGeometry = data; this.bufferGeometry.computeBoundingBox(); this.bufferGeometry.computeVertexNormals(); this.bufferGeometry.center(); this.bufferGeometry.rotateX(-Math.PI / 2); this.bufferGeometry.lookAt(new THREE.Vector3(0, 0, 1)); // Get geometry from bufferGeometry this.geometry = new THREE.Geometry().fromBufferGeometry(this.bufferGeometry); this.geometry.mergeVertices(); } startLoader() { const { source } = this.props; if (source) { ThreeLoader(source, this.renderModel.bind(this), { onStart: this.handleStart, onLoad: this.handleReady, onError: this.handleError, }); } } handleStart = () => { this.setState({ isLoading: true }); }; handleReady = () => { this.setState({ isReady: true, isLoading: false }); // GUI this.createInterfaceControls(); }; handleError = () => { this.setState({ error: "Sorry, looks like we can't load this file" }); }; handleResize = () => { const { offsetWidth: width, offsetHeight: height } = this.viewer.current; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.controls.update(); this.renderer.setSize(width, height); }; updateControlsTarget(point) { this.controls.target.fromArray([point.x, point.y, point.z]); this.controls.update(); } renderStl(data) { this.createGeometry(data); this.mesh = new THREE.Mesh(this.geometry, this.material); this.mesh.name = 'model'; this.scene.add(this.mesh); this.transformGroup(this.mesh); } renderObj(event) { const mesh = event.detail.loaderRootNode; this.mesh = new THREE.Group(); this.mesh.name = 'model'; // Assign new material mesh.traverse(child => { if (child instanceof THREE.Mesh) { // Get geometry from child const geometry = new THREE.Geometry(); geometry.fromBufferGeometry(child.geometry); // Create and regroup inner objects const innerObj = new THREE.Mesh(geometry, this.material); this.mesh.add(innerObj); // Clean up geometry geometry.dispose(); child.geometry.dispose(); } }); this.scene.add(this.mesh); this.transformGroup(this.mesh); } renderModel(fileType, parsedData) { const renderTypes = { stl: data => this.renderStl(data), obj: data => this.renderObj(data), }; if (renderTypes[fileType]) { renderTypes[fileType](parsedData); } } renderScene() { const { gridColor, centerLineColor } = this.theme; this.renderer = ThreeRenderer({ antialias: true, shadowMap: true, gammaCorrection: true, }); this.scene = ThreeScene({ showFog: true, ...this.theme, }); const viewer = this.viewer.current; const canvas = this.renderer.domElement; const { offsetWidth: width, offsetHeight: height } = viewer; // Grid this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor }); this.scene.add(this.grid); // Camera this.camera = new THREE.PerspectiveCamera(80, width / height, 0.1, 1000); this.camera.position.set(-9.5, 14, 11); // Controls this.controls = this.createOrbitControls(this.camera, canvas); // Set viewer size this.renderer.setSize(width, height); // Create model material this.material = new THREE.MeshPhongMaterial({ depthWrite: true, vertexColors: THREE.FaceColors, }); // Set material color this.material.color.set(this.materialColor); // Load file and render mesh this.startLoader(); // Append canvas viewer.appendChild(canvas); const updateScene = () => { this.frameID = requestAnimationFrame(updateScene); // this.controls.update(); this.renderer.render(this.scene, this.camera); }; updateScene(); } render() { const { error, isReady, isLoading } = this.state; const loadingMessage = __('Loading 3D model.'); const showViewer = isReady && !error; const showLoading = isLoading && !error; return ( {error && } {showLoading && }
); } } export default ThreeViewer;