2018-06-06 08:06:03 +02:00
|
|
|
// @flow
|
|
|
|
import * as React from 'react';
|
|
|
|
import LoadingScreen from 'component/common/loading-screen';
|
2018-06-13 00:40:55 +02:00
|
|
|
// ThreeJS
|
|
|
|
import * as THREE from './internal/three';
|
|
|
|
import detectWebGL from './internal/detector';
|
2018-07-20 03:31:00 +02:00
|
|
|
import ThreeGrid from './internal/grid';
|
2018-06-13 00:40:55 +02:00
|
|
|
import ThreeScene from './internal/scene';
|
|
|
|
import ThreeLoader from './internal/loader';
|
|
|
|
import ThreeRenderer from './internal/renderer';
|
2018-06-06 08:06:03 +02:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
theme: string,
|
|
|
|
autoRotate: boolean,
|
|
|
|
source: {
|
|
|
|
fileType: string,
|
2018-08-02 02:53:38 +02:00
|
|
|
downloadPath: string,
|
2018-06-06 08:06:03 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-08-13 05:30:13 +02:00
|
|
|
type State = {
|
2018-08-14 03:15:27 +02:00
|
|
|
error: ?string,
|
2018-08-13 05:30:13 +02:00
|
|
|
isReady: boolean,
|
|
|
|
isLoading: boolean,
|
|
|
|
};
|
|
|
|
|
|
|
|
class ThreeViewer extends React.PureComponent<Props, State> {
|
|
|
|
static testWebgl = new Promise((resolve, reject) => {
|
|
|
|
if (detectWebGL()) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
static createGeometry(data) {
|
|
|
|
const geometry = new THREE.Geometry();
|
|
|
|
geometry.fromBufferGeometry(data);
|
|
|
|
geometry.computeBoundingBox();
|
|
|
|
geometry.computeVertexNormals();
|
|
|
|
geometry.center();
|
|
|
|
geometry.rotateX(-Math.PI / 2);
|
|
|
|
geometry.lookAt(new THREE.Vector3(0, 0, 1));
|
|
|
|
return geometry;
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
2018-06-11 05:51:39 +02:00
|
|
|
|
|
|
|
const { theme } = this.props;
|
|
|
|
|
2018-06-13 00:40:55 +02:00
|
|
|
// Main container
|
2018-06-06 08:06:03 +02:00
|
|
|
this.viewer = React.createRef();
|
2018-06-11 05:51:39 +02:00
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
// Object colors
|
|
|
|
this.materialColors = {
|
|
|
|
red: '#e74c3c',
|
|
|
|
blue: '#3498db',
|
|
|
|
green: '#44b098',
|
|
|
|
orange: '#f39c12',
|
|
|
|
};
|
|
|
|
|
2018-06-11 05:51:39 +02:00
|
|
|
// Viewer themes
|
2018-06-06 08:06:03 +02:00
|
|
|
this.themes = {
|
|
|
|
dark: {
|
|
|
|
gridColor: '#414e5c',
|
|
|
|
groundColor: '#13233C',
|
|
|
|
backgroundColor: '#13233C',
|
|
|
|
centerLineColor: '#7f8c8d',
|
|
|
|
},
|
|
|
|
light: {
|
|
|
|
gridColor: '#7f8c8d',
|
|
|
|
groundColor: '#DDD',
|
|
|
|
backgroundColor: '#EEE',
|
|
|
|
centerLineColor: '#2F2F2F',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-06-09 22:33:31 +02:00
|
|
|
// Select current theme
|
2018-06-06 08:06:03 +02:00
|
|
|
this.theme = this.themes[theme] || this.themes.light;
|
2018-06-11 05:51:39 +02:00
|
|
|
|
|
|
|
// State
|
|
|
|
this.state = {
|
|
|
|
error: null,
|
|
|
|
isReady: false,
|
|
|
|
isLoading: false,
|
|
|
|
};
|
2018-06-06 08:06:03 +02:00
|
|
|
}
|
|
|
|
|
2018-06-13 00:40:55 +02:00
|
|
|
componentDidMount() {
|
2018-08-13 05:30:13 +02:00
|
|
|
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." });
|
|
|
|
});
|
2018-06-13 00:40:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2018-08-13 04:22:07 +02:00
|
|
|
// Remove event listeners
|
2018-06-13 00:40:55 +02:00
|
|
|
window.removeEventListener('resize', this.handleResize, false);
|
2018-08-13 04:22:07 +02:00
|
|
|
|
|
|
|
// Free memory
|
2018-08-14 03:15:27 +02:00
|
|
|
if (this.renderer && this.mesh) {
|
|
|
|
// Debug
|
|
|
|
console.info('before', this.renderer.info.programs.length);
|
2018-08-13 04:22:07 +02:00
|
|
|
// Clean up group
|
|
|
|
this.scene.remove(this.mesh);
|
|
|
|
if (this.mesh.geometry) this.mesh.geometry.dispose();
|
|
|
|
if (this.mesh.material) this.mesh.material.dispose();
|
2018-08-14 03:15:27 +02:00
|
|
|
// Clean up shared material
|
|
|
|
this.material.dispose();
|
|
|
|
// Clean up grid
|
|
|
|
this.grid.material.dispose();
|
|
|
|
this.grid.geometry.dispose();
|
2018-08-13 04:22:07 +02:00
|
|
|
// 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:
|
|
|
|
this.renderer.renderLists.dispose();
|
|
|
|
this.renderer.dispose();
|
2018-08-14 03:15:27 +02:00
|
|
|
// Debug
|
|
|
|
console.info('after', this.renderer.info.programs.length);
|
|
|
|
// Stop animation
|
|
|
|
cancelAnimationFrame(this.frameID);
|
|
|
|
// Empty objects
|
|
|
|
this.grid = null;
|
|
|
|
this.mesh = null;
|
|
|
|
this.material = null;
|
|
|
|
this.renderer = null;
|
2018-08-13 04:22:07 +02:00
|
|
|
}
|
2018-06-13 00:40:55 +02:00
|
|
|
}
|
|
|
|
|
2018-07-20 03:31:00 +02:00
|
|
|
transformGroup(group) {
|
2018-08-13 05:30:13 +02:00
|
|
|
ThreeViewer.fitMeshToCamera(group);
|
2018-07-20 03:31:00 +02:00
|
|
|
this.updateControlsTarget(group.position);
|
|
|
|
}
|
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
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;
|
2018-07-20 03:31:00 +02:00
|
|
|
controls.enablePan = false;
|
2018-06-06 08:06:03 +02:00
|
|
|
return controls;
|
|
|
|
}
|
|
|
|
|
|
|
|
startLoader() {
|
|
|
|
const { source } = this.props;
|
2018-06-13 00:40:55 +02:00
|
|
|
|
|
|
|
if (source) {
|
2018-06-06 08:06:03 +02:00
|
|
|
ThreeLoader(source, this.renderModel.bind(this), {
|
2018-07-19 08:45:32 +02:00
|
|
|
onStart: this.handleStart,
|
|
|
|
onLoad: this.handleReady,
|
|
|
|
onError: this.handleError,
|
2018-06-06 08:06:03 +02:00
|
|
|
});
|
2018-06-13 00:40:55 +02:00
|
|
|
}
|
2018-06-06 08:06:03 +02:00
|
|
|
}
|
|
|
|
|
2018-07-19 08:45:32 +02:00
|
|
|
handleStart = () => {
|
2018-06-06 08:06:03 +02:00
|
|
|
this.setState({ isLoading: true });
|
2018-07-19 08:45:32 +02:00
|
|
|
};
|
2018-06-06 08:06:03 +02:00
|
|
|
|
2018-07-19 08:45:32 +02:00
|
|
|
handleReady = () => {
|
2018-06-06 08:06:03 +02:00
|
|
|
this.setState({ isReady: true, isLoading: false });
|
2018-07-19 08:45:32 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
handleError = () => {
|
|
|
|
this.setState({ error: "Sorry, looks like we can't load this file" });
|
|
|
|
};
|
2018-06-06 08:06:03 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
|
|
|
handleColorChange(color) {
|
|
|
|
if (!this.mesh) return;
|
|
|
|
const pickColor = this.materialColors[color] || this.materialColors.green;
|
|
|
|
this.mesh.material.color.set(pickColor);
|
|
|
|
}
|
|
|
|
|
2018-06-13 00:40:55 +02:00
|
|
|
updateControlsTarget(point) {
|
|
|
|
this.controls.target.fromArray([point.x, point.y, point.z]);
|
|
|
|
this.controls.update();
|
|
|
|
}
|
|
|
|
|
2018-07-20 03:31:00 +02:00
|
|
|
renderStl(data) {
|
2018-08-13 05:30:13 +02:00
|
|
|
const geometry = ThreeViewer.createGeometry(data);
|
2018-07-20 03:31:00 +02:00
|
|
|
const group = new THREE.Mesh(geometry, this.material);
|
|
|
|
// Assign name
|
|
|
|
group.name = 'objectGroup';
|
|
|
|
this.scene.add(group);
|
|
|
|
this.transformGroup(group);
|
|
|
|
this.mesh = group;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderObj(event) {
|
|
|
|
const mesh = event.detail.loaderRootNode;
|
|
|
|
const group = new THREE.Group();
|
|
|
|
group.name = 'objGroup';
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
group.add(innerObj);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scene.add(group);
|
|
|
|
this.transformGroup(group);
|
|
|
|
this.mesh = group;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderModel(fileType, parsedData) {
|
|
|
|
const renderTypes = {
|
|
|
|
stl: data => this.renderStl(data),
|
|
|
|
obj: data => this.renderObj(data),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (renderTypes[fileType]) {
|
|
|
|
renderTypes[fileType](parsedData);
|
|
|
|
}
|
2018-06-06 08:06:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
renderScene() {
|
2018-07-20 03:31:00 +02:00
|
|
|
const { gridColor, centerLineColor } = this.theme;
|
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
this.renderer = ThreeRenderer({
|
|
|
|
antialias: true,
|
|
|
|
shadowMap: true,
|
2018-08-14 03:15:27 +02:00
|
|
|
gammaCorrection: true,
|
2018-06-06 08:06:03 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
this.scene = ThreeScene({
|
|
|
|
showFog: true,
|
|
|
|
...this.theme,
|
|
|
|
});
|
|
|
|
|
|
|
|
const viewer = this.viewer.current;
|
|
|
|
const canvas = this.renderer.domElement;
|
|
|
|
const { offsetWidth: width, offsetHeight: height } = viewer;
|
2018-07-20 03:31:00 +02:00
|
|
|
|
|
|
|
// Grid
|
|
|
|
this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor });
|
|
|
|
this.scene.add(this.grid);
|
2018-06-06 08:06:03 +02:00
|
|
|
// Camera
|
|
|
|
this.camera = new THREE.PerspectiveCamera(80, width / height, 0.1, 1000);
|
|
|
|
this.camera.position.set(-9.5, 14, 11);
|
2018-07-20 03:31:00 +02:00
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
// Controls
|
|
|
|
this.controls = this.createOrbitControls(this.camera, canvas);
|
2018-07-20 03:31:00 +02:00
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
// Set viewer size
|
|
|
|
this.renderer.setSize(width, height);
|
2018-07-20 03:31:00 +02:00
|
|
|
|
|
|
|
// Create model material
|
|
|
|
this.material = new THREE.MeshPhongMaterial({
|
2018-08-13 04:22:07 +02:00
|
|
|
depthWrite: true,
|
2018-07-20 03:31:00 +02:00
|
|
|
vertexColors: THREE.FaceColors,
|
|
|
|
});
|
2018-08-13 04:22:07 +02:00
|
|
|
|
2018-07-20 03:31:00 +02:00
|
|
|
// Set material color
|
|
|
|
this.material.color.set(this.materialColors.green);
|
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
// Load file and render mesh
|
|
|
|
this.startLoader();
|
|
|
|
|
2018-08-13 04:22:07 +02:00
|
|
|
// Append canvas
|
|
|
|
viewer.appendChild(canvas);
|
|
|
|
|
2018-06-06 08:06:03 +02:00
|
|
|
const updateScene = () => {
|
2018-08-14 03:15:27 +02:00
|
|
|
this.frameID = requestAnimationFrame(updateScene);
|
|
|
|
// this.controls.update();
|
2018-06-06 08:06:03 +02:00
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
|
|
};
|
|
|
|
|
|
|
|
updateScene();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2018-06-13 00:40:55 +02:00
|
|
|
const { error, isReady, isLoading } = this.state;
|
2018-08-13 04:22:07 +02:00
|
|
|
const loadingMessage = __('Loading 3D model.');
|
2018-06-06 08:06:03 +02:00
|
|
|
const showViewer = isReady && !error;
|
|
|
|
const showLoading = isLoading && !error;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Fragment>
|
2018-08-13 05:30:13 +02:00
|
|
|
{error && <LoadingScreen status={error} spinner={false} />}
|
2018-06-13 00:40:55 +02:00
|
|
|
{showLoading && <LoadingScreen status={loadingMessage} spinner />}
|
|
|
|
<div
|
|
|
|
style={{ opacity: showViewer ? 1 : 0 }}
|
|
|
|
className="three-viewer file-render__viewer"
|
|
|
|
ref={this.viewer}
|
|
|
|
/>
|
2018-06-06 08:06:03 +02:00
|
|
|
</React.Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ThreeViewer;
|