Doodle3D-Core/src/components/D3Panel.js

256 lines
8.3 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as THREE from 'three';
import injectSheet from 'react-jss';
import { CANVAS_SIZE } from '../constants/d2Constants';
import ShapesManager from '../d3/ShapesManager';
import { getSelectedObjectsSelector, getBoundingBox } from '../utils/selectionUtils';
import createRAFOnce from '../utils/rafOnce.js';
import { hasExtensionsFor } from '../utils/webGLSupport.js';
import { PIXEL_RATIO } from '../constants/general.js';
import * as toolsNames from '../constants/d3Tools.js';
import { EventScene, EventObject3D } from '../d3/EventScene.js';
import HeightTransformer from '../d3/transformers/HeightTransformer.js';
import TwistTransformer from '../d3/transformers/TwistTransformer.js';
import SculptTransformer from '../d3/transformers/SculptTransformer.js';
import StampTransformer from '../d3/transformers/StampTransformer.js';
import SelectionBox from '../d3/SelectionBox.js';
import RenderChain from '../d3/RenderChain';
import BaseTransformer from '../d3/transformers/BaseTransformer.js';
import Camera from '../d3/Camera.js';
import ReactResizeDetector from 'react-resize-detector';
// import createDebug from 'debug';
// const debug = createDebug('d3d:d3');
const rafOnce = createRAFOnce();
const CANVAS_WIDTH = CANVAS_SIZE * 2;
const CANVAS_HEIGHT = CANVAS_SIZE * 2;
const tools = {
[toolsNames.HEIGHT]: HeightTransformer,
[toolsNames.SCULPT]: SculptTransformer,
[toolsNames.TWIST]: TwistTransformer,
[toolsNames.STAMP]: StampTransformer
};
// TODO the same as 3d
const styles = {
container: {
flexGrow: 1,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
overflow: 'hidden'
},
canvasContainer: {
flexGrow: 1,
margin: '0px',
overflow: 'hidden',
'& canvas': {
position: 'absolute'
}
}
};
class D3Panel extends React.Component {
static propTypes = {
state: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
classes: PropTypes.objectOf(PropTypes.string)
};
needRender = false;
componentWillMount() {
this.createScene();
this.renderChain = new RenderChain(this.renderer, this.scene, this.camera, hasExtensionsFor.toonShaderPreview, {
UI: this.UIContainer,
shapes: this.shapesManager,
boundingBox: this.selectionBox,
plane: this.plane
});
this.DOM = null;
}
componentDidMount() {
this.container = this.refs.canvasContainer;
this.container.appendChild(this.renderer.domElement);
this.renderScene(); // immidiatly render because when THREE.JS inits, a black screen is generated
}
componentWillUnmount() {
this.scene.setCanvas(null);
}
createScene() {
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
this.scene = new EventScene(this.renderer.domElement);
this.camera = new Camera(50, 1, 1, 10000);
this.scene.add(this.camera);
// this.grid = new THREE.GridHelper(200, 20);
// this.grid.setColors(0xe0e0e0, 0xeeeeee);
// this.scene.add(this.grid);
const geometryPlane = new THREE.PlaneGeometry(CANVAS_WIDTH, CANVAS_HEIGHT);
geometryPlane.merge(new THREE.PlaneGeometry(CANVAS_WIDTH, CANVAS_HEIGHT), undefined, 1);
const materialPlane = new THREE.MultiMaterial([
new THREE.MeshBasicMaterial({ color: 0xcccccc, side: THREE.BackSide, transparent: true, opacity: 0.3 }),
new THREE.MeshBasicMaterial({ color: 0xcccccc, side: THREE.FrontSide, transparent: true, opacity: 0.1 })
]);
this.plane = new THREE.Mesh(geometryPlane, materialPlane);
this.plane.rotation.x = Math.PI / 2;
this.plane.position.y = -0.01;
this.plane.name = 'bed-plane';
this.plane.isBedPlane = true;
this.scene.add(this.plane);
const directionalLight = new THREE.PointLight(0xffffff, 0.6);
this.camera.add(directionalLight);
const ambientLight = new THREE.AmbientLight(0x505050);
this.scene.add(ambientLight);
this.shapesManager = new ShapesManager({ toonShader: hasExtensionsFor.toonShaderPreview });
this.UIContainer = new EventObject3D();
this.UIContainer.matrixAutoUpdate = false;
this.UIContainer.name = 'ui-container';
this.scene.add(this.shapesManager, this.UIContainer);
this.selectionBox = new SelectionBox();
this.scene.add(this.selectionBox);
}
updateTool(newState) {
this.switchTool(newState.d3.tool);
if (this.tool) {
const toolNeedRender = this.tool.update(newState);
if (toolNeedRender) this.activeNeedRender = true;
}
}
switchTool(toolName) {
if (this.state && toolName === this.state.d3.tool) return;
// cleanup & remove previous tool
if (this.tool) {
this.tool.destroy();
this.UIContainer.remove(this.tool);
}
// initialize new tool
// The tool itself is added to the active object container (Objects Container Space), which is
// influenced by zooming and panning, but the tool also needs to add objects to the container that
// isn't; the active scene (Scene Space). For example the wheelContainer, because it needs a
// mouse position, regardless of zoom level.
// Each tool also get's the renderRequest callback to request re-renders,
// even when it won't change the state
const ToolClass = tools[toolName] ? tools[toolName] : BaseTransformer;
this.tool = new ToolClass(this.props.dispatch, this.scene, this.camera, this.renderer.domElement);
this.UIContainer.add(this.tool);
this.needRender = true;
}
update(newState) {
if (this.state === newState) return;
if (!this.state || newState.activeSpace !== this.state.activeSpace) {
const spaceMatrix = newState.spaces[newState.activeSpace].matrix;
this.UIContainer.matrix.copy(spaceMatrix);
this.UIContainer.updateMatrixWorld(true);
this.selectionBox.matrix.copy(spaceMatrix);
this.selectionBox.updateMatrixWorld(true);
}
const selection = (this.state) ? this.state.selection : null;
const newSelection = newState.selection;
this.updateTool(newState);
const meshNeedRender = this.shapesManager.update(newState);
if (meshNeedRender) this.needRender = true;
const hasSelections = newSelection.objects.length > 0;
if (!this.state || newSelection !== selection) {
const newSelectedObjects = newSelection.objects;
if (!selection || selection.objects !== newSelectedObjects) {
const selected = newSelectedObjects.map((object) => object.id);
this.shapesManager.updateTransparent(selected);
this.needRender = true;
this.selectionBox.visible = hasSelections;
}
if (hasSelections) {
this.selectionBox.updateTransform(newSelection.transform);
this.needRender = true;
}
}
if (!this.state || this.state.d3.camera !== newState.d3.camera) {
this.camera.update(newState.d3.camera);
this.needRender = true;
}
// NOTE: if there is a selection, any state change will trigger this:
if (hasSelections) {
const objectsById = newState.objectsById;
const selectedShapeDatas = getSelectedObjectsSelector(newSelection.objects, objectsById);
const boundingBox = getBoundingBox(selectedShapeDatas, newSelection.transform);
// TODO: add selected shape datas and boundingbox diff checking
this.selectionBox.updateBoundingBox(boundingBox);
}
this.state = newState;
}
resizeHandler = (width, height) => {
// set renderer size
this.renderChain.setSize(width, height, PIXEL_RATIO, false);
this.renderRequest();
};
renderRequest = () => {
this.needRender = true;
rafOnce(this.renderScene);
};
render() {
const { state, classes } = this.props;
this.update(state);
this.renderScene();
return (
<div className={classes.container}>
<ReactResizeDetector handleWidth handleHeight onResize={this.resizeHandler} />
<div className={classes.canvasContainer} ref="canvasContainer"/>
</div>
);
}
renderScene = () => {
if (this.needRender) {
this.scene.updateMatrixWorld();
this.renderChain.render();
this.needRender = false;
}
};
}
// Wrap the component to inject dispatch and state into it
export default injectSheet(styles)(connect(state => ({
state: state.sketcher.present
}))(D3Panel));