lbry-desktop/src/renderer/component/threeViewer/index.jsx

274 lines
7.3 KiB
React
Raw Normal View History

2018-06-06 08:06:03 +02:00
// @flow
import * as React from 'react';
import * as THREE from './internal/three.js';
import detectWebGL from './internal/detector.js';
import ThreeScene from './internal/scene.js';
import ThreeLoader from './internal/loader.js';
import ThreeRenderer from './internal/renderer.js';
import LoadingScreen from 'component/common/loading-screen';
type Props = {
theme: string,
autoRotate: boolean,
source: {
fileType: string,
filePath: string,
},
};
class ThreeViewer extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
//Main container
this.viewer = React.createRef();
// Object colors
this.materialColors = {
red: '#e74c3c',
blue: '#3498db',
green: '#44b098',
orange: '#f39c12',
};
this.state = {
error: null,
isReady: false,
isLoading: false,
};
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
const { theme } = this.props;
this.theme = this.themes[theme] || this.themes.light;
}
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;
return controls;
}
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;
}
createWireFrame(group) {
const wireframe = new THREE.WireframeGeometry(group.geometry);
this.wireframe = new THREE.LineSegments(wireframe);
this.wireframe.material.depthTest = false;
this.wireframe.material.opacity = 0;
this.wireframe.transparent = true;
group.add(this.wireframe);
}
createMesh(geometry) {
const material = new THREE.MeshPhongMaterial({
opacity: 1,
transparent: true,
depthWrite: true,
vertexColors: THREE.FaceColors,
// Positive value pushes polygon further away
polygonOffsetFactor: 1,
polygonOffsetUnits: 1,
});
// Set material color
material.color.set(this.materialColors.green);
const mesh = new THREE.Mesh(geometry, material);
2018-06-09 22:33:31 +02:00
// Assign name
2018-06-06 08:06:03 +02:00
mesh.name = 'objectGroup';
2018-06-09 22:33:31 +02:00
2018-06-06 08:06:03 +02:00
this.scene.add(mesh);
this.fitMeshToCamera(mesh);
2018-06-09 22:33:31 +02:00
this.createWireFrame(mesh);
2018-06-06 08:06:03 +02:00
this.setControlsTarget(mesh.position);
return mesh;
}
toggleWireFrame(show = false) {
this.wireframe.opacity = show ? 1 : 0;
this.mesh.material.opacity = show ? 0 : 1;
}
fitMeshToCamera(group) {
let max = { x: 0, y: 0, z: 0 };
let min = { x: 0, y: 0, z: 0 };
group.traverse(child => {
if (child instanceof THREE.Mesh) {
const box = new THREE.Box3().setFromObject(group);
// 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 meshZ = Math.abs(max.z - min.z);
const scaleFactor = 10 / Math.max(meshX, meshY);
group.scale.set(scaleFactor, scaleFactor, scaleFactor);
group.position.y = meshY / 2 * scaleFactor;
group.position.multiplyScalar(-1);
group.position.y += meshY * scaleFactor;
}
setControlsTarget(point) {
this.controls.target.fromArray([point.x, point.y, point.z]);
this.controls.update();
}
startLoader() {
const { source } = this.props;
source &&
ThreeLoader(source, this.renderModel.bind(this), {
onStart: this.handleStart(this),
onLoad: this.handleReady.bind(this),
onError: this.handleError.bind(this),
onProgress: this.handleProgress.bind(this),
});
}
handleStart() {
this.setState({ isLoading: true });
}
handleReady() {
// Handle load ready
this.setState({ isReady: true, isLoading: false });
}
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);
};
handleError(url) {
this.setState({ error: "Sorry, looks like we can't load this file" });
}
handleProgress(url, currentItem, totalItems) {
2018-06-09 22:33:31 +02:00
// Handle progress
// TODO: Show progressbar...
2018-06-06 08:06:03 +02:00
}
handleColorChange(color) {
if (!this.mesh) return;
const pickColor = this.materialColors[color] || this.materialColors.green;
this.mesh.material.color.set(pickColor);
this.wireframe.material.color.set(pickColor);
}
renderModel(fileType, data) {
const geometry = this.createGeometry(data);
this.mesh = this.createMesh(geometry);
}
renderScene() {
this.renderer = ThreeRenderer({
antialias: true,
shadowMap: true,
});
this.scene = ThreeScene({
showFog: true,
showGrid: true,
...this.theme,
});
const viewer = this.viewer.current;
const canvas = this.renderer.domElement;
const { offsetWidth: width, offsetHeight: height } = viewer;
// 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);
// Load file and render mesh
this.startLoader();
const updateScene = () => {
requestAnimationFrame(updateScene);
this.controls.update();
this.renderer.render(this.scene, this.camera);
};
updateScene();
// Append canvas
viewer.appendChild(canvas);
}
componentDidMount() {
if (detectWebGL()) {
this.renderScene();
// Update render on resize window
window.addEventListener('resize', this.handleResize, false);
} else {
// No webgl support, handle Error...
2018-06-09 22:33:31 +02:00
// TODO: Use a better error message
2018-06-06 08:06:03 +02:00
this.state({ error: 'No webgl support!' });
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
}
render() {
const { isReady, isLoading, error } = this.state;
const loadingMessage = 'Rendering model.';
const showViewer = isReady && !error;
const showLoading = isLoading && !error;
return (
<React.Fragment>
{error && <LoadingScreen status={error} />}
{showLoading && <LoadingScreen status={loadingMessage} />}
<div style={{ opacity: isReady ? 1 : 0 }} className="three-viewer" ref={this.viewer} />
</React.Fragment>
);
}
}
export default ThreeViewer;