This commit is contained in:
Rick Companje 2018-11-27 14:13:25 +01:00
commit 7da0b2fa17
33 changed files with 4273 additions and 650 deletions

33
.eslintrc Normal file
View File

@ -0,0 +1,33 @@
{
"extends": "eslint-config-airbnb",
"parser": "babel-eslint",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"modules": true,
"jsx": true
},
"rules": {
"comma-dangle": [1, "never"],
"no-else-return": 0,
"no-use-before-define": [2, "nofunc"],
"no-param-reassign": 0,
"no-var": 1,
"no-labels": 0,
"guard-for-in": 0,
"prefer-const": 0,
"no-unused-vars": 1,
"key-spacing": [1, {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
"no-loop-func": 1,
"react/sort-comp": [0],
"max-len": [1, 110, 4],
"camelcase": 1,
"new-cap": 0
},
"env": {
"browser": true,
"es6": true
},
"globals": {
"THREE": false
}
}

97
comb.js Normal file
View File

@ -0,0 +1,97 @@
import comb from './src/sliceActions/helpers/comb.js';
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = 800;
canvas.height = 800;
const context = canvas.getContext('2d');
context.lineJoin = 'bevel';
function circle(radius = 10, x = 0, y = 0, clockWise = true, segments = 40) {
const shape = [];
for (let rad = 0; rad < Math.PI * 2; rad += Math.PI * 2 / segments) {
if (clockWise) {
shape.push({ x: Math.cos(rad) * radius + x, y: Math.sin(rad) * radius + y });
} else {
shape.push({ x: Math.cos(rad) * radius + x, y: -Math.sin(rad) * radius + y });
}
}
return shape;
}
const START = { x: 200, y: 400 };
const END = { x: 400, y: 300 };
const POLYGON = [[
{ x: 10, y: 10 },
{ x: 600, y: 10 },
{ x: 500, y: 200 },
{ x: 600, y: 600 },
{ x: 10, y: 600 }
], [
{ x: 160, y: 120 },
{ x: 120, y: 400 },
{ x: 400, y: 400 }
]];
// const POLYGON = [
// circle(300, 305, 305, true, 4),
// circle(40, 305, 105, false, 4),
// circle(40, 305, 205, false, 4),
// circle(40, 305, 305, false, 4),
// circle(40, 305, 405, false, 4),
// circle(40, 305, 505, false, 4)
// ];
canvas.onmousedown = (event) => {
START.x = event.offsetX;
START.y = event.offsetY;
compute();
};
canvas.onmousemove = (event) => {
END.x = event.offsetX;
END.y = event.offsetY;
compute();
};
compute();
function compute() {
const path = comb(POLYGON, START, END);
// draw
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
for (const shape of POLYGON) {
let first = true;
for (const { x, y } of shape) {
if (first) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
first = false;
}
}
context.closePath();
context.fillStyle = 'lightgray';
context.fill();
context.beginPath();
for (const { x, y } of path) {
context.lineTo(x, y);
}
context.lineWidth = 2;
context.stroke();
context.beginPath();
context.arc(START.x, START.y, 3, 0, Math.PI * 2);
context.fillStyle = 'blue';
context.fill();
context.beginPath();
context.arc(END.x, END.y, 3, 0, Math.PI * 2);
context.fillStyle = 'red';
context.fill();
}

View File

@ -1,9 +1,8 @@
import 'babel-polyfill' import 'babel-polyfill';
import React from 'react'; import React from 'react';
import { Interface } from 'doodle3d-slicer'; import { Interface } from 'doodle3d-slicer';
import { render } from 'react-dom'; import { render } from 'react-dom';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import injectTapEventPlugin from 'react-tap-event-plugin';
import jss from 'jss'; import jss from 'jss';
import preset from 'jss-preset-default'; import preset from 'jss-preset-default';
import normalize from 'normalize-jss'; import normalize from 'normalize-jss';
@ -19,8 +18,6 @@ const muiTheme = getMuiTheme({
} }
}); });
injectTapEventPlugin();
jss.setup(preset()); jss.setup(preset());
jss.createStyleSheet(normalize).attach(); jss.createStyleSheet(normalize).attach();
jss.createStyleSheet({ jss.createStyleSheet({
@ -32,11 +29,11 @@ jss.createStyleSheet({
} }
}).attach(); }).attach();
let { file, selectedPrinter, actions } = queryString.parse(location.search); let { file, selectedPrinter, actions, name } = queryString.parse(location.search);
if (actions) actions = JSON.parse(actions); if (actions) actions = JSON.parse(actions);
render(( render((
<MuiThemeProvider muiTheme={muiTheme}> <MuiThemeProvider muiTheme={muiTheme}>
<Interface actions={actions} fileUrl={file} selectedPrinter={selectedPrinter} name="doodle"/> <Interface actions={actions} fileUrl={file} selectedPrinter={selectedPrinter} name={name}/>
</MuiThemeProvider> </MuiThemeProvider>
), document.getElementById('app')); ), document.getElementById('app'));

