Make changes to UI

This commit is contained in:
casperlamboo 2017-12-04 15:08:29 +01:00
parent bc9f0e673e
commit 65d44db405
5 changed files with 312 additions and 248 deletions

View File

@ -5,57 +5,35 @@ import injectSheet from 'react-jss';
import MaterialUISelectField from 'material-ui/SelectField' import MaterialUISelectField from 'material-ui/SelectField'
import MaterialUICheckbox from 'material-ui/Checkbox'; import MaterialUICheckbox from 'material-ui/Checkbox';
import MaterialUITextField from 'material-ui/TextField'; import MaterialUITextField from 'material-ui/TextField';
import { grey100, grey300, grey500 } from 'material-ui/styles/colors';
const styles = { const contextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
fieldSet: {
border: 'none',
backgroundColor: grey100,
marginTop: '20px',
'& legend': {
fontFamily: 'sans-serif',
border: `1px solid ${grey300}`,
backgroundColor: 'white',
padding: '3px 13px',
color: grey500
}
}
};
export const SettingsGroup = injectSheet(styles)(({ name, classes, children }) => (
<fieldset className={classes.fieldSet}>
<legend>{name}</legend>
{children}
</fieldset>
));
SettingsGroup.propTypes = {
classes: PropTypes.objectOf(PropTypes.string),
name: PropTypes.string.isRequired,
children: PropTypes.node
};
export const SelectField = (props, context) => ( export const SelectField = (props, context) => (
<MaterialUISelectField <MaterialUISelectField
{ ...props } { ...props }
disabled={context.disabled}
value={_.get(context.state, props.name)} value={_.get(context.state, props.name)}
onChange={(event, index, value) => context.onChange(props.name, value)} onChange={(event, index, value) => context.onChange(props.name, value)}
/> />
); );
SelectField.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; SelectField.contextTypes = contextTypes;
export const TextField = (props, context) => ( export const TextField = (props, context) => (
<MaterialUITextField <MaterialUITextField
{ ...props } { ...props }
disabled={context.disabled}
value={_.get(context.state, props.name)} value={_.get(context.state, props.name)}
onChange={(event, value) => context.onChange(props.name, value)} onChange={(event, value) => context.onChange(props.name, value)}
/> />
); );
TextField.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; TextField.contextTypes = contextTypes;
export const Checkbox = (props, context) => ( export const Checkbox = (props, context) => (
<MaterialUICheckbox <MaterialUICheckbox
{ ...props } { ...props }
disabled={context.disabled}
checked={_.get(context.state, props.name)} checked={_.get(context.state, props.name)}
onCheck={(event, value) => context.onChange(props.name, value)} onCheck={(event, value) => context.onChange(props.name, value)}
/> />
); );
Checkbox.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; Checkbox.contextTypes = contextTypes;

View File

