react-floorplanner
Version:
react-floorplanner is a React Component for plans design. Draw a 2D floorplan and navigate it in 3D mode.
324 lines (240 loc) • 11.1 kB
JavaScript
"use strict";
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import * as Three from 'three';
import {parseData, updateScene} from './scene-creator';
import {disposeScene} from './three-memory-cleaner';
import diff from 'immutablediff';
import {initPointerLock} from "./pointer-lock-navigation";
import {firstPersonOnKeyDown, firstPersonOnKeyUp} from "./libs/first-person-controls";
import * as SharedStyle from '../../shared-style';
export default class Viewer3DFirstPerson extends React.Component {
constructor(props) {
super(props);
this.width = props.width;
this.height = props.height;
this.stopRendering = false;
this.renderer = window.__threeRenderer || new Three.WebGLRenderer({preserveDrawingBuffer: true});
window.__threeRenderer = this.renderer;
}
componentDidMount() {
/** Variables for movement control **/
let prevTime = performance.now();
let velocity = new Three.Vector3();
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
let {catalog} = this.context;
let actions = {
areaActions: this.context.areaActions,
holesActions: this.context.holesActions,
itemsActions: this.context.itemsActions,
linesActions: this.context.linesActions,
projectActions: this.context.projectActions
};
let {state} = this.props;
let data = state.scene;
let canvasWrapper = ReactDOM.findDOMNode(this.refs.canvasWrapper);
let scene3D = new Three.Scene();
// As I need to show the pointer above all scene objects, I use this workaround http://stackoverflow.com/a/13309722
let sceneOnTop = new Three.Scene();
//RENDERER
this.renderer.setClearColor(new Three.Color(SharedStyle.COLORS.white));
this.renderer.setSize(this.width, this.height);
// LOAD DATA
this.planData = parseData(data, actions, catalog);
scene3D.add(this.planData.plan);
// CAMERA
let aspectRatio = this.width / this.height;
let camera = new Three.PerspectiveCamera(45, aspectRatio, 0.1, 300000);
sceneOnTop.add(camera); // The pointer is on the camera so I show it above all
// Set position for the camera
camera.position.set(0, 0, 0);
camera.up = new Three.Vector3(0, 1, 0);
// HELPER AXIS
// let axisHelper = new Three.AxisHelper(100);
// scene3D.add(axisHelper);
// LIGHT
let light = new Three.AmbientLight(0xafafaf); // soft white light
scene3D.add(light);
// Add another light
let pointLight = new Three.PointLight(SharedStyle.COLORS.white, 0.4, 1000);
pointLight.position.set(0, 0, 0);
scene3D.add(pointLight);
// POINTER LOCK
document.body.requestPointerLock = document.body.requestPointerLock ||
document.body.mozRequestPointerLock ||
document.body.webkitRequestPointerLock;
document.body.requestPointerLock();
let {controls, pointerlockChangeEvent, requestPointerLockEvent} = initPointerLock(camera, this.renderer.domElement);
this.controls = controls;
this.pointerlockChangeListener = pointerlockChangeEvent;
this.requestPointerLockEvent = requestPointerLockEvent;
/* Set user initial position */
let humanHeight = 170; // 170 cm
let yInitialPosition = this.planData.boundingBox.min.y +
(this.planData.boundingBox.min.y - this.planData.boundingBox.max.y) / 2 + humanHeight;
this.controls.getObject().position.set(-50, yInitialPosition, -100);
sceneOnTop.add(this.controls.getObject()); // Add the pointer lock controls to the scene that will be rendered on top
// Add move controls on the page
this.keyDownEvent = (event) => {
let moveResult = firstPersonOnKeyDown(event, moveForward, moveLeft, moveBackward, moveRight);
moveForward = moveResult.moveForward;
moveLeft = moveResult.moveLeft;
moveBackward = moveResult.moveBackward;
moveRight = moveResult.moveRight;
};
this.keyUpEvent = (event) => {
let moveResult = firstPersonOnKeyUp(event, moveForward, moveLeft, moveBackward, moveRight);
moveForward = moveResult.moveForward;
moveLeft = moveResult.moveLeft;
moveBackward = moveResult.moveBackward;
moveRight = moveResult.moveRight;
};
document.addEventListener('keydown', this.keyDownEvent);
document.addEventListener('keyup', this.keyUpEvent);
// Add a pointer to the scene
let pointer = new Three.Object3D();
let pointerMaterial = new Three.MeshBasicMaterial({depthTest: false, depthWrite: false, color: SharedStyle.COLORS.black});
let pointerGeometry1 = new Three.Geometry();
pointerGeometry1.vertices.push(new Three.Vector3(-10, 0, 0));
pointerGeometry1.vertices.push(new Three.Vector3(10, 0, 0));
let linePointer1 = new Three.Line(pointerGeometry1, pointerMaterial);
linePointer1.position.z -= 100;
let pointerGeometry2 = new Three.Geometry();
pointerGeometry2.vertices.push(new Three.Vector3(0, 10, 0));
pointerGeometry2.vertices.push(new Three.Vector3(0, -10, 0));
let linePointer2 = new Three.Line(pointerGeometry2, pointerMaterial);
linePointer2.renderDepth = 1e20;
linePointer2.position.z -= 100;
let pointerGeometry3 = new Three.Geometry();
pointerGeometry3.vertices.push(new Three.Vector3(-1, 1, 0));
pointerGeometry3.vertices.push(new Three.Vector3(1, 1, 0));
pointerGeometry3.vertices.push(new Three.Vector3(1, -1, 0));
pointerGeometry3.vertices.push(new Three.Vector3(-1, -1, 0));
pointerGeometry3.vertices.push(new Three.Vector3(-1, 1, 0));
let linePointer3 = new Three.Line(pointerGeometry3, pointerMaterial);
linePointer3.position.z -= 100;
pointer.add(linePointer1);
pointer.add(linePointer2);
pointer.add(linePointer3);
camera.add(pointer); // Add the pointer to the camera
// OBJECT PICKING
let toIntersect = [this.planData.plan];
let mouseVector = new Three.Vector2(0, 0);
let raycaster = new Three.Raycaster();
this.firstPersonMouseDown = (event) => {
// First of all I check if controls are enabled
if (this.controls.enabled) {
event.preventDefault();
/* Per avere la direzione da assegnare al raycaster, chiamo il metodo getDirection di PointerLockControls,
* che restituisce una funzione che a sua volta prende un vettore, vi scrive i valori degli oggetti
* pitch e yaw e lo restituisce */
raycaster.setFromCamera(mouseVector, camera);
let intersects = raycaster.intersectObjects(toIntersect, true);
if (intersects.length > 0 && !(isNaN(intersects[0].distance))) {
intersects[0].object.interact && intersects[0].object.interact();
} else {
this.context.projectActions.unselectAll();
}
}
};
document.addEventListener('mousedown', this.firstPersonMouseDown, false);
this.renderer.domElement.style.display = 'block';
// add the output of the renderer to the html element
canvasWrapper.appendChild(this.renderer.domElement);
this.renderer.autoClear = false;
let render = () => {
if (!this.stopRendering) {
yInitialPosition = this.planData.boundingBox.min.y + humanHeight;
this.controls.getObject().position.y = yInitialPosition;
let time = performance.now();
let delta = ( time - prevTime ) / 200;
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
if (moveForward) velocity.z -= 400.0 * delta;
if (moveBackward) velocity.z += 400.0 * delta;
if (moveLeft) velocity.x -= 400.0 * delta;
if (moveRight) velocity.x += 400.0 * delta;
this.controls.getObject().translateX(velocity.x * delta);
this.controls.getObject().translateZ(velocity.z * delta);
prevTime = time;
// Set light position
let controlObjectPosition = this.controls.getObject().position;
pointLight.position.set(controlObjectPosition.x, controlObjectPosition.y, controlObjectPosition.z);
for (let elemID in this.planData.sceneGraph.LODs) {
this.planData.sceneGraph.LODs[elemID].update(camera);
}
this.renderer.clear(); // clear buffers
this.renderer.render(scene3D, camera); // render scene 1
this.renderer.clearDepth(); // clear depth buffer
this.renderer.render(sceneOnTop, camera); // render scene 2
requestAnimationFrame(render);
}
};
render();
this.camera = camera;
this.scene3D = scene3D;
this.sceneOnTop = sceneOnTop;
// this.planData = planData;
}
componentWillUnmount() {
this.stopRendering = true;
this.renderer.autoClear = true;
document.removeEventListener('mousedown', this.firstPersonMouseDown);
document.removeEventListener('keydown', this.keyDownEvent);
document.removeEventListener('keyup', this.keyUpEvent);
document.removeEventListener('pointerlockchange', this.pointerlockChangeEvent);
document.removeEventListener('mozpointerlockchange', this.pointerlockChangeEvent);
document.removeEventListener('webkitpointerlockchange', this.pointerlockChangeEvent);
this.renderer.domElement.removeEventListener('click', this.requestPointerLockEvent);
disposeScene(this.scene3D);
this.scene3D.remove(this.planData.plan);
this.scene3D = null;
this.planData = null;
}
componentWillReceiveProps(nextProps) {
let {width, height} = nextProps;
let {camera, renderer, scene3D, sceneOnTop, planData} = this;
let actions = {
areaActions: this.context.areaActions,
holesActions: this.context.holesActions,
itemsActions: this.context.itemsActions,
linesActions: this.context.linesActions,
projectActions: this.context.projectActions
};
this.width = width;
this.height = height;
camera.aspect = width / height;
camera.updateProjectionMatrix();
if (nextProps.scene !== this.props.state.scene) {
let changedValues = diff(this.props.state.scene, nextProps.state.scene);
updateScene(planData, nextProps.state.scene, this.props.state.scene, changedValues.toJS(), actions, this.context.catalog);
}
renderer.setSize(width, height);
renderer.clear(); // clear buffers
renderer.render(scene3D, camera); // render scene 1
renderer.clearDepth(); // clear depth buffer
renderer.render(sceneOnTop, camera); // render scene 2
}
render() {
return React.createElement("div", {
ref: "canvasWrapper"
});
}
}
Viewer3DFirstPerson.propTypes = {
state: PropTypes.object.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired
};
Viewer3DFirstPerson.contextTypes = {
areaActions: PropTypes.object.isRequired,
holesActions: PropTypes.object.isRequired,
itemsActions: PropTypes.object.isRequired,
linesActions: PropTypes.object.isRequired,
projectActions: PropTypes.object.isRequired,
catalog: PropTypes.object
};