4018
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"scripts": { "scripts": {
"start": "webpack-dev-server -w", "start": "webpack-dev-server -w",
"dist": "NODE_ENV=production webpack -p", "dist": "NODE_ENV=production webpack -p",
"lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print", "upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print",
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p", "analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p",
@ -20,7 +21,7 @@
"dependencies": { "dependencies": {
"@doodle3d/clipper-js": "^1.0.10", "@doodle3d/clipper-js": "^1.0.10",
"@doodle3d/doodle3d-api": "^1.0.5", "@doodle3d/doodle3d-api": "^1.0.5",
"@doodle3d/doodle3d-core": "github:doodle3d/doodle3d-core#0.18.0", "@doodle3d/doodle3d-core": "github:doodle3d/doodle3d-core",
"babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-class-properties": "^6.24.1",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"lodash": "^4.17.4", "lodash": "^4.17.4",
@ -41,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "6.24.1", "babel-cli": "6.24.1",
"babel-eslint": "^5.0.4",
"babel-loader": "7.0.0", "babel-loader": "7.0.0",
"babel-plugin-transform-es2015-classes": "^6.24.1", "babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
@ -51,14 +53,18 @@
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1", "babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"eslint": "^1.10.3",
"eslint-config-airbnb": "^3.1.0",
"eslint-plugin-react": "^3.16.1",
"file-loader": "^1.1.11",
"google-fonts-webpack-plugin": "^0.4.4",
"html-webpack-plugin": "^2.29.0", "html-webpack-plugin": "^2.29.0",
"html-webpack-template": "^6.0.2", "html-webpack-template": "^6.0.2",
"image-webpack-loader": "^4.2.0",
"imports-loader": "^0.7.1", "imports-loader": "^0.7.1",
"material-ui": "^0.19.4", "material-ui": "^0.19.4",
"normalize-jss": "^4.0.0", "normalize-jss": "^4.0.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"react-tap-event-plugin": "^3.0.2",
"url-loader": "^0.5.9",
"webpack": "^3.3.0", "webpack": "^3.3.0",
"webpack-dev-server": "^2.5.1", "webpack-dev-server": "^2.5.1",
"worker-loader": "^0.8.1", "worker-loader": "^0.8.1",

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'proptypes'; import PropTypes from 'proptypes';
import _ from 'lodash'; import _ from 'lodash';
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 TextFieldIcon from 'material-ui-textfield-icon'; import TextFieldIcon from 'material-ui-textfield-icon';
import RefreshIcon from 'material-ui-icons/Refresh'; import RefreshIcon from 'material-ui-icons/Refresh';
@ -39,7 +38,7 @@ const _TextField = ({ name, muiTheme: { palette }, ...props }, context) => (
{...props} {...props}
icon={context.advancedFields.includes(name) && <RefreshIcon icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }} style={{ fill: palette.textColor }}
onTouchTap={() => context.onChange(name, null)} onClick={() => context.onChange(name, null)}
/>} />}
floatingLabelStyle={{ floatingLabelStyle={{
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
@ -59,8 +58,8 @@ const _NumberField = ({ name, min, max, muiTheme: { palette }, ...props }, conte
type="number" type="number"
icon={context.advancedFields.includes(name) && <RefreshIcon icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }} style={{ fill: palette.textColor }}
onTouchTap={() => context.onChange(name, null)} /> onClick={() => context.onChange(name, null)}
} />}
floatingLabelStyle={{ floatingLabelStyle={{
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}} }}
@ -96,7 +95,7 @@ const _Checkbox = ({ name, muiTheme: { palette }, ...props }, context) => (
onCheck={(event, value) => context.onChange(name, value)} onCheck={(event, value) => context.onChange(name, value)}
/> />
{context.advancedFields.includes(name) && <RefreshIcon {context.advancedFields.includes(name) && <RefreshIcon
onTouchTap={() => context.onChange(name, null)} onClick={() => context.onChange(name, null)}
/>} />}
</span> </span>
); );

View File

@ -1,61 +0,0 @@
import React from 'react';
import PropTypes from 'proptypes';
import muiThemeable from 'material-ui/styles/muiThemeable';
import injectSheet from 'react-jss';
import FlatButton from 'material-ui/FlatButton';
import { sleep, getMalyanStatus } from './utils.js';
const styles = {
};
class MalyanControl extends React.Component {
static propTypes = {
ip: PropTypes.string.isRequired
};
state = {
status: null,
mounted: true
};
// componentDidMount = async () => {
// const { ip } = this.props;
// while (this.state.mounted) {
// const status = await getMalyanStatus(ip).catch(() => null);
// this.setState({ status });
// await sleep(1000);
// }
// };
home = () => {
const { ip } = this.props;
fetch(`http://${ip}/set?code=G28`, { method: 'GET', mode: 'no-cors' });
};
stop = () => {
const { ip } = this.props;
fetch(`http://${ip}/set?cmd={P:X}`, { method: 'GET', mode: 'no-cors' });
};
componentWillUnmount() {
this.setState({ mounted: false });
}
render() {
const { status } = this.state;
return (
<div>
{status && <span>
<p>Nozzle temperature: {status.nozzleTemperature}/{status.nozzleTargetTemperature}</p>
<p>Bed temperature: {status.bedTemperature}/{status.bedTargetTemperature}</p>
{status.state === 'printing' && <p>Progress: {status.progress}%</p>}
</span>}
<FlatButton label="Stop" onTouchTap={this.stop} />
<FlatButton label="Home" onTouchTap={this.home} />
</div>
);
}
}
export default muiThemeable()(injectSheet(styles)(MalyanControl));

View File

@ -25,7 +25,6 @@ import { Doodle3DManager } from 'doodle3d-api';
const DOODLE_3D_MANAGER = new Doodle3DManager(); const DOODLE_3D_MANAGER = new Doodle3DManager();
DOODLE_3D_MANAGER.checkNonServerBoxes = false; DOODLE_3D_MANAGER.checkNonServerBoxes = false;
DOODLE_3D_MANAGER.setAutoUpdate(true, 5000);
const CONNECT_URL = 'http://connect.doodle3d.com/'; const CONNECT_URL = 'http://connect.doodle3d.com/';
@ -45,13 +44,16 @@ const styles = {
'& h3': { '& h3': {
fontWeight: 'bold', fontWeight: 'bold',
marginTop: '20px', marginTop: '20px',
marginBottom: '20px', marginBottom: '20px'
} }
}, },
error: { error: {
color: red500 color: red500
}, }
};
const updateLocalStorage = (localStorage) => {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(localStorage));
}; };
const getLocalStorage = () => { const getLocalStorage = () => {
@ -66,10 +68,6 @@ const getLocalStorage = () => {
return localStorage; return localStorage;
}; };
const updateLocalStorage = (localStorage) => {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(localStorage));
};
class Settings extends React.Component { class Settings extends React.Component {
static propTypes = { static propTypes = {
selectedPrinter: PropTypes.string, selectedPrinter: PropTypes.string,
@ -110,9 +108,8 @@ class Settings extends React.Component {
const { localStorage } = this.state; const { localStorage } = this.state;
if (selectedPrinter && localStorage.active) { if (selectedPrinter && localStorage.active) {
const activePrinter = selectedPrinter && Object.entries(localStorage.printers) const activePrinter = selectedPrinter && Object.values(localStorage.printers)
.map(([key, value]) => ({ key, value })) .find(({ ip }) => ip === selectedPrinter);
.find(({ key, value: { ip } }) => ip === selectedPrinter);
if (activePrinter) { if (activePrinter) {
const state = this.changeSettings('activePrinter', activePrinter.key); const state = this.changeSettings('activePrinter', activePrinter.key);
@ -129,8 +126,8 @@ class Settings extends React.Component {
} }
const eventListener = ({ boxes }) => this.setState({ wifiBoxes: boxes }); const eventListener = ({ boxes }) => this.setState({ wifiBoxes: boxes });
this.setState({ wifiBoxes: DOODLE_3D_MANAGER.boxes, eventListener });
DOODLE_3D_MANAGER.addEventListener('boxeschanged', eventListener); DOODLE_3D_MANAGER.addEventListener('boxeschanged', eventListener);
this.setState({ eventListener });
} }
componentWillUnmount() { componentWillUnmount() {
@ -212,7 +209,6 @@ class Settings extends React.Component {
case 'settings.support.density': case 'settings.support.density':
case 'settings.support.minArea': case 'settings.support.minArea':
case 'settings.support.margin': case 'settings.support.margin':
case 'settings.support.speed':
case 'settings.support.flowRate': case 'settings.support.flowRate':
if (!localStorage.active) return this.openAddPrinterDialog(); if (!localStorage.active) return this.openAddPrinterDialog();
@ -307,14 +303,22 @@ class Settings extends React.Component {
}; };
editPrinter = () => { editPrinter = () => {
const { localStorage: { active, printers }, managePrinter: { printer, name, ip } } = this.state; const { localStorage: { active }, managePrinter: { printer, name, ip } } = this.state;
if (!name) { if (!name) {
this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a name' } } })); this.setState(update(this.state, {
managePrinter: {
error: { $set: 'Please enter a name' }
}
}));
return; return;
} }
if (printer === 'doodle3d_printer' && !validateIp(ip)) { if (printer === 'doodle3d_printer' && !validateIp(ip)) {
this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a valid IP adress' } } })); this.setState(update(this.state, {
managePrinter: {
error: { $set: 'Please enter a valid IP adress' }
}
}));
return; return;
} }
@ -356,7 +360,13 @@ class Settings extends React.Component {
closeAddPrinterDialog = (override) => this.setAddPrinterDialog(false, override); closeAddPrinterDialog = (override) => this.setAddPrinterDialog(false, override);
openAddPrinterDialog = (override) => this.setAddPrinterDialog(true, override); openAddPrinterDialog = (override) => this.setAddPrinterDialog(true, override);
setAddPrinterDialog = (open, override = {}) => this.setState({ setAddPrinterDialog = (open, override = {}) => {
if (open) {
DOODLE_3D_MANAGER.setAutoUpdate(true, 10000);
} else {
DOODLE_3D_MANAGER.setAutoUpdate(false);
}
this.setState({
addPrinter: { addPrinter: {
ip: '', ip: '',
name: '', name: '',
@ -366,12 +376,22 @@ class Settings extends React.Component {
...override ...override
} }
}); });
};
closeManagePrinterDialog = () => this.setManagePrinterDialog(false); closeManagePrinterDialog = () => this.setManagePrinterDialog(false);
openManagePrinterDialog = () => this.setManagePrinterDialog(true); openManagePrinterDialog = () => this.setManagePrinterDialog(true);
setManagePrinterDialog = (open) => { setManagePrinterDialog = (open) => {
const { localStorage: { active, printers } } = this.state; const { localStorage: { active, printers } } = this.state;
if (!active) return this.setState({ managePrinter: { open: false } }); if (!active) {
DOODLE_3D_MANAGER.setAutoUpdate(false);
return this.setState({ managePrinter: { open: false } });
}
if (open) {
DOODLE_3D_MANAGER.setAutoUpdate(true, 10000);
} else {
DOODLE_3D_MANAGER.setAutoUpdate(false);
}
this.setState({ this.setState({
managePrinter: { managePrinter: {
open, open,
@ -384,8 +404,8 @@ class Settings extends React.Component {
} }
render() { render() {
const { addPrinter, managePrinter, localStorage, wifiBoxes } = this.state; const { addPrinter, managePrinter, localStorage } = this.state;
const { classes, disabled } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.container}> <div className={classes.container}>
@ -395,10 +415,10 @@ class Settings extends React.Component {
<MenuItem key={id} value={id} primaryText={name} /> <MenuItem key={id} value={id} primaryText={name} />
))} ))}
<Divider /> <Divider />
<MenuItem onTouchTap={this.openAddPrinterDialog} value="add_printer" primaryText="Add Printer" /> <MenuItem onClick={this.openAddPrinterDialog} value="add_printer" primaryText="Add Printer" />
</SelectField> </SelectField>
{localStorage.active && <SettingsIcon {localStorage.active && <SettingsIcon
onTouchTap={this.openManagePrinterDialog} onClick={this.openManagePrinterDialog}
style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }} style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }}
/>} />}
</div> </div>
@ -505,16 +525,16 @@ function printDialog(props, state, title, form, submitText, data, closeDialog, r
actions={[ actions={[
closeDialog && <FlatButton closeDialog && <FlatButton
label="Close" label="Close"
onTouchTap={closeDialog} onClick={closeDialog}
/>, />,
removeActivePrinter && <FlatButton removeActivePrinter && <FlatButton
label="Remove Printer" label="Remove Printer"
onTouchTap={removeActivePrinter} onClick={removeActivePrinter}
/>, />,
<RaisedButton <RaisedButton
label={submitText} label={submitText}
primary primary
onTouchTap={save} onClick={save}
/> />
]} ]}
> >
@ -531,7 +551,7 @@ function printDialog(props, state, title, form, submitText, data, closeDialog, r
{wifiBoxes.map(({ localip, id, wifiboxid }) => (<MenuItem key={id} value={localip} primaryText={wifiboxid} />))} {wifiBoxes.map(({ localip, id, wifiboxid }) => (<MenuItem key={id} value={localip} primaryText={wifiboxid} />))}
</SelectField> </SelectField>
{data.ip && <ExitToAppIcon {data.ip && <ExitToAppIcon
onTouchTap={() => window.open(`${CONNECT_URL}/?uuid=0#control?localip=${data.ip}`, '_blank')} onClick={() => window.open(`${CONNECT_URL}/?uuid=0#control?localip=${data.ip}`, '_blank')}
style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }} style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }}
/>} />}
</div> </div>
@ -540,5 +560,8 @@ function printDialog(props, state, title, form, submitText, data, closeDialog, r
</Dialog> </Dialog>
); );
} }
printDialog.propTypes = {
classes: PropTypes.objectOf(PropTypes.string)
};
export default injectSheet(styles)(Settings); export default injectSheet(styles)(Settings);