@ -4,17 +4,29 @@ import _ from 'lodash';
import { Tabs, Tab } from 'material-ui/Tabs'; import { Tabs, Tab } from 'material-ui/Tabs';
import MenuItem from 'material-ui/MenuItem'; import MenuItem from 'material-ui/MenuItem';
import injectSheet from 'react-jss'; import injectSheet from 'react-jss';
import { SettingsGroup, SelectField, TextField, Checkbox } from './FormComponents.js'; import { SelectField, TextField, Checkbox } from './FormComponents.js';
import { grey500 } from 'material-ui/styles/colors'; import { grey900 } from 'material-ui/styles/colors';
const styles = { const styles = {
textFieldRow: { textFieldRow: {
display: 'flex' display: 'flex'
},
text: {
fontWeight: 'bold',
color: grey900
},
container: {
width: '100%',
flexGrow: 1,
overflowY: 'auto'
} }
}; };
class Settings extends React.Component { class Settings extends React.Component {
static childContextTypes = { state: PropTypes.object, onChange: PropTypes.func }; static childContextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
static defaultProps: {
disabled: false
};
static propTypes = { static propTypes = {
classes: PropTypes.objectOf(PropTypes.string), classes: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func, onChange: PropTypes.func,
@ -24,7 +36,8 @@ class Settings extends React.Component {
defaultQuality: PropTypes.string.isRequired, defaultQuality: PropTypes.string.isRequired,
material: PropTypes.object.isRequired, material: PropTypes.object.isRequired,
defaultMaterial: PropTypes.string.isRequired, defaultMaterial: PropTypes.string.isRequired,
initialSettings: PropTypes.object.isRequired initialSettings: PropTypes.object.isRequired,
disabled: PropTypes.bool.isRequired
}; };
constructor(props) { constructor(props) {
super(); super();
@ -59,97 +72,87 @@ class Settings extends React.Component {
}; };
getChildContext() { getChildContext() {
return { state: this.state, onChange: this.changeSettings }; return { state: this.state, onChange: this.changeSettings, disabled: this.props.disabled };
} }
render() { render() {
const { classes, printers, quality, material } = this.props; const { classes, printers, quality, material, disabled } = this.props;
return ( return (
<Tabs> <div className={classes.container}>
<Tab label="basic"> <SelectField name="printers" floatingLabelText="Printer" fullWidth>
<div> {Object.entries(printers).map(([value, { title }]) => (
<SelectField name="printers" floatingLabelText="Printer" fullWidth> <MenuItem key={value} value={value} primaryText={title} />
{Object.entries(printers).map(([value, { title }]) => ( ))}
<MenuItem key={value} value={value} primaryText={title} /> </SelectField>
))} <SelectField name="material" floatingLabelText="Material" fullWidth>
</SelectField> {Object.entries(material).map(([value, { title }]) => (
<SelectField name="quality" floatingLabelText="Quality" fullWidth> <MenuItem key={value} value={value} primaryText={title} />
{Object.entries(quality).map(([value, { title }]) => ( ))}
<MenuItem key={value} value={value} primaryText={title} /> </SelectField>
))} <h3 className={classes.text}>Printer Setup</h3>
</SelectField> <Tabs>
<SelectField name="material" floatingLabelText="Material" fullWidth> <Tab label="basic">
{Object.entries(material).map(([value, { title }]) => ( <div>
<MenuItem key={value} value={value} primaryText={title} /> <SelectField name="quality" floatingLabelText="Quality" fullWidth>
))} {Object.entries(quality).map(([value, { title }]) => (
</SelectField> <MenuItem key={value} value={value} primaryText={title} />
</div> ))}
</Tab> </SelectField>
<Tab label="advanced"> </div>
<div> </Tab>
<SettingsGroup name="Printer dimensions"> <Tab label="advanced">
<div>
<p className={classes.text}>Printer dimensions</p>
<div className={classes.textFieldRow}> <div className={classes.textFieldRow}>
<TextField name="settings.dimensions.x" fullWidth floatingLabelText="X" type="number" /> <TextField name="settings.dimensions.x" fullWidth floatingLabelText="X" type="number" />
<TextField name="settings.dimensions.y" fullWidth floatingLabelText="Y" type="number" /> <TextField name="settings.dimensions.y" fullWidth floatingLabelText="Y" type="number" />
<TextField name="settings.dimensions.z" fullWidth floatingLabelText="Z" type="number" /> <TextField name="settings.dimensions.z" fullWidth floatingLabelText="Z" type="number" />
</div> </div>
</SettingsGroup> <p className={classes.text}>Nozzle</p>
<SettingsGroup name="Nozzle">
<TextField name="settings.nozzleDiameter" fullWidth floatingLabelText="Diameter" type="number" /> <TextField name="settings.nozzleDiameter" fullWidth floatingLabelText="Diameter" type="number" />
</SettingsGroup> <p className={classes.text}>Bed</p>
<SettingsGroup name="Bed">
<TextField name="settings.bedTemperature" fullWidth floatingLabelText="Temperature" type="number" /> <TextField name="settings.bedTemperature" fullWidth floatingLabelText="Temperature" type="number" />
<Checkbox name="settings.heatedBed" label="Heated" /> <Checkbox name="settings.heatedBed" label="Heated" />
</SettingsGroup> <p className={classes.text}>Material</p>
<SettingsGroup name="Material">
<TextField name="settings.filamentThickness" fullWidth floatingLabelText="Thickness" type="number" /> <TextField name="settings.filamentThickness" fullWidth floatingLabelText="Thickness" type="number" />
<TextField name="settings.temperature" fullWidth floatingLabelText="Temperature" type="number" /> <TextField name="settings.temperature" fullWidth floatingLabelText="Temperature" type="number" />
</SettingsGroup> <p className={classes.text}>Thickness</p>
<SettingsGroup name="Thickness">
<TextField name="settings.thickness.top" fullWidth floatingLabelText="top" type="number" /> <TextField name="settings.thickness.top" fullWidth floatingLabelText="top" type="number" />
<TextField name="settings.thickness.bottom" fullWidth floatingLabelText="bottom" type="number" /> <TextField name="settings.thickness.bottom" fullWidth floatingLabelText="bottom" type="number" />
<TextField name="settings.thickness.shell" fullWidth floatingLabelText="shell" type="number" /> <TextField name="settings.thickness.shell" fullWidth floatingLabelText="shell" type="number" />
</SettingsGroup> <p className={classes.text}>Retraction</p>
<SettingsGroup name="Retraction">
<Checkbox name="settings.retraction.enabled" label="Enabled" /> <Checkbox name="settings.retraction.enabled" label="Enabled" />
<TextField name="settings.retraction.amount" fullWidth floatingLabelText="Amount" type="number" /> <TextField name="settings.retraction.amount" fullWidth floatingLabelText="Amount" type="number" />
<TextField name="settings.retraction.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.retraction.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.retraction.minDistance" fullWidth floatingLabelText="Min distance" type="number" /> <TextField name="settings.retraction.minDistance" fullWidth floatingLabelText="Min distance" type="number" />
</SettingsGroup> <p className={classes.text}>Travel</p>
<SettingsGroup name="Travel">
<TextField name="settings.travel.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.travel.speed" fullWidth floatingLabelText="Speed" type="number" />
<Checkbox name="settings.combing" label="Combing" /> <Checkbox name="settings.combing" label="Combing" />
</SettingsGroup> <p className={classes.text}>Inner shell</p>
<SettingsGroup name="Inner shell">
<TextField name="settings.innerShell.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.innerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.innerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.innerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> <p className={classes.text}>Outer shell</p>
<SettingsGroup name="Outer shell">
<TextField name="settings.outerShell.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.outerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.outerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.outerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> <p className={classes.text}>Inner infill</p>
<SettingsGroup name="Inner infill">
<TextField name="settings.innerInfill.gridSize" fullWidth floatingLabelText="Grid size" type="number" /> <TextField name="settings.innerInfill.gridSize" fullWidth floatingLabelText="Grid size" type="number" />
<TextField name="settings.innerInfill.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.innerInfill.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.innerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.innerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> <p className={classes.text}>Outer infill</p>
<SettingsGroup name="Outer infill">
<TextField name="settings.outerInfill.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.outerInfill.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.outerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.outerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> <p className={classes.text}>Brim</p>
<SettingsGroup name="Brim">
<TextField name="settings.brim.offset" fullWidth floatingLabelText="Offset" type="number" /> <TextField name="settings.brim.offset" fullWidth floatingLabelText="Offset" type="number" />
<TextField name="settings.brim.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.brim.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.brim.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.brim.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> <p className={classes.text}>First layer</p>
<SettingsGroup name="First layer">
<TextField name="settings.firstLayer.speed" fullWidth floatingLabelText="Speed" type="number" /> <TextField name="settings.firstLayer.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.firstLayer.flowRate" fullWidth floatingLabelText="Flow rate" type="number" /> <TextField name="settings.firstLayer.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
</SettingsGroup> </div>
</div> </Tab>
</Tab> </Tabs>
</Tabs> </div>
); );
} }
} }

View File

@ -2,12 +2,12 @@ import _ from 'lodash';
import React from 'react'; import React from 'react';
import * as THREE from 'three'; import * as THREE from 'three';
import PropTypes from 'proptypes'; import PropTypes from 'proptypes';
import { placeOnGround, createScene, createGcodeGeometry } from './utils.js'; import { placeOnGround, createScene, fetchProgress, slice } from './utils.js';
import injectSheet from 'react-jss'; import injectSheet from 'react-jss';
import { sliceGeometry } from '../slicer.js';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import Slider from 'material-ui/Slider'; import Slider from 'material-ui/Slider';
import { grey100, grey300 } from 'material-ui/styles/colors'; import LinearProgress from 'material-ui/LinearProgress';
import { grey100, grey300, red500 } from 'material-ui/styles/colors';
import Settings from './Settings.js'; import Settings from './Settings.js';
import baseSettings from '../settings/default.yml'; import baseSettings from '../settings/default.yml';
import printerSettings from '../settings/printer.yml'; import printerSettings from '../settings/printer.yml';
@ -15,13 +15,16 @@ import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml'; import qualitySettings from '../settings/quality.yml';
import ReactResizeDetector from 'react-resize-detector'; import ReactResizeDetector from 'react-resize-detector';
const MAX_FULLSCREEN_WIDTH = 720;
const styles = { const styles = {
container: { container: {
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
height: '100%', height: '100%',
backgroundColor: grey100, backgroundColor: grey100,
overflow: 'hidden' overflow: 'hidden',
fontFamily: 'roboto, sans-serif'
}, },
controlBar: { controlBar: {
position: 'absolute', position: 'absolute',
@ -29,38 +32,40 @@ const styles = {
left: '10px' left: '10px'
}, },
d3View: { d3View: {
flexGrow: 1 flexGrow: 1,
flexBasis: 0
}, },
canvas: { canvas: {
position: 'absolute' position: 'absolute'
}, },
sliceBar: { sliceBar: {
width: '240px', display: 'flex',
flexDirection: 'column',
maxWidth: '380px',
boxSizing: 'border-box',
padding: '10px', padding: '10px',
overflowY: 'auto',
backgroundColor: 'white', backgroundColor: 'white',
borderLeft: `1px solid ${grey300}` borderLeft: `1px solid ${grey300}`
}, },
overlay: {
position: 'absolute',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
top: 0,
right: 0,
bottom: 0,
left: 0,
padding: '20px',
fontFamily: 'monospace'
},
sliceActions: { sliceActions: {
listStyleType: 'none', flexShrink: 0,
paddingLeft: 0 },
sliceButtons: {
justifyContent: 'flex-end',
display: 'flex'
}, },
button: { button: {
margin: '5px 0' margin: '5px 0 5px 5px'
}, },
controlButton: { controlButton: {
marginRight: '2px' marginRight: '2px'
},
buttonContainer: {
width: '100%',
padding: '10px'
},
error: {
color: red500
} }
}; };
@ -99,8 +104,12 @@ class Interface extends React.Component {
const { defaultPrinter, defaultQuality, defaultMaterial, printers, quality, material, defaultSettings } = props; const { defaultPrinter, defaultQuality, defaultMaterial, printers, quality, material, defaultSettings } = props;
this.state = { this.state = {
controlMode: 'translate', controlMode: 'translate',
showFullScreen: {
active: false,
settings: true
},
isSlicing: false, isSlicing: false,
sliced: false, error: null,
printers: defaultPrinter, printers: defaultPrinter,
quality: defaultQuality, quality: defaultQuality,
material: defaultMaterial, material: defaultMaterial,
@ -120,6 +129,10 @@ class Interface extends React.Component {
this.setState({ ...scene }); this.setState({ ...scene });
} }
componentWillUnmount() {
if (this.state.editorControls) this.state.editorControls.dispose();
}
resetMesh = () => { resetMesh = () => {
const { mesh, render } = this.state; const { mesh, render } = this.state;
if (mesh) { if (mesh) {
@ -132,84 +145,68 @@ class Interface extends React.Component {
} }
}; };
reset = () => { scaleUp = () => this.scaleMesh(0.9);
const { control, mesh, render, gcode, scene } = this.state; scaleDown = () => this.scaleMesh(1.0 / 0.9);
control.enabled = true; scaleMesh = (factor) => {
control.setSize(1); const { mesh, render } = this.state;
control.visible = true; if (mesh) {
mesh.visible = true; mesh.scale.multiplyScalar(factor);
mesh.updateMatrix();
placeOnGround(mesh);
render();
}
};
scene.remove(gcode.linePreview); rotateX = () => this.rotate(new THREE.Vector3(0, 0, 1));
gcode.linePreview.geometry.dispose(); rotateY = () => this.rotate(new THREE.Vector3(1, 0, 0));
rotateZ = () => this.rotate(new THREE.Vector3(0, 1, 0));
this.setState({ sliced: false, gcode: null }); rotate = (axis, angle = Math.PI / 2.0) => {
render(); const { mesh, render } = this.state;
if (mesh) {
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(axis, angle);
mesh.quaternion.premultiply(quaternion);
mesh.updateMatrix();
placeOnGround(mesh);
render();
}
}; };
slice = async () => { slice = async () => {
const { mesh, render, scene, control, settings } = this.state; const { mesh, settings, isSlicing, printers, quality, material } = this.state;
const { dimensions } = settings; if (isSlicing) return;
const centerX = dimensions.x / 2;
const centerY = dimensions.y / 2;
const geometry = mesh.geometry.clone(); this.setState({ isSlicing: true, progress: { action: '', slicing: 0, uploading: 0 }, error: null });
mesh.updateMatrix();
this.setState({ isSlicing: true, progress: { actions: [], percentage: 0 } }); try {
await slice(mesh, settings, printers, quality, material, progress => {
const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX).multiply(mesh.matrix); this.setState({ progress: { ...this.state.progress, ...progress } });
const gcode = await sliceGeometry(settings, geometry, matrix, false, true, ({ progress }) => { });
this.setState({ progress: { } catch (error) {
actions: [...this.state.progress.actions, progress.action], this.setState({ error: error.message });
percentage: progress.done / progress.total }
} });
});
this.setState({ isSlicing: false }); this.setState({ isSlicing: false });
// TODO
// can't disable control ui still interacts with mouse input
control.enabled = false;
// hack to disable control
control.setSize(0);
control.visible = false;
mesh.visible = false;
gcode.linePreview.position.x = -centerY;
gcode.linePreview.position.z = -centerX;
scene.add(gcode.linePreview);
this.setState({ sliced: true, gcode });
render();
}; };
onChangeSettings = (settings) => { onChangeSettings = (settings) => {
this.setState(settings); this.setState(settings);
}; };
updateDrawRange = (event, value) => {
const { gcode, render } = this.state;
gcode.linePreview.geometry.setDrawRange(0, value);
render();
};
componentWillUnmount() {
if (this.state.editorControls) this.state.editorControls.dispose();
if (this.state.control) this.state.control.dispose();
}
componentWillUpdate(nextProps, nextState) { componentWillUpdate(nextProps, nextState) {
const { control, box, render, setSize } = this.state; const { box, render, setSize } = this.state;
if (control && nextState.controlMode !== this.state.controlMode) control.setMode(nextState.controlMode); let changed = false;
if (box && nextState.settings.dimensions !== this.state.settings.dimensions) { if (box && nextState.settings.dimensions !== this.state.settings.dimensions) {
const { dimensions } = nextState.settings; const { dimensions } = nextState.settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x); box.scale.set(dimensions.y, dimensions.z, dimensions.x);
render(); box.updateMatrix();
changed = true;
} }
if (changed) render();
} }
onResize = (width, height) => { onResize3dView = (width, height) => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const { setSize } = this.state; const { setSize } = this.state;
const { pixelRatio } = this.props; const { pixelRatio } = this.props;
@ -217,35 +214,61 @@ class Interface extends React.Component {
}); });
}; };
onResizeContainer = (width) => {
this.setState({
showFullScreen: {
active: width > MAX_FULLSCREEN_WIDTH,
settings: this.state.showFullScreen.settings
}
});
};
render() { render() {
const { classes, onCompleteActions, defaultPrinter, defaultQuality, defaultMaterial } = this.props; const { classes, onCompleteActions, defaultPrinter, defaultQuality, defaultMaterial } = this.props;
const { sliced, isSlicing, progress, gcode, controlMode, settings, printers, quality, material } = this.state; const { isSlicing, progress, gcode, settings, printers, quality, material, showFullScreen, error } = this.state;
const showSettings = showFullScreen.active || showFullScreen.settings;
const showPreview = showFullScreen.active || !showFullScreen.settings;
const percentage = progress ? (progress.uploading + progress.slicing) / 2.0 * 100.0 : 0.0;
const toggleFullScreen = () => {
this.setState({
showFullScreen: {
...this.state.showFullScreen,
settings: !this.state.showFullScreen.settings
}
});
};
return ( return (
<div className={classes.container}> <div className={classes.container}>
<div className={classes.d3View}> <ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize} /> {<div style={{ display: showPreview ? 'inherit' : 'none' }} className={classes.d3View}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
<canvas className={classes.canvas} ref="canvas" /> <canvas className={classes.canvas} ref="canvas" />
{!sliced && <div className={classes.controlBar}> {!showFullScreen.active && <div className={classes.buttonContainer}>
<RaisedButton className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" /> <RaisedButton fullWidth label="Edit settings" onTouchTap={toggleFullScreen}/>
<RaisedButton className={classes.controlButton} disabled={controlMode === 'translate'} onTouchTap={() => this.setState({ controlMode: 'translate' })} label="translate" /> </div>}
<RaisedButton className={classes.controlButton} disabled={controlMode === 'rotate'} onTouchTap={() => this.setState({ controlMode: 'rotate' })} label="rotate" /> {!isSlicing && <div className={classes.controlBar}>
<RaisedButton className={classes.controlButton} disabled={controlMode === 'scale'} onTouchTap={() => this.setState({ controlMode: 'scale' })} label="scale" /> <RaisedButton className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" />
<RaisedButton className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" />
<RaisedButton className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateX} label="rotate x" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateY} label="rotate y" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateZ} label="rotate z" />
</div>} </div>}
</div>
{sliced && <div className={classes.controlBar}>
<Slider
axis="y"
style={{ height: '300px' }}
step={2}
min={1}
max={gcode.linePreview.geometry.getAttribute('position').count}
defaultValue={gcode.linePreview.geometry.getAttribute('position').count}
onChange={this.updateDrawRange}
/>
</div>} </div>}
{!sliced && <div className={classes.sliceBar}> <div
className={classes.sliceBar}
style={{
display: showSettings ? 'inherit' : 'none',
...showPreview ? {} : { maxWidth: 'inherit', width: '100%' }
}}
>
{!showFullScreen.active && <RaisedButton label="Edit model" onTouchTap={toggleFullScreen}/>}
<Settings <Settings
disabled={isSlicing}
printers={printerSettings} printers={printerSettings}
defaultPrinter={defaultPrinter} defaultPrinter={defaultPrinter}
quality={qualitySettings} quality={qualitySettings}
@ -255,20 +278,21 @@ class Interface extends React.Component {
initialSettings={settings} initialSettings={settings}
onChange={this.onChangeSettings} onChange={this.onChangeSettings}
/> />
<RaisedButton className={classes.button} fullWidth disabled={isSlicing} onTouchTap={this.slice} primary label="slice" /> <div className={classes.sliceActions}>
</div>} {error && <p className={classes.error}>{error}</p>}
{sliced && <div className={classes.sliceBar}> {isSlicing && <p>{progress.action}</p>}
<RaisedButton className={classes.button} fullWidth onTouchTap={this.reset} primary label="slice again" /> {isSlicing && <LinearProgress mode="determinate" value={percentage} />}
{onCompleteActions.map(({ title, callback }, i) => ( <div className={classes.sliceButtons}>
<RaisedButton className={classes.button} key={i} fullWidth onTouchTap={() => callback({ gcode, settings, printers, quality, material })} primary label={title} /> <RaisedButton
))} label="Print"
</div>} primary
{isSlicing && <div className={classes.overlay}> className={`${classes.button}`}
<p>Slicing: {progress.percentage.toLocaleString(navigator.language, { style: 'percent' })}</p> onTouchTap={this.slice}
<ul className={classes.sliceActions}> disabled={isSlicing}
{progress.actions.map((action, i) => <li key={i}>{action}</li>)} />
</ul> </div>
</div>} </div>
</div>
</div> </div>
); );
} }

