Doodle3D-Slicer/src/interface/index.js

468 lines
14 KiB
JavaScript
Raw Normal View History

2018-01-18 12:06:14 +01:00
import * as THREE from 'three';
2017-11-11 20:23:45 +01:00
import React from 'react';
import PropTypes from 'proptypes';
2018-01-22 18:12:33 +01:00
import { centerGeometry, placeOnGround, createScene, slice, TabTemplate } from './utils.js';
2017-11-11 20:23:45 +01:00
import injectSheet from 'react-jss';
2017-11-12 16:58:59 +01:00
import RaisedButton from 'material-ui/RaisedButton';
2017-12-04 15:08:29 +01:00
import LinearProgress from 'material-ui/LinearProgress';
2017-12-04 17:51:56 +01:00
import { grey50, grey300, grey800, red500 } from 'material-ui/styles/colors';
2017-12-24 17:18:33 +01:00
import Popover from 'material-ui/Popover/Popover';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
2017-12-04 17:44:08 +01:00
import { Tabs, Tab } from 'material-ui/Tabs';
2017-11-13 02:09:39 +01:00
import Settings from './Settings.js';
2017-11-13 12:42:35 +01:00
import ReactResizeDetector from 'react-resize-detector';
import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData';
import createSceneData from 'doodle3d-core/d3/createSceneData.js';
import { generateExportMesh } from 'doodle3d-core/utils/exportUtils.js';
2018-01-17 13:26:30 +01:00
import muiThemeable from 'material-ui/styles/muiThemeable';
2018-01-30 00:19:05 +01:00
import logo from '../../img/logo.png';
2017-11-13 02:47:53 +01:00
2017-12-04 15:08:29 +01:00
const MAX_FULLSCREEN_WIDTH = 720;
2017-11-11 20:23:45 +01:00
const styles = {
container: {
2017-11-12 18:41:00 +01:00
position: 'relative',
2017-11-13 12:42:35 +01:00
display: 'flex',
height: '100%',
2017-12-04 17:51:56 +01:00
backgroundColor: grey50,
color: grey800,
2017-12-04 15:08:29 +01:00
overflow: 'hidden',
fontFamily: 'roboto, sans-serif'
2017-11-11 20:23:45 +01:00
},
controlBar: {
position: 'absolute',
2017-11-12 16:58:59 +01:00
bottom: '10px',
left: '10px'
2017-11-11 20:23:45 +01:00
},
2017-11-13 12:42:35 +01:00
d3View: {
2017-12-04 15:08:29 +01:00
flexGrow: 1,
flexBasis: 0
2017-11-13 12:42:35 +01:00
},
canvas: {
position: 'absolute'
},
2017-12-04 17:44:08 +01:00
settingsBar: {
2017-12-04 15:08:29 +01:00
display: 'flex',
flexDirection: 'column',
2018-01-17 12:21:36 +01:00
maxWidth: '320px',
2017-12-04 15:08:29 +01:00
boxSizing: 'border-box',
2018-01-17 12:21:36 +01:00
padding: '10px 20px',
2017-11-13 12:42:35 +01:00
backgroundColor: 'white',
borderLeft: `1px solid ${grey300}`
2017-11-12 12:34:50 +01:00
},
sliceActions: {
2018-04-17 14:17:28 +02:00
flexShrink: 0
2017-12-04 15:08:29 +01:00
},
2018-01-17 16:52:14 +01:00
sliceInfo: {
2018-01-29 17:12:27 +01:00
margin: '10px 0',
'& p': {
2018-02-12 17:36:07 +01:00
marginBottom: '5px',
fontSize: '11px'
2018-01-29 17:12:27 +01:00
}
2018-01-17 16:52:14 +01:00
},
2017-12-04 15:08:29 +01:00
sliceButtons: {
justifyContent: 'flex-end',
display: 'flex'
2017-11-12 18:41:00 +01:00
},
button: {
2017-12-04 15:08:29 +01:00
margin: '5px 0 5px 5px'
2017-11-13 11:01:57 +01:00
},
controlButton: {
2018-01-30 00:19:05 +01:00
marginRight: '5px'
2017-12-04 15:08:29 +01:00
},
buttonContainer: {
width: '100%',
padding: '10px'
},
error: {
color: red500
2017-12-04 17:51:56 +01:00
},
title: {
userSelect: 'none',
2018-01-17 09:11:15 +01:00
position: 'absolute',
left: '10px'
2018-01-17 08:40:48 +01:00
},
detail: {
userSelect: 'none',
2018-01-30 00:19:05 +01:00
marginTop: '10px',
2018-01-17 08:40:48 +01:00
marginBottom: '10px'
2018-01-30 11:07:52 +01:00
},
logo: {
position: 'absolute',
left: '20px',
top: '20px',
width: '150px',
height: '51px'
2017-11-11 20:23:45 +01:00
}
};
class Interface extends React.Component {
2017-11-16 14:54:47 +01:00
static propTypes = {
fileUrl: PropTypes.string,
selectedPrinter: PropTypes.string,
mesh: PropTypes.shape({ isMesh: PropTypes.oneOf([true]) }),
2017-11-16 14:54:47 +01:00
classes: PropTypes.objectOf(PropTypes.string),
2017-12-04 19:31:15 +01:00
pixelRatio: PropTypes.number.isRequired,
onCancel: PropTypes.func,
2018-01-17 13:26:30 +01:00
name: PropTypes.string.isRequired,
2018-01-17 17:42:58 +01:00
muiTheme: PropTypes.object.isRequired,
2018-03-07 18:21:41 +01:00
allowDragDrop: PropTypes.bool.isRequired,
2018-03-07 19:13:05 +01:00
actions: PropTypes.arrayOf(PropTypes.shape({ target: PropTypes.string }))
2017-11-16 14:54:47 +01:00
};
static defaultProps = {
2018-03-07 18:21:41 +01:00
actions: [{
2018-03-07 19:13:05 +01:00
target: 'WIFI_PRINT',
2018-03-07 18:21:41 +01:00
title: 'Print over WiFi'
}, {
2018-03-07 19:13:05 +01:00
target: 'DOWNLOAD',
2018-03-07 18:21:41 +01:00
title: 'Download GCode'
}],
pixelRatio: 1,
2018-01-17 17:42:58 +01:00
name: 'Doodle3D',
allowDragDrop: true
2017-11-16 14:54:47 +01:00
};
2017-11-12 01:41:05 +01:00
constructor(props) {
super(props);
2018-01-16 17:57:34 +01:00
const scene = createScene(this.props);
2017-11-12 01:41:05 +01:00
this.state = {
2018-01-16 17:57:34 +01:00
scene,
settings: null,
2018-01-29 12:30:05 +01:00
showFullScreen: window.innerWidth > MAX_FULLSCREEN_WIDTH,
2017-11-12 16:58:59 +01:00
isSlicing: false,
2017-12-04 15:08:29 +01:00
error: null,
2018-01-17 23:53:40 +01:00
mesh: null,
2018-01-17 08:40:48 +01:00
objectDimensions: '0x0x0mm',
2018-01-25 17:57:38 +01:00
popover: { open: false, element: null }
2017-11-12 01:41:05 +01:00
};
}
2017-11-11 20:23:45 +01:00
componentDidMount() {
const { canvas } = this.refs;
2018-01-16 17:57:34 +01:00
const { scene } = this.state;
scene.updateCanvas(canvas);
const { mesh, fileUrl } = this.props;
if (mesh) {
this.updateMesh(mesh, scene);
} else if (fileUrl) {
2018-01-17 17:42:58 +01:00
this.loadFile(fileUrl);
}
}
2018-01-17 17:42:58 +01:00
loadFile = (fileUrl) => {
const { origin, pathname, password, username, port } = new URL(fileUrl);
const headers = {};
if (password && username) headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
fetch(`${origin}${port}${pathname}`, { headers })
2018-01-17 17:42:58 +01:00
.then(resonse => resonse.json())
.then(json => JSONToSketchData(json))
.then(file => createSceneData(file))
2018-01-18 12:06:14 +01:00
.then(sketch => generateExportMesh(sketch, { offsetSingleWalls: false, matrix: new THREE.Matrix4() }))
2018-01-17 17:42:58 +01:00
.then(mesh => this.updateMesh(mesh));
};
updateMesh(mesh, scene = this.state.scene) {
scene.mesh.geometry = mesh.geometry;
centerGeometry(scene.mesh);
placeOnGround(scene.mesh);
2018-01-17 08:40:48 +01:00
this.calculateDimensions();
scene.render();
2018-01-17 23:53:40 +01:00
this.setState({ mesh });
2017-11-11 20:23:45 +01:00
}
2017-12-04 15:08:29 +01:00
componentWillUnmount() {
2018-01-30 12:01:37 +01:00
const { scene: { editorControls, mesh: { material }, renderer } } = this.state;
editorControls.dispose();
material.dispose();
renderer.dispose();
2017-12-04 15:08:29 +01:00
}
2017-11-11 20:23:45 +01:00
resetMesh = () => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
2017-11-11 20:23:45 +01:00
if (mesh) {
mesh.position.set(0, 0, 0);
mesh.scale.set(1, 1, 1);
mesh.rotation.set(0, 0, 0);
mesh.updateMatrix();
placeOnGround(mesh);
2018-01-17 08:40:48 +01:00
this.calculateDimensions();
2017-11-11 20:23:45 +01:00
render();
}
2017-11-12 01:04:26 +01:00
};
2017-12-04 15:08:29 +01:00
scaleUp = () => this.scaleMesh(0.9);
scaleDown = () => this.scaleMesh(1.0 / 0.9);
scaleMesh = (factor) => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
2017-12-04 15:08:29 +01:00
if (mesh) {
mesh.scale.multiplyScalar(factor);
mesh.updateMatrix();
placeOnGround(mesh);
2018-01-17 08:40:48 +01:00
this.calculateDimensions();
2017-12-04 15:08:29 +01:00
render();
}
};
2017-11-12 01:04:26 +01:00
2018-01-18 12:06:14 +01:00
rotateX = () => this.rotate(new THREE.Vector3(0, 0, 1), Math.PI / 2.0);
rotateY = () => this.rotate(new THREE.Vector3(1, 0, 0), Math.PI / 2.0);
rotateZ = () => this.rotate(new THREE.Vector3(0, 1, 0), Math.PI / 2.0);
2017-12-05 11:10:17 +01:00
rotate = (axis, angle) => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
2017-12-04 15:08:29 +01:00
if (mesh) {
2017-12-06 11:54:09 +01:00
mesh.rotateOnWorldAxis(axis, angle);
2017-12-04 15:08:29 +01:00
placeOnGround(mesh);
2018-01-17 08:40:48 +01:00
this.calculateDimensions();
2017-12-04 15:08:29 +01:00
render();
}
2017-11-12 01:04:26 +01:00
};
2017-11-11 20:23:45 +01:00
2018-03-07 18:21:41 +01:00
slice = async (action) => {
2018-04-17 14:17:28 +02:00
const { isSlicing, settings, mesh, scene: { mesh: { matrix } } } = this.state;
const { name } = this.props;
2017-11-12 00:46:00 +01:00
if (isSlicing) return;
2018-01-23 16:35:04 +01:00
if (!settings) {
this.setState({ error: 'please select a printer first' });
2018-01-25 17:57:38 +01:00
return;
2018-01-23 16:35:04 +01:00
}
2018-03-07 18:21:41 +01:00
if (action.target === 'WIFI_PRINT' && !settings.ip) {
2018-01-23 16:35:04 +01:00
this.setState({ error: 'please connect to a WiFi enabled printer' });
return;
}
if (!mesh) {
this.setState({ error: 'there is no file to slice' });
return;
}
2017-11-12 00:46:00 +01:00
2017-12-24 17:18:33 +01:00
this.closePopover();
this.setState({ isSlicing: true, progress: { action: '', percentage: 0, step: 0 }, error: null });
2017-11-12 12:34:50 +01:00
2018-01-18 12:06:14 +01:00
const exportMesh = new THREE.Mesh(mesh.geometry, mesh.material);
exportMesh.applyMatrix(matrix);
2017-12-04 15:08:29 +01:00
try {
2018-01-16 17:57:34 +01:00
const updateProgres = progress => this.setState({ progress: { ...this.state.progress, ...progress } });
2018-03-07 18:21:41 +01:00
await slice(action, name, exportMesh, settings, updateProgres);
2017-12-04 15:08:29 +01:00
} catch (error) {
2018-01-25 17:57:38 +01:00
this.setState({ error: error.message });
throw error;
} finally {
this.setState({ isSlicing: false });
2017-12-04 15:08:29 +01:00
}
2017-11-12 00:11:05 +01:00
};
2017-12-24 17:18:33 +01:00
openPopover = (event) => {
event.preventDefault();
this.setState({
popover: {
element: event.currentTarget,
open: true
}
});
};
closePopover = () => {
this.setState({
popover: {
element: null,
open: false
}
});
};
2017-12-04 17:44:08 +01:00
componentDidUpdate() {
2018-01-15 14:21:42 +01:00
const { scene: { updateCanvas } } = this.state;
2017-12-04 17:44:08 +01:00
const { canvas } = this.refs;
if (updateCanvas && canvas) updateCanvas(canvas);
}
2017-12-04 15:08:29 +01:00
onResize3dView = (width, height) => {
2017-11-13 12:42:35 +01:00
window.requestAnimationFrame(() => {
2018-01-15 14:21:42 +01:00
const { scene: { setSize } } = this.state;
2017-11-13 12:42:35 +01:00
const { pixelRatio } = this.props;
if (setSize) setSize(width, height, pixelRatio);
2017-11-13 12:42:35 +01:00
});
};
2017-12-04 15:08:29 +01:00
onResizeContainer = (width) => {
2017-12-04 17:44:08 +01:00
this.setState({ showFullScreen: width > MAX_FULLSCREEN_WIDTH });
2017-12-04 15:08:29 +01:00
};
2018-01-16 17:57:34 +01:00
onChangeSettings = (settings) => {
const { scene: { box, render } } = this.state;
let changed = false;
if (!this.state.settings || this.state.settings.dimensions !== settings.dimensions) {
box.scale.set(settings.dimensions.y, settings.dimensions.z, settings.dimensions.x);
box.updateMatrix();
changed = true;
}
if (changed) render();
this.setState({ settings, error: null });
};
2018-01-17 08:40:48 +01:00
calculateDimensions = () => {
const { scene: { mesh } } = this.state;
2018-01-18 12:06:14 +01:00
const { x, y, z } = new THREE.Box3().setFromObject(mesh).getSize();
this.setState({ objectDimensions: `${Math.round(y)}x${Math.round(z)}x${Math.round(x)}mm` });
2018-01-17 08:40:48 +01:00
};
2018-01-17 17:42:58 +01:00
onDrop = (event) => {
event.preventDefault();
if (!this.props.allowDragDrop) return;
for (const file of event.dataTransfer.files) {
const extentions = file.name.split('.').pop();
switch (extentions.toUpperCase()) {
case 'D3SKETCH':
this.loadFile(URL.createObjectURL(file));
break;
default:
break;
}
}
}
2017-11-11 20:23:45 +01:00
render() {
2018-03-07 18:21:41 +01:00
const { classes, onCancel, selectedPrinter, actions } = this.props;
2018-04-17 14:17:28 +02:00
const { isSlicing, progress, showFullScreen, error, objectDimensions } = this.state;
2017-12-04 15:08:29 +01:00
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
2017-12-04 15:08:29 +01:00
2017-12-04 17:44:08 +01:00
const settingsPanel = (
<div className={classes.settingsBar} style={style}>
2017-12-04 17:44:08 +01:00
<Settings
selectedPrinter={selectedPrinter}
disabled={isSlicing}
2017-12-04 17:44:08 +01:00
onChange={this.onChangeSettings}
/>
<div className={classes.sliceActions}>
2018-01-17 16:52:14 +01:00
<div className={classes.sliceInfo}>
{error && <p className={classes.error}>{error}</p>}
{isSlicing && <p>{progress.action}</p>}
{isSlicing && <LinearProgress mode="determinate" value={progress.percentage * 100.0} />}
</div>
2017-12-04 17:44:08 +01:00
<div className={classes.sliceButtons}>
2017-12-04 19:31:15 +01:00
{onCancel && <RaisedButton
2018-02-06 17:21:39 +01:00
label="Close"
2017-12-04 19:31:15 +01:00
className={`${classes.button}`}
onTouchTap={onCancel}
/>}
2018-03-07 18:21:41 +01:00
{actions.length === 1 ? (
<RaisedButton
primary
label={actions[0].title}
onTouchTap={() => this.slice(actions[0])}
className={`${classes.button}`}
disabled={isSlicing}
/>
) : (
<span>
<RaisedButton
label="Print"
ref="button"
primary
className={`${classes.button}`}
onTouchTap={this.openPopover}
disabled={isSlicing}
/>
<Popover
open={this.state.popover.open}
anchorEl={this.state.popover.element}
2018-04-17 14:17:28 +02:00
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'left', vertical: 'bottom' }}
2018-03-07 18:21:41 +01:00
onRequestClose={this.closePopover}
>
<Menu>
{actions.map((action) => (
2018-03-15 14:26:04 +01:00
<MenuItem key={action.target} primaryText={action.title} onTouchTap={() => this.slice(action)} />
2018-03-07 18:21:41 +01:00
))}
</Menu>
</Popover>
</span>
)}
2017-12-04 15:08:29 +01:00
</div>
</div>
2017-11-11 20:23:45 +01:00
</div>
);
2017-12-04 17:44:08 +01:00
const d3Panel = (
<div className={classes.d3View}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
<canvas className={classes.canvas} ref="canvas" />
<div className={classes.controlBar}>
2018-01-17 08:40:48 +01:00
<div className={classes.detail}>
<p>Dimensions: {objectDimensions}</p>
</div>
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateX} label="rotate x" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateY} label="rotate y" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateZ} label="rotate z" />
</div>
2017-12-04 17:44:08 +01:00
</div>
);
if (showFullScreen) {
return (
2018-01-17 17:42:58 +01:00
<div
className={classes.container}
ref={(container) => {
if (container) {
container.addEventListener('dragover', event => event.preventDefault());
container.addEventListener('drop', this.onDrop);
}
}}
>
2017-12-04 17:44:08 +01:00
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
2018-01-30 11:07:52 +01:00
<img src={logo} className={classes.logo} />
2017-12-04 17:44:08 +01:00
{d3Panel}
{settingsPanel}
</div>
);
} else {
return (
2018-01-17 17:42:58 +01:00
<div
className={classes.container}
ref={(container) => {
if (container) {
container.addEventListener('dragover', event => event.preventDefault());
container.addEventListener('drop', this.onDrop);
}
}}
>
2017-12-04 17:44:08 +01:00
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
<Tabs
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
tabItemContainerStyle={{ flexShrink: 0 }}
contentContainerStyle={{ flexGrow: 1, display: 'flex' }}
tabTemplateStyle={{ display: 'flex' }}
tabTemplate={TabTemplate}
>
<Tab label="Settings">
{settingsPanel}
</Tab>
2017-12-04 17:45:28 +01:00
<Tab label="Edit Model">
2017-12-04 17:44:08 +01:00
{d3Panel}
</Tab>
</Tabs>
</div>
);
}
2017-11-11 20:23:45 +01:00
}
}
2018-01-17 13:26:30 +01:00
export default muiThemeable()(injectSheet(styles)(Interface));