View File

@ -1,59 +0,0 @@
import React from 'react';
import PropTypes from 'proptypes';
import muiThemeable from 'material-ui/styles/muiThemeable';
import injectSheet from 'react-jss';
import FlatButton from 'material-ui/FlatButton';
import { Doodle3DBox } from 'doodle3d-api';
const styles = {
};
class WifiBoxControl extends React.Component {
static propTypes = {
ip: PropTypes.string.isRequired
};
state = {
box: null,
status: null
};
componentDidMount = async () => {
const { ip } = this.props;
const box = new Doodle3DBox(ip);
window.d3dbox = box;
box.addEventListener('update', ({ state }) => this.setState({ status: state }));
box.setAutoUpdate(true, 5000);
this.setState({ box });
const alive = await box.checkAlive();
};
stop = async () => {
const { box } = this.state;
const result = await box.printer.stop();
console.log('result: ', result);
};
componentWillUnmount() {
const { box } = this.state;
if (box) box.setAutoUpdate(false);
this.setState({ mounted: false });
}
render() {
const { status } = this.state;
return (
<div>
<FlatButton label="Stop" onTouchTap={this.stop} />
</div>
);
}
}
export default muiThemeable()(injectSheet(styles)(WifiBoxControl));

View File

@ -1,12 +1,9 @@
import * as THREE from 'three'; import * as THREE from 'three';
import _ from 'lodash';
import React from 'react'; import React from 'react';
import PropTypes from 'proptypes'; import PropTypes from 'proptypes';
import { centerGeometry, placeOnGround, createScene, slice, TabTemplate } from './utils.js'; import { centerGeometry, placeOnGround, createScene, slice, TabTemplate } from './utils.js';
import injectSheet from 'react-jss'; import injectSheet from 'react-jss';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton';
import Slider from 'material-ui/Slider';
import LinearProgress from 'material-ui/LinearProgress'; import LinearProgress from 'material-ui/LinearProgress';
import { grey50, grey300, grey800, red500 } from 'material-ui/styles/colors'; import { grey50, grey300, grey800, red500 } from 'material-ui/styles/colors';
import Popover from 'material-ui/Popover/Popover'; import Popover from 'material-ui/Popover/Popover';
@ -14,14 +11,11 @@ import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem'; import MenuItem from 'material-ui/MenuItem';
import { Tabs, Tab } from 'material-ui/Tabs'; import { Tabs, Tab } from 'material-ui/Tabs';
import Settings from './Settings.js'; import Settings from './Settings.js';
// import MalyanControl from './MalyanControl.js';
// import WifiBoxControl from './WifiBoxControl.js';
import ReactResizeDetector from 'react-resize-detector'; import ReactResizeDetector from 'react-resize-detector';
import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData'; import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData';
import createSceneData from 'doodle3d-core/d3/createSceneData.js'; import createSceneData from 'doodle3d-core/d3/createSceneData.js';
import { generateExportMesh } from 'doodle3d-core/utils/exportUtils.js'; import { generateExportMesh } from 'doodle3d-core/utils/exportUtils.js';
import muiThemeable from 'material-ui/styles/muiThemeable'; import muiThemeable from 'material-ui/styles/muiThemeable';
import Dialog from 'material-ui/Dialog';
import logo from '../../img/logo.png'; import logo from '../../img/logo.png';
const MAX_FULLSCREEN_WIDTH = 720; const MAX_FULLSCREEN_WIDTH = 720;
@ -58,7 +52,7 @@ const styles = {
borderLeft: `1px solid ${grey300}` borderLeft: `1px solid ${grey300}`
}, },
sliceActions: { sliceActions: {
flexShrink: 0, flexShrink: 0
}, },
sliceInfo: { sliceInfo: {
margin: '10px 0', margin: '10px 0',
@ -166,8 +160,8 @@ class Interface extends React.Component {
fetch(`${origin}${port}${pathname}`, { headers }) fetch(`${origin}${port}${pathname}`, { headers })
.then(resonse => resonse.json()) .then(resonse => resonse.json())
.then(json => JSONToSketchData(json)) .then(JSONToSketchData)
.then(file => createSceneData(file)) .then(createSceneData)
.then(sketch => generateExportMesh(sketch, { offsetSingleWalls: false, matrix: new THREE.Matrix4() })) .then(sketch => generateExportMesh(sketch, { offsetSingleWalls: false, matrix: new THREE.Matrix4() }))
.then(mesh => this.updateMesh(mesh)); .then(mesh => this.updateMesh(mesh));
}; };
@ -232,7 +226,7 @@ class Interface extends React.Component {
}; };
slice = async (action) => { slice = async (action) => {
const { isSlicing, settings, mesh, scene: { material, mesh: { matrix } } } = this.state; const { isSlicing, settings, mesh, scene: { mesh: { matrix } } } = this.state;
const { name } = this.props; const { name } = this.props;
if (isSlicing) return; if (isSlicing) return;
@ -342,7 +336,7 @@ class Interface extends React.Component {
render() { render() {
const { classes, onCancel, selectedPrinter, actions } = this.props; const { classes, onCancel, selectedPrinter, actions } = this.props;
const { isSlicing, progress, showFullScreen, error, objectDimensions, settings } = this.state; const { isSlicing, progress, showFullScreen, error, objectDimensions } = this.state;
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) }; const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
@ -363,17 +357,13 @@ class Interface extends React.Component {
{onCancel && <RaisedButton {onCancel && <RaisedButton
label="Close" label="Close"
className={`${classes.button}`} className={`${classes.button}`}
onTouchTap={onCancel} onClick={onCancel}
/>} />}
{/* (settings && settings.ip) && ((settings.printer === 'doodle3d_printer') ?
<MalyanControl ip={settings.ip} /> :
<WifiBoxControl ip={settings.ip} />
) */}
{actions.length === 1 ? ( {actions.length === 1 ? (
<RaisedButton <RaisedButton
primary primary
label={actions[0].title} label={actions[0].title}
onTouchTap={() => this.slice(actions[0])} onClick={() => this.slice(actions[0])}
className={`${classes.button}`} className={`${classes.button}`}
disabled={isSlicing} disabled={isSlicing}
/> />
@ -384,19 +374,19 @@ class Interface extends React.Component {
ref="button" ref="button"
primary primary
className={`${classes.button}`} className={`${classes.button}`}
onTouchTap={this.openPopover} onClick={this.openPopover}
disabled={isSlicing} disabled={isSlicing}
/> />
<Popover <Popover
open={this.state.popover.open} open={this.state.popover.open}
anchorEl={this.state.popover.element} anchorEl={this.state.popover.element}
anchorOrigin={{horizontal: 'left', vertical: 'bottom'}} anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
targetOrigin={{horizontal: 'left', vertical: 'bottom'}} targetOrigin={{ horizontal: 'left', vertical: 'bottom' }}
onRequestClose={this.closePopover} onRequestClose={this.closePopover}
> >
<Menu> <Menu>
{actions.map((action) => ( {actions.map((action) => (
<MenuItem key={action.target} primaryText={action.title} onTouchTap={() => this.slice(action)} /> <MenuItem key={action.target} primaryText={action.title} onClick={() => this.slice(action)} />
))} ))}
</Menu> </Menu>
</Popover> </Popover>
@ -415,12 +405,12 @@ class Interface extends React.Component {
<div className={classes.detail}> <div className={classes.detail}>
<p>Dimensions: {objectDimensions}</p> <p>Dimensions: {objectDimensions}</p>
</div> </div>
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.resetMesh} label="reset" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.scaleUp} label="scale down" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.scaleDown} label="scale up" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateX} label="rotate x" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateX} label="rotate x" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateY} label="rotate y" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateY} label="rotate y" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.rotateZ} label="rotate z" /> <RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateZ} label="rotate z" />
</div> </div>
</div> </div>
); );

