lbry-desktop/ui/component/viewers/threeViewer/index.jsx

506 lines
14 KiB
React
Raw Permalink Normal View History

2018-06-06 08:06:03 +02:00
// @flow
import * as React from 'react';
2018-08-14 06:03:46 +02:00
import * as dat from 'dat.gui';
2018-06-06 08:06:03 +02:00
import LoadingScreen from 'component/common/loading-screen';
2018-08-14 06:03:46 +02:00
2018-06-13 00:40:55 +02:00
// ThreeJS
2019-03-07 04:37:25 +01:00
import { LoadingManager } from 'three-full/sources/loaders/LoadingManager';
import { STLLoader } from 'three-full/sources/loaders/STLLoader';
import { OBJLoader2 } from 'three-full/sources/loaders/OBJLoader2';
import { OrbitControls } from 'three-full/sources/controls/OrbitControls';
import { Geometry } from 'three-full/sources/core/Geometry';
import { Box3 } from 'three-full/sources/math/Box3';
import { Vector3 } from 'three-full/sources/math/Vector3';
import { Mesh } from 'three-full/sources/objects/Mesh';
import { Group } from 'three-full/sources/objects/Group';
import { PerspectiveCamera } from 'three-full/sources/cameras/PerspectiveCamera';
import { MeshPhongMaterial } from 'three-full/sources/materials/MeshPhongMaterial';
2018-06-13 00:40:55 +02:00
import detectWebGL from './internal/detector';
import ThreeGrid from './internal/grid';
2018-06-13 00:40:55 +02:00
import ThreeScene from './internal/scene';
import ThreeRenderer from './internal/renderer';
2018-06-06 08:06:03 +02:00
2019-03-05 05:46:57 +01:00
const Manager = ({ onLoad, onStart, onError }) => {
2019-03-07 04:37:25 +01:00
const manager = new LoadingManager();
2019-03-05 05:46:57 +01:00
manager.onLoad = onLoad;
manager.onStart = onStart;
manager.onError = onError;
return manager;
};
const Loader = (fileType, manager) => {
const fileTypes = {
2019-03-07 04:37:25 +01:00
stl: () => new STLLoader(manager),
obj: () => new OBJLoader2(manager),
2019-03-05 05:46:57 +01:00
};
return fileTypes[fileType] ? fileTypes[fileType]() : null;
};
const ThreeLoader = ({ fileType = null, downloadPath = null }, renderModel, managerEvents) => {
if (fileType && downloadPath) {
const manager = Manager(managerEvents);
const loader = Loader(fileType, manager);
if (loader) {
loader.load(`file://${downloadPath}`, data => {
renderModel(fileType, data);
});
}
}
};
type viewerTheme = {
showFog: boolean,
gridColor: number,
backgroundColor: number,
centerLineColor: number,
};
2018-06-06 08:06:03 +02:00
type Props = {
source: {
fileType: string,
downloadPath: string,
2018-06-06 08:06:03 +02:00
},
};
type State = {
error: ?string,
isReady: boolean,
isLoading: boolean,
};
class ThreeViewer extends React.PureComponent<Props, State> {
2018-10-15 08:26:46 +02:00
static testWebgl = (): Promise<void> =>
new Promise((resolve, reject) => {
if (detectWebGL()) resolve();
else reject();
});
static createOrbitControls(camera: any, canvas: any) {
2019-03-07 04:37:25 +01:00
const controls = new OrbitControls(camera, canvas);
2018-08-16 03:47:08 +02:00
// Controls configuration
controls.enableDamping = true;
controls.dampingFactor = 0.75;
controls.enableZoom = true;
controls.minDistance = 5;
controls.maxDistance = 14;
controls.autoRotate = false;
controls.enablePan = false;
controls.saveState();
return controls;
}
static fitMeshToCamera(group: any) {
const max = { x: 0, y: 0, z: 0 };
const min = { x: 0, y: 0, z: 0 };
group.traverse(child => {
2019-03-07 04:37:25 +01:00
if (child instanceof Mesh) {
const box = new 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
2019-03-07 04:37:25 +01:00
const box = new Box3().setFromObject(group);
// Update position
2018-08-18 07:07:33 +02:00
box.getCenter(group.position);
group.position.multiplyScalar(-1);
group.position.setY(group.position.y + meshY * scaleFactor);
}
/*
See: https://github.com/mrdoob/three.js/blob/dev/docs/scenes/js/material.js#L195
*/
2018-06-06 08:06:03 +02:00
constructor(props: Props) {
super(props);
2018-08-15 02:54:27 +02:00
// Object defualt color
this.materialColor = 0xffffff;
// Default viewer Theme
this.theme = {
showFog: true,
gridColor: 0x414e5c,
centerLineColor: 0x7f8c8d,
backgroundColor: 0x0b0c0d,
2018-06-06 08:06:03 +02:00
};
2018-06-11 05:51:39 +02:00
// State
this.state = {
error: null,
isReady: false,
isLoading: false,
};
// Internal objects
this.gui = null;
this.grid = null;
this.mesh = null;
this.camera = null;
this.frameID = null;
this.renderer = null;
this.material = null;
this.geometry = null;
this.targetCenter = null;
this.bufferGeometry = null;
2018-06-06 08:06:03 +02:00
}
2018-06-13 00:40:55 +02:00
componentDidMount() {
2018-10-15 18:01:27 +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
if (this.renderer && this.mesh) {
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-15 02:54:27 +02:00
// Cleanup shared geometry
if (this.geometry) this.geometry.dispose();
if (this.bufferGeometry) this.bufferGeometry.dispose();
// Clean up shared material
2018-08-15 02:54:27 +02:00
if (this.material) this.material.dispose();
// Clean up grid
2018-08-15 02:54:27 +02:00
if (this.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 => {
2019-03-07 04:37:25 +01:00
if (child instanceof Mesh) {
2018-08-13 04:22:07 +02:00
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
}
});
2018-08-15 08:15:15 +02:00
// Clean up controls
if (this.controls) this.controls.dispose();
// Clean up scene
if (this.scene) this.scene.dispose();
2018-08-15 02:54:27 +02:00
if (this.renderer) {
this.renderer.context = null;
this.renderer.domElement = null;
2018-08-15 02:54:27 +02:00
this.renderer.renderLists.dispose();
this.renderer.forceContextLoss();
2018-08-15 02:54:27 +02:00
this.renderer.dispose();
this.renderer = null;
2018-08-15 02:54:27 +02:00
}
// Stop animation
cancelAnimationFrame(this.frameID);
2018-08-14 06:03:46 +02:00
// Destroy GUI Controls
if (this.gui) this.gui.destroy();
2018-08-13 04:22:07 +02:00
}
2018-06-13 00:40:55 +02:00
}
// Define component types
theme: viewerTheme;
materialColor: number;
2018-10-15 08:26:46 +02:00
// Refs
viewer: ?HTMLElement;
guiContainer: ?HTMLElement;
// Too complex to add a custom type
gui: any;
grid: any;
mesh: any;
scene: any;
camera: any;
frameID: any;
controls: any;
material: any;
geometry: any;
targetCenter: any;
bufferGeometry: any;
updateMaterial(material: any, geometry: any) {
this.material.vertexColors = +material.vertexColors; // Ensure number
this.material.side = +material.side; // Ensure number
this.material.needsUpdate = true;
// If Geometry needs update
if (geometry) {
this.geometry.verticesNeedUpdate = true;
this.geometry.normalsNeedUpdate = true;
this.geometry.colorsNeedUpdate = true;
}
}
transformGroup(group: any) {
ThreeViewer.fitMeshToCamera(group);
2018-08-18 07:07:33 +02:00
if (!this.targetCenter) {
2019-03-07 04:37:25 +01:00
const box = new Box3();
const center = new Vector3();
this.targetCenter = box.setFromObject(this.mesh).getCenter(center);
2018-08-18 07:07:33 +02:00
}
this.updateControlsTarget(this.targetCenter);
}
2018-08-14 06:03:46 +02:00
createInterfaceControls() {
if (this.guiContainer && this.mesh) {
2018-08-15 08:15:15 +02:00
this.gui = new dat.GUI({ autoPlace: false, name: 'controls' });
2018-08-14 06:03:46 +02:00
const config = {
2018-08-15 02:54:27 +02:00
color: this.materialColor,
reset: () => {
// Reset material color
config.color = this.materialColor;
// Reset material
this.material.color.set(config.color);
this.material.flatShading = true;
this.material.shininess = 30;
this.material.wireframe = false;
// Reset autoRotate
this.controls.autoRotate = false;
// Reset camera
this.restoreCamera();
},
2018-08-15 08:15:15 +02:00
};
2018-08-15 02:54:27 +02:00
const materialFolder = this.gui.addFolder('Material');
2018-08-15 08:15:15 +02:00
// Color picker
const colorPicker = materialFolder
2018-08-18 07:07:33 +02:00
.addColor(config, 'color')
.name('Color')
.listen();
2018-08-14 06:03:46 +02:00
colorPicker.onChange(color => {
2018-08-15 02:54:27 +02:00
this.material.color.set(color);
2018-08-14 06:03:46 +02:00
});
materialFolder
.add(this.material, 'shininess', 0, 100)
.name('Shininess')
.listen();
materialFolder
.add(this.material, 'flatShading')
.name('FlatShading')
.onChange(() => {
this.updateMaterial(this.material);
})
.listen();
materialFolder
2018-08-18 07:07:33 +02:00
.add(this.material, 'wireframe')
.name('Wireframe')
.listen();
const sceneFolder = this.gui.addFolder('Scene');
sceneFolder
2018-08-18 07:07:33 +02:00
.add(this.controls, 'autoRotate')
.name('Auto-Rotate')
.listen();
sceneFolder.add(config, 'reset').name('Reset');
2018-10-15 08:26:46 +02:00
if (this.guiContainer) {
this.guiContainer.appendChild(this.gui.domElement);
}
2018-08-14 06:03:46 +02:00
}
}
createGeometry(data: any) {
2018-08-15 02:54:27 +02:00
this.bufferGeometry = data;
this.bufferGeometry.computeBoundingBox();
this.bufferGeometry.center();
this.bufferGeometry.rotateX(-Math.PI / 2);
2019-03-07 04:37:25 +01:00
this.bufferGeometry.lookAt(new Vector3(0, 0, 1));
2018-08-15 02:54:27 +02:00
// Get geometry from bufferGeometry
2019-03-07 04:37:25 +01:00
this.geometry = new Geometry().fromBufferGeometry(this.bufferGeometry);
2018-08-15 02:54:27 +02:00
this.geometry.mergeVertices();
this.geometry.computeVertexNormals();
2018-08-15 02:54:27 +02:00
}
2018-06-06 08:06:03 +02:00
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-08-14 06:03:46 +02:00
// GUI
this.createInterfaceControls();
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 = () => {
2018-10-15 08:26:46 +02:00
const { offsetWidth: width, offsetHeight: height } = this.viewer || {};
2018-06-06 08:06:03 +02:00
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.controls.update();
this.renderer.setSize(width, height);
};
updateControlsTarget(point: { x: number, y: number, z: number }) {
2018-06-13 00:40:55 +02:00
this.controls.target.fromArray([point.x, point.y, point.z]);
this.controls.update();
}
2018-08-15 08:15:15 +02:00
restoreCamera() {
this.controls.reset();
this.camera.position.set(-9.5, 14, 11);
2018-08-18 07:07:33 +02:00
this.updateControlsTarget(this.targetCenter);
2018-08-15 08:15:15 +02:00
}
// Flow requested to add it here
renderer: any;
renderStl(data: any) {
2018-08-15 02:54:27 +02:00
this.createGeometry(data);
2019-03-07 04:37:25 +01:00
this.mesh = new Mesh(this.geometry, this.material);
2018-08-15 02:54:27 +02:00
this.mesh.name = 'model';
this.scene.add(this.mesh);
this.transformGroup(this.mesh);
}
renderObj(event: any) {
const mesh = event.detail.loaderRootNode;
2019-03-07 04:37:25 +01:00
this.mesh = new Group();
2018-08-15 02:54:27 +02:00
this.mesh.name = 'model';
// Assign new material
mesh.traverse(child => {
2019-03-07 04:37:25 +01:00
if (child instanceof Mesh) {
// Get geometry from child
2019-03-07 04:37:25 +01:00
const geometry = new Geometry();
geometry.fromBufferGeometry(child.geometry);
geometry.mergeVertices();
geometry.computeVertexNormals();
// Create and regroup inner objects
2019-03-07 04:37:25 +01:00
const innerObj = new Mesh(geometry, this.material);
2018-08-15 02:54:27 +02:00
this.mesh.add(innerObj);
// Clean up geometry
geometry.dispose();
child.geometry.dispose();
}
});
2018-08-15 02:54:27 +02:00
this.scene.add(this.mesh);
this.transformGroup(this.mesh);
}
renderModel(fileType: string, parsedData: any) {
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() {
const { gridColor, centerLineColor } = this.theme;
2018-06-06 08:06:03 +02:00
this.renderer = ThreeRenderer({
antialias: true,
shadowMap: true,
gammaCorrection: true,
2018-06-06 08:06:03 +02:00
});
this.scene = ThreeScene(this.theme);
2018-06-06 08:06:03 +02:00
const canvas = this.renderer.domElement;
2018-10-15 08:26:46 +02:00
const { offsetWidth: width, offsetHeight: height } = this.viewer || {};
// Grid
this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor });
this.scene.add(this.grid);
2018-06-06 08:06:03 +02:00
// Camera
2019-03-07 04:37:25 +01:00
this.camera = new PerspectiveCamera(80, width / height, 0.1, 1000);
2018-06-06 08:06:03 +02:00
this.camera.position.set(-9.5, 14, 11);
2018-06-06 08:06:03 +02:00
// Controls
2018-08-16 03:47:08 +02:00
this.controls = ThreeViewer.createOrbitControls(this.camera, canvas);
2018-06-06 08:06:03 +02:00
// Set viewer size
this.renderer.setSize(width, height);
// Create material
2019-03-07 04:37:25 +01:00
this.material = new MeshPhongMaterial({
2018-08-13 04:22:07 +02:00
depthWrite: true,
2018-08-19 20:40:51 +02:00
flatShading: true,
2019-03-07 04:37:25 +01:00
vertexColors: 1,
});
2018-08-13 04:22:07 +02:00
// Set material color
2018-08-15 02:54:27 +02:00
this.material.color.set(this.materialColor);
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
2018-10-15 08:26:46 +02:00
if (this.viewer) {
this.viewer.appendChild(canvas);
}
2018-08-13 04:22:07 +02:00
2018-06-06 08:06:03 +02:00
const updateScene = () => {
this.frameID = requestAnimationFrame(updateScene);
2018-08-16 03:47:08 +02:00
if (this.controls.autoRotate) 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.');
const showLoading = isLoading && !error && !isReady;
const containerClass = 'gui-container';
2018-06-06 08:06:03 +02:00
return (
<>
<div className="file-render__viewer file-render__viewer--three">
<div ref={element => (this.guiContainer = element)} className={containerClass} />
<div className="three-viewer" ref={viewer => (this.viewer = viewer)}>
{error && <LoadingScreen status={error} spinner={false} />}
{showLoading && <LoadingScreen status={loadingMessage} spinner />}
</div>
</div>
</>
2018-06-06 08:06:03 +02:00
);
}
}
export default ThreeViewer;