View File

@ -1,6 +1,9 @@
import * as THREE from 'three'; import * as THREE from 'three';
import 'three/examples/js/controls/EditorControls'; import 'three/examples/js/controls/EditorControls';
import 'three/examples/js/controls/TransformControls'; import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml';
import { sliceGeometry } from '../slicer.js';
export function placeOnGround(mesh) { export function placeOnGround(mesh) {
const boundingBox = new THREE.Box3().setFromObject(mesh); const boundingBox = new THREE.Box3().setFromObject(mesh);
@ -18,22 +21,11 @@ export function createScene(canvas, props, state) {
const center = geometry.boundingBox.getCenter(); const center = geometry.boundingBox.getCenter();
geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z)); geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0xffffff, 0);
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 1, 10000); const camera = new THREE.PerspectiveCamera(50, 1, 1, 10000);
camera.position.set(0, 400, 300); camera.position.set(0, 400, 300);
const setSize = (width, height, pixelRatio = 1) => {
renderer.setSize(width, height);
renderer.setPixelRatio(pixelRatio);
camera.aspect = width / height;
camera.updateProjectionMatrix();
render();
};
const directionalLight = new THREE.DirectionalLight(0xd5d5d5); const directionalLight = new THREE.DirectionalLight(0xd5d5d5);
directionalLight.position.set(1, 1, 1); directionalLight.position.set(1, 1, 1);
scene.add(directionalLight); scene.add(directionalLight);
@ -45,36 +37,109 @@ export function createScene(canvas, props, state) {
placeOnGround(mesh); placeOnGround(mesh);
scene.add(mesh); scene.add(mesh);
const editorControls = new THREE.EditorControls(camera, canvas); const box = new THREE.BoxHelper(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1).applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5, 0))), 0x72bcd4);
editorControls.focus(mesh);
const control = new THREE.TransformControls(camera, canvas);
control.setMode(controlMode);
control.setRotationSnap(THREE.Math.degToRad(45));
control.addEventListener('mouseDown', () => editorControls.enabled = false);
control.addEventListener('mouseUp', () => {
editorControls.enabled = true;
placeOnGround(mesh);
});
control.attach(mesh);
scene.add(control);
const render = () => {
control.update();
renderer.render(scene, camera);
};
control.addEventListener('change', render);
editorControls.addEventListener('change', render);
const box = new THREE.BoxHelper();
box.update(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1).applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5, 0))));
box.material.color.setHex(0x72bcd4);
scene.add(box); scene.add(box);
const { dimensions } = settings; const { dimensions } = settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x); box.scale.set(dimensions.y, dimensions.z, dimensions.x);
box.updateMatrix();
return { control, editorControls, scene, mesh, camera, renderer, render, box, setSize }; const editorControls = new THREE.EditorControls(camera, canvas);
editorControls.focus(mesh);
const render = () => renderer.render(scene, camera);
editorControls.addEventListener('change', render);
const setSize = (width, height, pixelRatio = 1) => {
renderer.setSize(width, height);
renderer.setPixelRatio(pixelRatio);
camera.aspect = width / height;
camera.updateProjectionMatrix();
render();
};
let renderer;
const updateCanvas = (canvas) => {
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0xffffff, 0);
render();
};
updateCanvas(canvas);
return { editorControls, scene, mesh, camera, renderer, render, box, setSize, updateCanvas };
}
export function fetchProgress(url, { method = 'get', headers = {}, body = {} } = {}, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
if (headers) {
for (const key in headers) {
const header = headers[key];
xhr.setRequestHeader(key, header);
}
}
xhr.onload = event => resolve(event.target.responseText);
xhr.onerror = reject;
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress;
xhr.send(body);
});
}
const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com';
const CONNECT_URL = 'http://connect.doodle3d.com/';
export async function slice(mesh, settings, printers, quality, material, updateProgress) {
const { dimensions } = settings;
const centerX = dimensions.x / 2;
const centerY = dimensions.y / 2;
const geometry = mesh.geometry.clone();
mesh.updateMatrix();
const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX).multiply(mesh.matrix);
const gcode = await sliceGeometry(settings, geometry, matrix, false, false, ({ progress }) => {
updateProgress({
action: progress.action,
slicing: progress.done / progress.total
});
});
// upload G-code file to AWS S3
const { data: { reservation, id } } = await fetch(`${GCODE_SERVER_URL}/upload`, { method: 'POST' })
.then(response => response.json());
const body = new FormData();
const { fields } = reservation;
for (const key in fields) {
body.append(key, fields[key]);
}
const file = ';' + JSON.stringify({
name: `${name}.gcode`,
...settings,
printer: {
type: printers,
title: printerSettings[printers].title
},
material: {
type: material,
title: materialSettings[material].title
},
quality: {
type: quality,
title: qualitySettings[quality].title
}
}).trim() + '\n' + gcode;
body.append('file', file);
await fetchProgress(reservation.url, { method: 'POST', body }, (progess) => {
updateProgress({
action: 'Uploading',
uploading: progess.loaded / progess.total
});
});
const popup = window.open(`${CONNECT_URL}?uuid=${id}`, '_blank');
if (!popup) throw new Error('popup was blocked by browser');
} }

