diff --git a/src/interface/FormComponents.js b/src/interface/FormComponents.js index 8bf8da6..aff0278 100644 --- a/src/interface/FormComponents.js +++ b/src/interface/FormComponents.js @@ -5,57 +5,35 @@ import injectSheet from 'react-jss'; import MaterialUISelectField from 'material-ui/SelectField' import MaterialUICheckbox from 'material-ui/Checkbox'; import MaterialUITextField from 'material-ui/TextField'; -import { grey100, grey300, grey500 } from 'material-ui/styles/colors'; -const styles = { - 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 }) => ( -
- {name} - {children} -
-)); -SettingsGroup.propTypes = { - classes: PropTypes.objectOf(PropTypes.string), - name: PropTypes.string.isRequired, - children: PropTypes.node -}; +const contextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool }; export const SelectField = (props, context) => ( context.onChange(props.name, value)} /> ); -SelectField.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; +SelectField.contextTypes = contextTypes; export const TextField = (props, context) => ( context.onChange(props.name, value)} /> ); -TextField.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; +TextField.contextTypes = contextTypes; export const Checkbox = (props, context) => ( context.onChange(props.name, value)} /> ); -Checkbox.contextTypes = { state: PropTypes.object, onChange: PropTypes.func }; +Checkbox.contextTypes = contextTypes; diff --git a/src/interface/Settings.js b/src/interface/Settings.js index ca3a91a..5a67ca6 100644 --- a/src/interface/Settings.js +++ b/src/interface/Settings.js @@ -4,17 +4,29 @@ import _ from 'lodash'; import { Tabs, Tab } from 'material-ui/Tabs'; import MenuItem from 'material-ui/MenuItem'; import injectSheet from 'react-jss'; -import { SettingsGroup, SelectField, TextField, Checkbox } from './FormComponents.js'; -import { grey500 } from 'material-ui/styles/colors'; +import { SelectField, TextField, Checkbox } from './FormComponents.js'; +import { grey900 } from 'material-ui/styles/colors'; const styles = { textFieldRow: { display: 'flex' + }, + text: { + fontWeight: 'bold', + color: grey900 + }, + container: { + width: '100%', + flexGrow: 1, + overflowY: 'auto' } }; 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 = { classes: PropTypes.objectOf(PropTypes.string), onChange: PropTypes.func, @@ -24,7 +36,8 @@ class Settings extends React.Component { defaultQuality: PropTypes.string.isRequired, material: PropTypes.object.isRequired, defaultMaterial: PropTypes.string.isRequired, - initialSettings: PropTypes.object.isRequired + initialSettings: PropTypes.object.isRequired, + disabled: PropTypes.bool.isRequired }; constructor(props) { super(); @@ -59,97 +72,87 @@ class Settings extends React.Component { }; getChildContext() { - return { state: this.state, onChange: this.changeSettings }; + return { state: this.state, onChange: this.changeSettings, disabled: this.props.disabled }; } render() { - const { classes, printers, quality, material } = this.props; + const { classes, printers, quality, material, disabled } = this.props; return ( - - -
- - {Object.entries(printers).map(([value, { title }]) => ( - - ))} - - - {Object.entries(quality).map(([value, { title }]) => ( - - ))} - - - {Object.entries(material).map(([value, { title }]) => ( - - ))} - -
-
- -
- +
+ + {Object.entries(printers).map(([value, { title }]) => ( + + ))} + + + {Object.entries(material).map(([value, { title }]) => ( + + ))} + +

Printer Setup

+ + +
+ + {Object.entries(quality).map(([value, { title }]) => ( + + ))} + +
+
+ +
+

Printer dimensions

- - +

Nozzle

-
- +

Bed

-
- +

Material

-
- +

Thickness

-
- +

Retraction

-
- +

Travel

-
- +

Inner shell

-
- +

Outer shell

-
- +

Inner infill

-
- +

Outer infill

-
- +

Brim

-
- +

First layer

-
-
-
-
+
+ + +
); } } diff --git a/src/interface/index.js b/src/interface/index.js index 9d8bd85..be82411 100644 --- a/src/interface/index.js +++ b/src/interface/index.js @@ -2,12 +2,12 @@ import _ from 'lodash'; import React from 'react'; import * as THREE from 'three'; 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 { sliceGeometry } from '../slicer.js'; import RaisedButton from 'material-ui/RaisedButton'; 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 baseSettings from '../settings/default.yml'; import printerSettings from '../settings/printer.yml'; @@ -15,13 +15,16 @@ import materialSettings from '../settings/material.yml'; import qualitySettings from '../settings/quality.yml'; import ReactResizeDetector from 'react-resize-detector'; +const MAX_FULLSCREEN_WIDTH = 720; + const styles = { container: { position: 'relative', display: 'flex', height: '100%', backgroundColor: grey100, - overflow: 'hidden' + overflow: 'hidden', + fontFamily: 'roboto, sans-serif' }, controlBar: { position: 'absolute', @@ -29,38 +32,40 @@ const styles = { left: '10px' }, d3View: { - flexGrow: 1 + flexGrow: 1, + flexBasis: 0 }, canvas: { position: 'absolute' }, sliceBar: { - width: '240px', + display: 'flex', + flexDirection: 'column', + maxWidth: '380px', + boxSizing: 'border-box', padding: '10px', - overflowY: 'auto', backgroundColor: 'white', 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: { - listStyleType: 'none', - paddingLeft: 0 + flexShrink: 0, + }, + sliceButtons: { + justifyContent: 'flex-end', + display: 'flex' }, button: { - margin: '5px 0' + margin: '5px 0 5px 5px' }, controlButton: { 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; this.state = { controlMode: 'translate', + showFullScreen: { + active: false, + settings: true + }, isSlicing: false, - sliced: false, + error: null, printers: defaultPrinter, quality: defaultQuality, material: defaultMaterial, @@ -120,6 +129,10 @@ class Interface extends React.Component { this.setState({ ...scene }); } + componentWillUnmount() { + if (this.state.editorControls) this.state.editorControls.dispose(); + } + resetMesh = () => { const { mesh, render } = this.state; if (mesh) { @@ -132,84 +145,68 @@ class Interface extends React.Component { } }; - reset = () => { - const { control, mesh, render, gcode, scene } = this.state; - control.enabled = true; - control.setSize(1); - control.visible = true; - mesh.visible = true; + scaleUp = () => this.scaleMesh(0.9); + scaleDown = () => this.scaleMesh(1.0 / 0.9); + scaleMesh = (factor) => { + const { mesh, render } = this.state; + if (mesh) { + mesh.scale.multiplyScalar(factor); + mesh.updateMatrix(); + placeOnGround(mesh); + render(); + } + }; - scene.remove(gcode.linePreview); - gcode.linePreview.geometry.dispose(); - - this.setState({ sliced: false, gcode: null }); - render(); + rotateX = () => this.rotate(new THREE.Vector3(0, 0, 1)); + rotateY = () => this.rotate(new THREE.Vector3(1, 0, 0)); + rotateZ = () => this.rotate(new THREE.Vector3(0, 1, 0)); + rotate = (axis, angle = Math.PI / 2.0) => { + 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 () => { - const { mesh, render, scene, control, settings } = this.state; + const { mesh, settings, isSlicing, printers, quality, material } = this.state; - const { dimensions } = settings; - const centerX = dimensions.x / 2; - const centerY = dimensions.y / 2; + if (isSlicing) return; - const geometry = mesh.geometry.clone(); - mesh.updateMatrix(); + this.setState({ isSlicing: true, progress: { action: '', slicing: 0, uploading: 0 }, error: null }); - this.setState({ isSlicing: true, progress: { actions: [], percentage: 0 } }); - - const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX).multiply(mesh.matrix); - const gcode = await sliceGeometry(settings, geometry, matrix, false, true, ({ progress }) => { - this.setState({ progress: { - actions: [...this.state.progress.actions, progress.action], - percentage: progress.done / progress.total - } }); - }); + try { + await slice(mesh, settings, printers, quality, material, progress => { + this.setState({ progress: { ...this.state.progress, ...progress } }); + }); + } catch (error) { + this.setState({ error: error.message }); + } 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) => { 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) { - const { control, box, render, setSize } = this.state; - if (control && nextState.controlMode !== this.state.controlMode) control.setMode(nextState.controlMode); + const { box, render, setSize } = this.state; + let changed = false; if (box && nextState.settings.dimensions !== this.state.settings.dimensions) { const { dimensions } = nextState.settings; 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(() => { const { setSize } = this.state; 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() { 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 (
-
- + + {
+ - {!sliced &&
- - this.setState({ controlMode: 'translate' })} label="translate" /> - this.setState({ controlMode: 'rotate' })} label="rotate" /> - this.setState({ controlMode: 'scale' })} label="scale" /> + {!showFullScreen.active &&
+ +
} + {!isSlicing &&
+ + + + + +
} -
- {sliced &&
-
} - {!sliced &&
+
+ {!showFullScreen.active && } - -
} - {sliced &&
- - {onCompleteActions.map(({ title, callback }, i) => ( - callback({ gcode, settings, printers, quality, material })} primary label={title} /> - ))} -
} - {isSlicing &&
-

Slicing: {progress.percentage.toLocaleString(navigator.language, { style: 'percent' })}

-
    - {progress.actions.map((action, i) =>
  • {action}
  • )} -
-
} +
+ {error &&

{error}

} + {isSlicing &&

{progress.action}

} + {isSlicing && } +
+ +
+
+
); } diff --git a/src/interface/utils.js b/src/interface/utils.js index 4e0bfd5..bc91f35 100644 --- a/src/interface/utils.js +++ b/src/interface/utils.js @@ -1,6 +1,9 @@ import * as THREE from 'three'; 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) { const boundingBox = new THREE.Box3().setFromObject(mesh); @@ -18,22 +21,11 @@ export function createScene(canvas, props, state) { const center = geometry.boundingBox.getCenter(); 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 camera = new THREE.PerspectiveCamera(50, 1, 1, 10000); 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); directionalLight.position.set(1, 1, 1); scene.add(directionalLight); @@ -45,36 +37,109 @@ export function createScene(canvas, props, state) { placeOnGround(mesh); scene.add(mesh); - const editorControls = new THREE.EditorControls(camera, canvas); - 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); + const box = new THREE.BoxHelper(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1).applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5, 0))), 0x72bcd4); scene.add(box); const { dimensions } = settings; 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'); } diff --git a/src/sliceActions/slice.js b/src/sliceActions/slice.js index 42b2226..a1cbaad 100644 --- a/src/sliceActions/slice.js +++ b/src/sliceActions/slice.js @@ -99,31 +99,25 @@ function gcodeToString(gcode) { } const MAX_SPEED = 100 * 60; +const COLOR = new THREE.Color(); function createGcodeGeometry(gcode) { const positions = []; const colors = []; - let lastPoint + let lastPoint = [0, 0, 0]; for (let i = 0; i < gcode.length; i ++) { const { G, F, X, Y, Z } = gcode[i]; 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 (lastPoint) positions.push(lastPoint[0], lastPoint[1], lastPoint[2]); + positions.push(lastPoint.Y, lastPoint.Z, lastPoint.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); } - - lastPoint = [Y, Z, X]; + lastPoint = { X, Y, Z }; } }