View File

@ -4,7 +4,6 @@ import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml'; import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml'; import qualitySettings from '../settings/quality.yml';
import { sliceGeometry } from '../slicer.js'; import { sliceGeometry } from '../slicer.js';
import { grey800, red500 } from 'material-ui/styles/colors';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
@ -24,7 +23,7 @@ export function centerGeometry(mesh) {
mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z)); mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
} }
export function createScene({ pixelRatio, muiTheme }) { export function createScene({ muiTheme }) {
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);
@ -52,7 +51,7 @@ export function createScene({ pixelRatio, muiTheme }) {
let renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); let renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
let editorControls = new THREE.EditorControls(camera, renderer.domElement); let editorControls = new THREE.EditorControls(camera, renderer.domElement);
box.scale.set(1., 1., 1.); box.scale.set(1, 1, 1);
box.updateMatrix(); box.updateMatrix();
const render = () => renderer.render(scene, camera); const render = () => renderer.render(scene, camera);
@ -95,16 +94,16 @@ export function fetchProgress(url, data = {}, onProgress) {
// const headers = new Headers(xhr.getAllResponseHeaders() || ''); // const headers = new Headers(xhr.getAllResponseHeaders() || '');
// const { status, statusText, response, responseText, responseURL: url = headers.get('X-Request-URL') } = xhr; // const { status, statusText, response, responseText, responseURL: url = headers.get('X-Request-URL') } = xhr;
// resolve(new Response(response || responseText, { headers, status, statusText, url })); // resolve(new Response(response || responseText, { headers, status, statusText, url }));
} };
xhr.onerror = () => reject(new TypeError('Network request failed')); xhr.onerror = () => reject(new TypeError('Network request failed'));
xhr.ontimeout = () => reject(new TypeError('Network request failed')); xhr.ontimeout = () => reject(new TypeError('Network request failed'));
xhr.open(request.method, url, true); xhr.open(request.method, url, true);
if (request.credentials === 'include') { if (request.credentials === 'include') {
xhr.withCredentials = true xhr.withCredentials = true;
} else if (request.credentials === 'omit') { } else if (request.credentials === 'omit') {
xhr.withCredentials = false xhr.withCredentials = false;
} }
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress; if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress;
if (xhr.responseType) xhr.responseType = 'blob'; if (xhr.responseType) xhr.responseType = 'blob';
@ -134,7 +133,7 @@ export function getMalyanStatus(ip) {
break; break;
} }
return status; return status;
}) });
} }
export function sleep(time) { export function sleep(time) {
@ -153,12 +152,11 @@ export async function slice(action, name, mesh, settings, updateProgress) {
break; break;
case 'WIFI_PRINT': case 'WIFI_PRINT':
if (settings.printer === 'doodle3d_printer') { if (settings.printer === 'doodle3d_printer') {
const { state } = await getMalyanStatus(settings.ip); // const { state } = await getMalyanStatus(settings.ip);
if (state !== 'idle') throw { message: 'printer is busy', code: 0 }; // if (state !== 'idle') throw { message: 'printer is busy', code: 0 };
} else { } else {
wifiBox = new Doodle3DBox(settings.ip); wifiBox = new Doodle3DBox(settings.ip);
if (!await wifiBox.checkAlive()) throw { message: `can't connect to printer`, code: 4 } if (! await wifiBox.checkAlive()) throw { message: `can't connect to printer`, code: 4 };
const { state } = await wifiBox.info.status(); const { state } = await wifiBox.info.status();
if (state !== 'idle') throw { message: 'printer is busy', code: 0 }; if (state !== 'idle') throw { message: 'printer is busy', code: 0 };
@ -170,7 +168,6 @@ export async function slice(action, name, mesh, settings, updateProgress) {
break; break;
default: default:
throw { message: 'unknown target', code: 1 }; throw { message: 'unknown target', code: 1 };
break;
} }
const { dimensions } = settings; const { dimensions } = settings;
@ -214,7 +211,7 @@ export async function slice(action, name, mesh, settings, updateProgress) {
loaded += 15 * 1024; loaded += 15 * 1024;
updateProgress({ updateProgress({
action: 'Uploading to printer', action: 'Uploading to printer',
percentage: (currentStep + loaded / file.size) / steps percentage: (currentStep + loaded / gcode.size) / steps
}); });
}, 1000); }, 1000);
@ -250,7 +247,7 @@ export async function slice(action, name, mesh, settings, updateProgress) {
}); });
currentStep ++; currentStep ++;
const result = await wifiBox.printer.fetch(id); await wifiBox.printer.fetch(id);
} }
break; break;
} }
@ -270,7 +267,6 @@ export async function slice(action, name, mesh, settings, updateProgress) {
default: default:
throw { message: 'unknown target', code: 1 }; throw { message: 'unknown target', code: 1 };
break;
} }
} }
@ -297,5 +293,5 @@ export const TabTemplate = ({ children, selected, style }) => {
TabTemplate.propTypes = { TabTemplate.propTypes = {
children: PropTypes.node, children: PropTypes.node,
selected: PropTypes.bool, selected: PropTypes.bool,
style: PropTypes.object, style: PropTypes.object
}; };

View File

@ -35,7 +35,7 @@ filamentThickness: 2.85
temperature: 210 temperature: 210
bedTemperature: 50 bedTemperature: 50
layerHeight: 0.15 layerHeight: 0.15
combing: true combing: false
thickness: thickness:
top: 0.45 top: 0.45
bottom: 0.45 bottom: 0.45

View File

@ -1,7 +1,7 @@
doodle3d_printer: doodle3d_printer:
startCode: |- startCode: |-
{if heatedBed}M140 S{bedTemperature}
M104 S{temperature} M104 S{temperature}
{if heatedBed}M140 S{bedTemperature}
G28 G28
M109 S{temperature} M109 S{temperature}
{if heatedBed}M190 S{bedTemperature} {if heatedBed}M190 S{bedTemperature}

View File

@ -20,8 +20,8 @@ export default function addBrim(slices, settings) {
const [firstLayer] = slices; const [firstLayer] = slices;
const brim = firstLayer.parts.reduce((brim, { shape }) => ( const brim = firstLayer.parts.reduce((_brim, { shape }) => (
brim.join(shape.offset(nozzleRadius, { _brim.join(shape.offset(nozzleRadius, {
...OFFSET_OPTIONS, ...OFFSET_OPTIONS,
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound' endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
})) }))

View File

@ -1,5 +1,5 @@
import { PRECISION } from '../constants.js';
import { divide } from './helpers/vector2.js'; import { divide } from './helpers/vector2.js';
import { PRECISION } from '../constants.js'
export default function applyPrecision(layers) { export default function applyPrecision(layers) {
for (let layer = 0; layer < layers.length; layer ++) { for (let layer = 0; layer < layers.length; layer ++) {

View File

@ -21,7 +21,8 @@ export default function calculateLayersIntersections(lines, settings) {
if (layerIndex >= 0 && layerIndex < numLayers) { if (layerIndex >= 0 && layerIndex < numLayers) {
const y = layerIndex * layerHeight + Z_OFFSET; const y = layerIndex * layerHeight + Z_OFFSET;
let x, z; let x;
let z;
if (line.start.y === line.end.y) { if (line.start.y === line.end.y) {
x = line.start.x; x = line.start.x;
z = line.start.z; z = line.start.z;

View File

@ -1,7 +1,7 @@
import * as vector2 from './helpers/vector2.js'; import * as vector2 from './helpers/vector2.js';
import * as vector3 from './helpers/vector3.js'; import * as vector3 from './helpers/vector3.js';
export default function createLines(geometry, settings) { export default function createLines(geometry) {
const faces = []; const faces = [];
const lines = []; const lines = [];
const lineLookup = {}; const lineLookup = {};
@ -12,7 +12,7 @@ export default function createLines(geometry, settings) {
const normal = calculateNormal(geometry.vertices, a, b, c); const normal = calculateNormal(geometry.vertices, a, b, c);
// skip faces that point up or down // skip faces that point up or down
if (normal.y > .999 || normal.y < -.999) { if (normal.y > 0.999 || normal.y < -0.999) {
faces.push(null); faces.push(null);
continue; continue;
} }

View File

@ -1,6 +1,5 @@
import { PRECISION } from '../constants.js' import { PRECISION } from '../constants.js';
import getFillTemplate from './getFillTemplate.js'; import getFillTemplate from './getFillTemplate.js';
import Shape from 'clipper-js';
export default function generateInfills(slices, settings) { export default function generateInfills(slices, settings) {
let { let {

View File

@ -1,4 +1,4 @@
import { PRECISION } from '../constants.js' import { PRECISION } from '../constants.js';
const OFFSET_OPTIONS = { const OFFSET_OPTIONS = {
jointType: 'jtSquare', jointType: 'jtSquare',
@ -10,7 +10,6 @@ const OFFSET_OPTIONS = {
export default function generateInnerLines(slices, settings) { export default function generateInnerLines(slices, settings) {
// need to scale up everything because of clipper rounding errors // need to scale up everything because of clipper rounding errors
let { let {
layerHeight,
nozzleDiameter, nozzleDiameter,
thickness: { shell: shellThickness } thickness: { shell: shellThickness }
} = settings; } = settings;

View File

@ -1,6 +1,6 @@
import Shape from 'clipper-js'; import Shape from 'clipper-js';
export default function calculateOutlines(slices, settings) { export default function calculateOutlines(slices) {
for (let layer = 0; layer < slices.length; layer ++) { for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer]; const slice = slices[layer];

View File

@ -1,5 +1,5 @@
import { scale, distanceTo } from './vector2.js'; import { distanceTo } from './vector2.js';
import { PRECISION, VERSION } from '../../constants.js'; import { VERSION } from '../../constants.js';
export const MOVE = 'G'; export const MOVE = 'G';
export const M_COMMAND = 'M'; export const M_COMMAND = 'M';
@ -14,7 +14,7 @@ export default class GCode {
constructor(settings) { constructor(settings) {
this._nozzleToFilamentRatio = 1; this._nozzleToFilamentRatio = 1;
this._gcode = [ this._gcode = [
`; ${JSON.stringify(settings).trim()}`, `; ${JSON.stringify(settings)}`,
`; Generated with Doodle3D Slicer V${VERSION}` `; Generated with Doodle3D Slicer V${VERSION}`
]; ];
this._currentValues = {}; this._currentValues = {};
@ -55,7 +55,7 @@ export default class GCode {
} }
moveTo(x, y, z, { speed }) { moveTo(x, y, z, { speed }) {
const newNozzlePosition = scale({ x, y }, PRECISION); const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition); const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._duration += lineLength / speed; this._duration += lineLength / speed;
@ -74,7 +74,7 @@ export default class GCode {
} }
lineTo(x, y, z, { speed, flowRate }) { lineTo(x, y, z, { speed, flowRate }) {
const newNozzlePosition = scale({ x, y }, PRECISION); const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition); const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate; this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
@ -132,9 +132,9 @@ export default class GCode {
addGCode(gcode, { temperature, bedTemperature, heatedBed }) { addGCode(gcode, { temperature, bedTemperature, heatedBed }) {
gcode = gcode gcode = gcode
.replace(/{temperature}/gi, temperature) .replace(/{temperature}/g, temperature)
.replace(/{bedTemperature}/gi, bedTemperature) .replace(/{if heatedBed}.*?\n/g, str => heatedBed ? str.replace(/{if heatedBed}/g, '') : '')
.replace(/{if heatedBed}/gi, heatedBed ? '' : ';'); .replace(/{bedTemperature}/g, bedTemperature);
this._addGCode(gcode); this._addGCode(gcode);
} }

View File

@ -1,8 +1,10 @@
export function hslToRgb(h, s, l){ export function hslToRgb(h, s, l) {
let r, g, b; let r;
let g;
let b;
if (s === 0) { if (s === 0) {
r = g = b = lightness; r = g = b = l;
} else { } else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q; const p = 2 * l - q;
@ -14,7 +16,7 @@ export function hslToRgb(h, s, l){
return [r, g, b]; return [r, g, b];
} }
function hueToRgb(p, q, t){ function hueToRgb(p, q, t) {
if (t < 0) t += 1; if (t < 0) t += 1;
if (t > 1) t -= 1; if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 6) return p + (q - p) * 6 * t;

View File

@ -1,131 +1,197 @@
import Shape from 'clipper-js'; import { angle, subtract, distanceTo } from './vector2.js';
import { subtract, add, scale, normalize, dot, length, distanceTo } from './vector2.js';
import { PRECISION } from '../../constants.js';
const TOLERANCE = 1 / PRECISION; const graphs = new WeakMap();
export default function comb(polygons, start, end) {
if (!graphs.has(polygons)) graphs.set(polygons, createGraph(polygons));
let { edges, graph, points } = graphs.get(polygons);
export default function comb(outline, start, end) { points = [...points, start, end];
if (distanceTo(start, end) < TOLERANCE) { graph = [...graph];
return [start, end];
}
let combPath = new Shape([[start, end]], false, true, false); const startNode = createNode(graph, points, edges, start);
const endNode = createNode(graph, points, edges, end);
for (let i = 0; i < outline.paths.length; i ++) { let result;
let outlinePart = new Shape([outline.paths[i]], true, false, false, true); if (graph[startNode].some(node => node.to === endNode)) {
result = [start, end];
let snappedCombPaths = outlinePart.orientation(0) ? combPath.intersect(outlinePart) : combPath.difference(outlinePart);
snappedCombPaths = snappedCombPaths.mapToLower();
outlinePart = outlinePart.mapToLower()[0];
if (distanceTo(start, outlinePart[outlinePart.length - 1]) < distanceTo(start, outlinePart[0])) {
outlinePart = outlinePart.reverse();
}
const distanceMap = new WeakMap();
for (let i = 0; i < snappedCombPaths.length; i ++) {
const snappedCombPath = snappedCombPaths[i];
const distanceStart = distanceTo(start, snappedCombPath[0]);
const distanceEnd = distanceTo(start, snappedCombPath[snappedCombPath.length - 1]);
if (distanceStart < distanceEnd) {
distanceMap.set(snappedCombPath, distanceStart);
} else { } else {
snappedCombPath.reverse(); const path = shortestPath(graph, startNode, endNode);
distanceMap.set(snappedCombPath, distanceEnd); if (path) {
} result = path.map(index => points[index]);
}
snappedCombPaths.sort((a, b) => distanceMap.get(a) - distanceMap.get(b));
const firstPath = snappedCombPaths[0];
const lastPath = snappedCombPaths[snappedCombPaths.length - 1];
if (snappedCombPaths.length === 0) {
snappedCombPaths.push([start], [end]);
} else if (distanceTo(firstPath[0], start) > 1.) {
snappedCombPaths.unshift([start]);
} else if (distanceTo(lastPath[lastPath.length - 1], end) > 1.) {
snappedCombPaths.push([end]);
}
if (snappedCombPaths.length === 1) {
continue;
}
const startPath = snappedCombPaths[0];
const startPoint = startPath[startPath.length - 1];
const endPath = snappedCombPaths[snappedCombPaths.length - 1];
const endPoint = endPath[0];
const lineIndexStart = findClosestLineOnPath(outlinePart, startPoint);
const lineIndexEnd = findClosestLineOnPath(outlinePart, endPoint);
const path = [];
if (lineIndexEnd === lineIndexStart) {
continue;
} else if (lineIndexEnd > lineIndexStart) {
if (lineIndexStart + outlinePart.length - lineIndexEnd < lineIndexEnd - lineIndexStart) {
for (let i = lineIndexStart + outlinePart.length; i > lineIndexEnd; i --) {
path.push(outlinePart[i % outlinePart.length]);
}
} else { } else {
for (let i = lineIndexStart; i < lineIndexEnd; i ++) { result = [start, end];
path.push(outlinePart[i + 1]);
}
}
} else {
if (lineIndexEnd + outlinePart.length - lineIndexStart < lineIndexStart - lineIndexEnd) {
for (let i = lineIndexStart; i < lineIndexEnd + outlinePart.length; i ++) {
path.push(outlinePart[(i + 1) % outlinePart.length]);
}
} else {
for (let i = lineIndexStart; i > lineIndexEnd; i --) {
path.push(outlinePart[i]);
}
} }
} }
combPath = new Shape([[...startPath, ...path, ...endPath]], false, true, false, true); return result;
}
return combPath.mapToLower()[0];
} }
function findClosestLineOnPath(path, point) { function createGraph(polygons) {
let distance = Infinity; const points = [];
let lineIndex; const edges = [];
const nextPoints = new WeakMap();
const previousPoints = new WeakMap();
for (let i = 0; i < polygons.length; i ++) {
const polygon = polygons[i];
for (let j = 0; j < polygon.length; j ++) {
const point = polygon[j];
const nextPoint = polygon[(j + 1) % polygon.length];
const previousPoint = polygon[(j - 1 + polygon.length) % polygon.length];
for (let i = 0; i < path.length; i ++) { points.push(point);
const pointA = path[i]; edges.push([point, nextPoint]);
const pointB = path[(i + 1) % path.length]; nextPoints.set(point, nextPoint);
previousPoints.set(point, previousPoint);
const tempClosestPoint = findClosestPointOnLine(pointA, pointB, point);
const tempDistance = distanceTo(tempClosestPoint, point);
if (tempDistance < distance) {
distance = tempDistance;
lineIndex = i;
} }
} }
return lineIndex; const graph = points.map(() => ([]));
for (let i = 0; i < points.length; i ++) {
const a = points[i];
for (let j = i + 1; j < points.length; j ++) {
const b = points[j];
const nextPoint = nextPoints.get(a);
const previousPoint = previousPoints.get(a);
if (!lineIsVisible(previousPoint, nextPoint, edges, a, b)) continue;
const distance = distanceTo(a, b);
const connectNodeA = graph[i];
connectNodeA.push({ to: j, distance });
const connectNodeB = graph[j];
connectNodeB.push({ to: i, distance });
}
}
return { graph, edges, points };
} }
function findClosestPointOnLine(a, b, c) { function createNode(graph, points, edges, point) {
const b_ = subtract(b, a); const node = [];
const c_ = subtract(c, a); const to = graph.length;
graph.push(node);
const lambda = dot(normalize(b_), c_) / length(b_); let previousPoint;
let nextPoint;
if (lambda >= 1) { for (let j = 0; j < edges.length; j ++) {
return b; const edge = edges[j];
} else if (lambda > 0) { if (pointOnLine(edge, point)) [previousPoint, nextPoint] = edge;
return add(a, scale(b_, lambda));
} else {
return a;
} }
for (let i = 0; i < graph.length; i ++) {
const b = points[i];
if (!lineIsVisible(previousPoint, nextPoint, edges, point, b)) continue;
const distance = distanceTo(point, b);
node.push({ to: i, distance });
graph[i] = [...graph[i], { to, distance }];
}
return to;
}
function lineIsVisible(previousPoint, nextPoint, edges, a, b) {
if (b === nextPoint || b === previousPoint) return true;
if (previousPoint && nextPoint) {
const angleLine = angle(subtract(b, a));
const anglePrevious = angle(subtract(previousPoint, a));
const angleNext = angle(subtract(nextPoint, a));
if (betweenAngles(angleLine, anglePrevious, angleNext)) return false;
}
if (lineCrossesEdges(edges, a, b)) return false;
return true;
}
function lineCrossesEdges(edges, a, b) {
for (let i = 0; i < edges.length; i ++) {
const [c, d] = edges[i];
if (lineSegmentsCross(a, b, c, d)) return true;
}
return false;
}
function lineSegmentsCross(a, b, c, d) {
const denominator = ((b.x - a.x) * (d.y - c.y)) - ((b.y - a.y) * (d.x - c.x));
if (denominator === 0.0) return false;
const numerator1 = ((a.y - c.y) * (d.x - c.x)) - ((a.x - c.x) * (d.y - c.y));
const numerator2 = ((a.y - c.y) * (b.x - a.x)) - ((a.x - c.x) * (b.y - a.y));
if (numerator1 === 0.0 || numerator2 === 0.0) return false;
const r = numerator1 / denominator;
const s = numerator2 / denominator;
return (r > 0.0 && r < 1.0) && (s >= 0.0 && s <= 1.0);
}
const TAU = Math.PI * 2.0;
function normalizeAngle(a) {
a %= TAU;
return a > 0.0 ? a : a + TAU;
}
function betweenAngles(n, a, b) {
n = normalizeAngle(n);
a = normalizeAngle(a);
b = normalizeAngle(b);
return a < b ? a <= n && n <= b : a <= n || n <= b;
}
// dijkstra's algorithm
function shortestPath(graph, start, end) {
const distances = graph.map(() => Infinity);
distances[start] = 0;
const traverse = [];
const queue = [];
for (let i = 0; i < distances.length; i ++) {
queue.push(i);
}
while (queue.length > 0) {
let queueIndex;
let minDistance = Infinity;
for (let index = 0; index < queue.length; index ++) {
const nodeIndex = queue[index];
const distance = distances[nodeIndex];
if (distances[nodeIndex] < minDistance) {
queueIndex = index;
minDistance = distance;
}
}
const [nodeIndex] = queue.splice(queueIndex, 1);
const node = graph[nodeIndex];
for (let i = 0; i < node.length; i ++) {
const child = node[i];
const distance = distances[nodeIndex] + child.distance;
if (distance < distances[child.to]) {
distances[child.to] = distance;
traverse[child.to] = nodeIndex;
}
}
}
if (!traverse.hasOwnProperty(end)) return null;
const path = [end];
let nodeIndex = end;
do {
nodeIndex = traverse[nodeIndex];
path.push(nodeIndex);
} while (nodeIndex !== start);
return path.reverse();
}
function pointOnLine([a, b], point) {
return (a.x - point.x) * (a.y - point.y) === (b.x - point.x) * (b.y - point.y);
} }

View File

@ -23,6 +23,7 @@ export const almostEquals = (a, b) => Math.abs(a.x - b.x) < 0.001 && Math.abs(a.
export const dot = (a, b) => a.x * b.x + a.y * b.y; export const dot = (a, b) => a.x * b.x + a.y * b.y;
export const length = (v) => Math.sqrt(v.x * v.x + v.y * v.y); export const length = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
export const distanceTo = (a, b) => length(subtract(a, b)); export const distanceTo = (a, b) => length(subtract(a, b));
export const angle = (v) => Math.atan2(v.y, v.x);
export const normalize = (v) => { export const normalize = (v) => {
const l = length(v); const l = length(v);

View File

@ -1,6 +1,6 @@
import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js'; import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js';
export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings) { export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes) {
const layers = []; const layers = [];
for (let layer = 0; layer < layerPoints.length; layer ++) { for (let layer = 0; layer < layerPoints.length; layer ++) {
@ -59,6 +59,7 @@ export default function intersectionsToShapes(layerPoints, layerFaceIndexes, fac
} else { } else {
lineSegment.push(...startConnects[pointB]); lineSegment.push(...startConnects[pointB]);
endConnects[lineSegment[lineSegment.length - 1]] = lineSegment; endConnects[lineSegment[lineSegment.length - 1]] = lineSegment;
shapes[objectIndex].splice(shapes[objectIndex].indexOf(startConnects[pointB]), 1);
} }
} else { } else {
lineSegment.push(pointB); lineSegment.push(pointB);
@ -70,6 +71,7 @@ export default function intersectionsToShapes(layerPoints, layerFaceIndexes, fac
if (endConnects[pointA]) { if (endConnects[pointA]) {
lineSegment.unshift(...endConnects[pointA]); lineSegment.unshift(...endConnects[pointA]);
startConnects[lineSegment[0]] = lineSegment; startConnects[lineSegment[0]] = lineSegment;
shapes[objectIndex].splice(shapes[objectIndex].indexOf(endConnects[pointA]), 1);
} else { } else {
lineSegment.unshift(pointA); lineSegment.unshift(pointA);
startConnects[pointA] = lineSegment; startConnects[pointA] = lineSegment;
@ -87,7 +89,7 @@ export default function intersectionsToShapes(layerPoints, layerFaceIndexes, fac
for (const objectIndex in shapes) { for (const objectIndex in shapes) {
const shape = shapes[objectIndex] const shape = shapes[objectIndex]
.map(lineSegment => lineSegment.map(pointIndex => points[pointIndex])) .map(lineSegment => lineSegment.map(pointIndex => points[pointIndex]))
.filter(lineSegment => lineSegment.some(i => !almostEquals(lineSegment[0], lineSegment[1]))); .filter(lineSegment => lineSegment.some(point => !almostEquals(lineSegment[0], point)));
const openShape = openObjectIndexes[objectIndex]; const openShape = openObjectIndexes[objectIndex];
const connectPoints = []; const connectPoints = [];

View File

@ -1,7 +1,7 @@
import { length, distanceTo } from './helpers/vector2.js'; import { distanceTo } from './helpers/vector2.js';
import Shape from 'clipper-js'; import Shape from 'clipper-js';
export default function optimizePaths(slices, settings) { export default function optimizePaths(slices) {
let start = { x: 0, y: 0 }; let start = { x: 0, y: 0 };
for (let layer = 0; layer < slices.length; layer ++) { for (let layer = 0; layer < slices.length; layer ++) {
@ -82,12 +82,12 @@ export default function optimizePaths(slices, settings) {
} }
function optimizeShape(shape, start) { function optimizeShape(shape, start) {
const inputPaths = shape.mapToLower(); const inputPaths = shape.mapToLower().filter(path => path.length > 0);
const optimizedPaths = []; const optimizedPaths = [];
const donePaths = []; const donePaths = [];
while (optimizedPaths.length !== inputPaths.length) { while (optimizedPaths.length !== inputPaths.length) {
let minLength = false; let minLength = Infinity;
let reverse; let reverse;
let minPath; let minPath;
let offset; let offset;
@ -101,7 +101,7 @@ function optimizeShape(shape, start) {
if (shape.closed) { if (shape.closed) {
for (let j = 0; j < path.length; j += 1) { for (let j = 0; j < path.length; j += 1) {
const length = distanceTo(path[j], start); const length = distanceTo(path[j], start);
if (minLength === false || length < minLength) { if (length < minLength) {
minPath = path; minPath = path;
minLength = length; minLength = length;
offset = j; offset = j;
@ -110,7 +110,7 @@ function optimizeShape(shape, start) {
} }
} else { } else {
const lengthToStart = distanceTo(path[0], start); const lengthToStart = distanceTo(path[0], start);
if (minLength === false || lengthToStart < minLength) { if (lengthToStart < minLength) {
minPath = path; minPath = path;
minLength = lengthToStart; minLength = lengthToStart;
reverse = false; reverse = false;

View File

@ -3,7 +3,7 @@ import Slice from './helpers/Slice.js';
import { PRECISION, MIN_AREA } from '../constants.js'; import { PRECISION, MIN_AREA } from '../constants.js';
export default function shapesToSlices(shapes, settings) { export default function shapesToSlices(shapes) {
const sliceLayers = []; const sliceLayers = [];
for (let layer = 0; layer < shapes.length; layer ++) { for (let layer = 0; layer < shapes.length; layer ++) {

View File

@ -11,9 +11,9 @@ import shapesToSlices from './shapesToSlices.js';
import slicesToGCode from './slicesToGCode.js'; import slicesToGCode from './slicesToGCode.js';
import applyPrecision from './applyPrecision.js'; import applyPrecision from './applyPrecision.js';
import { hslToRgb } from './helpers/color.js'; import { hslToRgb } from './helpers/color.js';
// // import removePrecision from './removePrecision.js'; import removePrecision from './removePrecision.js';
export default function(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) { export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
const total = 11; const total = 11;
let done = -1; let done = -1;
const updateProgress = action => { const updateProgress = action => {
@ -48,7 +48,7 @@ export default function(settings, geometry, openObjectIndexes, constructLinePrev
updateProgress('Optimizing paths'); updateProgress('Optimizing paths');
optimizePaths(slices, settings); optimizePaths(slices, settings);
// removePrecision(slices); removePrecision(slices);
updateProgress('Constructing gcode'); updateProgress('Constructing gcode');
const gcode = slicesToGCode(slices, settings); const gcode = slicesToGCode(slices, settings);

View File

@ -1,7 +1,6 @@
import GCode from './helpers/GCode.js'; import GCode from './helpers/GCode.js';
import comb from './helpers/comb.js'; import comb from './helpers/comb.js';
import { divide } from './helpers/vector2.js'; import { Z_OFFSET } from '../constants.js';
import { PRECISION, Z_OFFSET } from '../constants.js';
const PROFILE_TYPES = ['support', 'innerShell', 'outerShell', 'innerInfill', 'outerInfill', 'brim']; const PROFILE_TYPES = ['support', 'innerShell', 'outerShell', 'innerInfill', 'outerInfill', 'brim'];
@ -10,14 +9,13 @@ export default function slicesToGCode(slices, settings) {
layerHeight, layerHeight,
filamentThickness, filamentThickness,
nozzleDiameter, nozzleDiameter,
travelSpeed,
retraction, retraction,
travel, travel,
combing combing
} = settings; } = settings;
const gcode = new GCode(settings); const gcode = new GCode(settings);
gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness) gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness);
if (settings.startCode) gcode.addGCode(settings.startCode, settings); if (settings.startCode) gcode.addGCode(settings.startCode, settings);
@ -37,12 +35,12 @@ export default function slicesToGCode(slices, settings) {
isFirstLayer = false; isFirstLayer = false;
} }
const profiles = PROFILE_TYPES.reduce((profiles, profileType) => { const profiles = PROFILE_TYPES.reduce((_profiles, profileType) => {
profiles[profileType] = { _profiles[profileType] = {
...defaultProfile, ...defaultProfile,
lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType] lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType]
}; };
return profiles; return _profiles;
}, {}); }, {});
if (typeof slice.brim !== 'undefined') { if (typeof slice.brim !== 'undefined') {
@ -53,7 +51,7 @@ export default function slicesToGCode(slices, settings) {
const part = slice.parts[i]; const part = slice.parts[i];
if (part.closed) { if (part.closed) {
const outline = part.shell[0]; const outline = part.shell[0].mapToLower();
for (let i = 0; i < part.shell.length; i ++) { for (let i = 0; i < part.shell.length; i ++) {
const shell = part.shell[i]; const shell = part.shell[i];
@ -73,7 +71,8 @@ export default function slicesToGCode(slices, settings) {
} }
if (typeof slice.support !== 'undefined') { if (typeof slice.support !== 'undefined') {
pathToGCode(slice.supportOutline, combing, gcode, slice.support, true, true, z, profiles.support); const supportOutline = slice.supportOutline.mapToLower();
pathToGCode(supportOutline, combing, gcode, slice.support, true, true, z, profiles.support);
} }
} }
@ -82,7 +81,8 @@ export default function slicesToGCode(slices, settings) {
return gcode.getGCode(); return gcode.getGCode();
} }
function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, { lineProfile, travelProfile, retractionProfile }) { function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, profiles) {
const { lineProfile, travelProfile, retractionProfile } = profiles;
const { closed } = shape; const { closed } = shape;
const paths = shape.mapToLower(); const paths = shape.mapToLower();
@ -95,7 +95,7 @@ function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, { li
if (i === 0) { if (i === 0) {
if (combing) { if (combing) {
const combPath = comb(outline, divide(gcode._nozzlePosition, PRECISION), point); const combPath = comb(outline, gcode._nozzlePosition, point);
for (let i = 0; i < combPath.length; i ++) { for (let i = 0; i < combPath.length; i ++) {
const combPoint = combPath[i]; const combPoint = combPath[i];
gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile); gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);

View File

@ -99,6 +99,9 @@ function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview,
if (typeof onProgress !== 'undefined') onProgress(data); if (typeof onProgress !== 'undefined') onProgress(data);
break; break;
} }
default:
break;
} }
}); });

View File

@ -6,7 +6,7 @@ const onProgress = progress => {
message: 'PROGRESS', message: 'PROGRESS',
data: progress data: progress
}); });
} };
self.addEventListener('message', (event) => { self.addEventListener('message', (event) => {
const { message, data } = event.data; const { message, data } = event.data;
@ -28,5 +28,7 @@ self.addEventListener('message', (event) => {
}, buffers); }, buffers);
break; break;
} }
default:
break;
} }
}, false); }, false);

View File

@ -1,6 +1,7 @@
const path = require('path'); const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const HTMLWebpackPlugin = require('html-webpack-plugin'); const HTMLWebpackPlugin = require('html-webpack-plugin');
const GoogleFontsPlugin = require('google-fonts-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const analyzeBundle = process.env.ANALYZE_BUNDLE; const analyzeBundle = process.env.ANALYZE_BUNDLE;
@ -59,10 +60,21 @@ module.exports = {
inline: false, inline: false,
name: '[name].js' name: '[name].js'
} }
}, babelLoader], }, babelLoader]
}, { }, {
test: /\.(png|jpg|gif)$/, test: /\.(png|jpg|gif)$/,
use: ['url-loader?name=images/[name].[ext]'] use: [{
loader: 'file-loader',
options: { name: '[path][name].[ext]' }
},
...(!devMode ? [{
loader: 'image-webpack-loader',
options: {
mozjpeg: { progressive: true, quality: 65 },
optipng: { enabled: false },
pngquant: { quality: '65-90', speed: 4 }
}
}] : [])]
}, { }, {
test: /\.glsl$/, test: /\.glsl$/,
use: ['raw-loader'] use: ['raw-loader']
@ -75,7 +87,20 @@ module.exports = {
title: 'Doodle3D Slicer', title: 'Doodle3D Slicer',
template: require('html-webpack-template'), template: require('html-webpack-template'),
inject: false, inject: false,
hash: !devMode,
appMountId: 'app' appMountId: 'app'
}),
new GoogleFontsPlugin({
fonts: [
{ family: 'Oswald' },
{ family: 'Ranga' },
{ family: 'Joti One' },
{ family: 'Bellefair' },
{ family: 'Lobster' },
{ family: 'Abril Fatface' },
{ family: 'Play' },
{ family: 'Fascinate' }
]
}) })
], ],
devtool: devMode ? 'source-map' : false, devtool: devMode ? 'source-map' : false,