View File

@ -99,31 +99,25 @@ function gcodeToString(gcode) {
} }
const MAX_SPEED = 100 * 60; const MAX_SPEED = 100 * 60;
const COLOR = new THREE.Color();
function createGcodeGeometry(gcode) { function createGcodeGeometry(gcode) {
const positions = []; const positions = [];
const colors = []; const colors = [];
let lastPoint let lastPoint = [0, 0, 0];
for (let i = 0; i < gcode.length; i ++) { for (let i = 0; i < gcode.length; i ++) {
const { G, F, X, Y, Z } = gcode[i]; const { G, F, X, Y, Z } = gcode[i];
if (X || Y || Z) { if (X || Y || Z) {
let color;
if (G === 0) {
color = new THREE.Color(0x00ff00);
} else if (G === 1) {
color = new THREE.Color().setHSL(F / MAX_SPEED, 0.5, 0.5);
}
if (G === 1) { if (G === 1) {
if (lastPoint) positions.push(lastPoint[0], lastPoint[1], lastPoint[2]); positions.push(lastPoint.Y, lastPoint.Z, lastPoint.X);
positions.push(Y, Z, X); positions.push(Y, Z, X);
const color = (G === 0) ? COLOR.setHex(0x00ff00) : COLOR.setHSL(F / MAX_SPEED, 0.5, 0.5);
colors.push(color.r, color.g, color.b); colors.push(color.r, color.g, color.b);
colors.push(color.r, color.g, color.b); colors.push(color.r, color.g, color.b);
} }
lastPoint = { X, Y, Z };
lastPoint = [Y, Z, X];
} }
} }