mirror of
https://github.com/Doodle3D/Doodle3D-Slicer.git
synced 2024-11-16 19:17:57 +01:00
Merge branch 'develop'
This commit is contained in:
commit
74ed9e58e4
136
DOCS.md
Normal file
136
DOCS.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Doodle3D Slicer
|
||||||
|
This document explains how the slice process works.
|
||||||
|
|
||||||
|
In this slicer Z is the "up" vector.
|
||||||
|
|
||||||
|
Requisites
|
||||||
|
- 2D Vector math
|
||||||
|
- 3D Vector math
|
||||||
|
- 2D Boolean operations (union, difference)
|
||||||
|
- 2D Path offsetting
|
||||||
|
|
||||||
|
### Step 0: Preparation
|
||||||
|
The first step is to prepare the data for slicing. Most of the model data is mapped into `typed arrays`. This way they can be send to the worker very efficiently (due to the transferable nature of typed arrays).
|
||||||
|
```
|
||||||
|
Vertices: Float32Array
|
||||||
|
Faces: Uint32Array
|
||||||
|
ObjectIndexes: UInt8Array
|
||||||
|
OpenObjectIndexes: [...Int]
|
||||||
|
Settings:
|
||||||
|
startCode: String
|
||||||
|
endcode: String
|
||||||
|
dimensions:
|
||||||
|
x: Number
|
||||||
|
y: Number
|
||||||
|
z: Number
|
||||||
|
heatedBed: Bool
|
||||||
|
nozzleDiameter: Number
|
||||||
|
filamentThickness: Number
|
||||||
|
temperature: Number
|
||||||
|
bedTemperature: Number
|
||||||
|
layerHeight: Number
|
||||||
|
combing: Bool
|
||||||
|
thickness:
|
||||||
|
top: Number
|
||||||
|
bottom: Number
|
||||||
|
shell: Number
|
||||||
|
retraction:
|
||||||
|
enabled: Bool
|
||||||
|
amount: Number
|
||||||
|
speed: Number
|
||||||
|
minDistance: Number
|
||||||
|
travel:
|
||||||
|
speed: Number
|
||||||
|
support:
|
||||||
|
enabled: Bool
|
||||||
|
minArea: Number
|
||||||
|
distanceY: Number
|
||||||
|
density: Number
|
||||||
|
margin: Number
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
innerShell:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
outerShell:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
innerInfill:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
density: Number
|
||||||
|
outerInfill:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
brim:
|
||||||
|
size: Number
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
firstLayer:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
```
|
||||||
|
- Vertices: List of points in 3d
|
||||||
|
- Faces: Indexes refering to points in the vertices list that make a triangular surface
|
||||||
|
- ObjectIndexes: Describes of what object each face is part of (important for the generating of 2d shapes)
|
||||||
|
- OpenObjectIndexes: Determines weather a object is open or closed (important for the generating of 2d shapes)
|
||||||
|
- Settings: object containing all the settings for slicing. We go in depth in this object when it's needed
|
||||||
|
|
||||||
|
### Step 1: Creating lines
|
||||||
|
In this we take the 3d model and look at each surface to extract all individual lines. Note some lines are part of multiple surfaces. In addition we also add some additional data to each line, like the surfaces it is part of we'll also store the 2d normal.
|
||||||
|
|
||||||
|
```
|
||||||
|
function calculateNormal(vertices, a, b, c) {
|
||||||
|
a = getVertex(vertices, a);
|
||||||
|
b = getVertex(vertices, b);
|
||||||
|
c = getVertex(vertices, c);
|
||||||
|
|
||||||
|
const cb = vector3.subtract(c, b);
|
||||||
|
const ab = vector3.subtract(a, b);
|
||||||
|
const normal = vector3.normalize(vector3.cross(cb, ab));
|
||||||
|
|
||||||
|
return normal;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to extract all unique lines from the model we'll loop through each face of the model.
|
||||||
|
|
||||||
|
### Step 2: Calculate Layers Intersections
|
||||||
|
This is a fairly straight forward step. We take the lines and calculate on what layers that line will be intersecting. Additinally we calculate the coordinates where the line intersects each layer.
|
||||||
|
|
||||||
|
### Step 3: Intersections To Shapes
|
||||||
|
### Step 4: Shapes To Slices
|
||||||
|
### Step 5: Generate Inner Lines
|
||||||
|
### Step 6: Generate Outlines
|
||||||
|
### Step 7: Generate Infills
|
||||||
|
### Step 8: Generate Support
|
||||||
|
### Step 9: AddBrim
|
||||||
|
|
||||||
|
```
|
||||||
|
let {
|
||||||
|
brim: { size: brimSize },
|
||||||
|
nozzleDiameter
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
nozzleDiameter /= PRECISION;
|
||||||
|
brimSize /= PRECISION;
|
||||||
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
|
|
||||||
|
const [firstLayer] = slices;
|
||||||
|
|
||||||
|
const brim = firstLayer.parts.reduce((brim, { shape }) => (
|
||||||
|
brim.join(shape.offset(nozzleRadius, {
|
||||||
|
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
||||||
|
}))
|
||||||
|
), new Shape([], true)).simplify('pftNonZero');
|
||||||
|
|
||||||
|
firstLayer.brim = new Shape([], true);
|
||||||
|
|
||||||
|
for (let offset = 0; offset < brimSize; offset += nozzleDiameter) {
|
||||||
|
const brimPart = brim.offset(offset, OFFSET_OPTIONS);
|
||||||
|
firstLayer.brim = firstLayer.brim.join(brimPart);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 10: Optimize Paths
|
||||||
|
### Step 11: Slices To GCode
|
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
img/logo.png
Normal file
BIN
img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
32
index.js
32
index.js
@ -1,17 +1,23 @@
|
|||||||
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 doodleURL from '!url-loader!./models/Doodle_2.d3sketch';
|
|
||||||
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 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';
|
||||||
import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData';
|
import queryString from 'query-string';
|
||||||
import createSceneData from 'doodle3d-core/d3/createSceneData.js';
|
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||||
import { generateExportMesh } from 'doodle3d-core/utils/exportUtils.js';
|
import { grey400, blue500, blue700 } from 'material-ui/styles/colors';
|
||||||
import { Matrix4 } from 'three/src/math/Matrix4.js';
|
|
||||||
|
const muiTheme = getMuiTheme({
|
||||||
|
palette: {
|
||||||
|
primary1Color: blue500,
|
||||||
|
primary2Color: blue700,
|
||||||
|
accent1Color: blue500,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
injectTapEventPlugin();
|
injectTapEventPlugin();
|
||||||
|
|
||||||
@ -26,17 +32,11 @@ jss.createStyleSheet({
|
|||||||
}
|
}
|
||||||
}).attach();
|
}).attach();
|
||||||
|
|
||||||
function init(mesh) {
|
let { file, selectedPrinter, actions } = queryString.parse(location.search);
|
||||||
|
if (actions) actions = JSON.parse(actions);
|
||||||
|
|
||||||
render((
|
render((
|
||||||
<MuiThemeProvider>
|
<MuiThemeProvider muiTheme={muiTheme}>
|
||||||
<Interface mesh={mesh} name="doodle"/>
|
<Interface actions={actions} fileUrl={file} selectedPrinter={selectedPrinter} name="doodle"/>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
), document.getElementById('app'));
|
), document.getElementById('app'));
|
||||||
}
|
|
||||||
|
|
||||||
fetch(doodleURL)
|
|
||||||
.then(resonse => resonse.json())
|
|
||||||
.then(json => JSONToSketchData(json))
|
|
||||||
.then(file => createSceneData(file))
|
|
||||||
.then(sketch => generateExportMesh(sketch, { offsetSingleWalls: false, matrix: new Matrix4() }))
|
|
||||||
.then(init);
|
|
||||||
|
1273
package-lock.json
generated
1273
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -7,7 +7,10 @@
|
|||||||
"esnext": "src/index.js",
|
"esnext": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server -w",
|
"start": "webpack-dev-server -w",
|
||||||
|
"dist": "NODE_ENV=production webpack -p",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
|
"upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print",
|
||||||
|
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p",
|
||||||
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
|
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
|
||||||
"build:main": "BABEL_ENV=main babel src -s -d lib",
|
"build:main": "BABEL_ENV=main babel src -s -d lib",
|
||||||
"build:module": "BABEL_ENV=module babel src -s -d module",
|
"build:module": "BABEL_ENV=module babel src -s -d module",
|
||||||
@ -15,20 +18,28 @@
|
|||||||
"build:module:settings": "cp -r src/settings module"
|
"build:module:settings": "cp -r src/settings module"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@doodle3d/clipper-js": "^1.0.7",
|
"@doodle3d/clipper-js": "^1.0.10",
|
||||||
|
"@doodle3d/doodle3d-api": "^1.0.5",
|
||||||
|
"@doodle3d/doodle3d-core": "github:doodle3d/doodle3d-core#0.18.0",
|
||||||
"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",
|
||||||
"material-ui": "^0.19.4",
|
"material-ui": "^0.19.4",
|
||||||
|
"material-ui-icons": "^1.0.0-beta.17",
|
||||||
|
"material-ui-textfield-icon": "^0.2.2-1",
|
||||||
"proptypes": "^1.1.0",
|
"proptypes": "^1.1.0",
|
||||||
|
"query-string": "^5.0.1",
|
||||||
"react": "^16.0.0",
|
"react": "^16.0.0",
|
||||||
|
"react-addons-update": "^15.6.2",
|
||||||
"react-dom": "^16.0.0",
|
"react-dom": "^16.0.0",
|
||||||
"react-jss": "^7.2.0",
|
"react-jss": "^7.2.0",
|
||||||
"react-resize-detector": "^1.1.0",
|
"react-resize-detector": "^1.1.0",
|
||||||
"three": "^0.88.0"
|
"shortid": "^2.2.8",
|
||||||
|
"three": "^0.88.0",
|
||||||
|
"validate-ip": "^1.0.1",
|
||||||
|
"webpack-bundle-analyzer": "^2.9.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@doodle3d/doodle3d-core": "github:doodle3d/doodle3d-core",
|
|
||||||
"babel-cli": "6.24.1",
|
"babel-cli": "6.24.1",
|
||||||
"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",
|
||||||
|
@ -1 +1,5 @@
|
|||||||
export const PRECISION = 0.01;
|
export const PRECISION = 0.01;
|
||||||
|
export const VERSION = '0.0.18';
|
||||||
|
export const LOCAL_STORAGE_KEY = 'PRINTER_SETTINGS';
|
||||||
|
export const MIN_AREA = 1; // holes smaller as 1mm2 get removed
|
||||||
|
export const Z_OFFSET = 0.2;
|
||||||
|
@ -4,12 +4,14 @@ import _defaultSettings from './settings/default.yml';
|
|||||||
import printerSettings from './settings/printer.yml';
|
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 infillSettings from './settings/infill.yml';
|
||||||
|
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
default: _defaultSettings,
|
default: _defaultSettings,
|
||||||
printer: printerSettings,
|
printer: printerSettings,
|
||||||
material: materialSettings,
|
material: materialSettings,
|
||||||
quality: qualitySettings
|
quality: qualitySettings,
|
||||||
|
infill: infillSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -4,36 +4,102 @@ import _ from 'lodash';
|
|||||||
import injectSheet from 'react-jss';
|
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 TextFieldIcon from 'material-ui-textfield-icon';
|
||||||
|
import RefreshIcon from 'material-ui-icons/Refresh';
|
||||||
|
import muiThemeable from 'material-ui/styles/muiThemeable';
|
||||||
|
|
||||||
const contextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
|
export const contextTypes = {
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool.isRequired,
|
||||||
|
addPrinter: PropTypes.object.isRequired,
|
||||||
|
managePrinter: PropTypes.object.isRequired,
|
||||||
|
advancedFields: PropTypes.array.isRequired,
|
||||||
|
activePrinter: PropTypes.string
|
||||||
|
};
|
||||||
|
const propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
muiTheme: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export const SelectField = (props, context) => (
|
export const _SelectField = ({ name, muiTheme, ...props }, context) => (
|
||||||
<MaterialUISelectField
|
<MaterialUISelectField
|
||||||
{...props}
|
{...props}
|
||||||
disabled={context.disabled}
|
disabled={context.disabled}
|
||||||
value={_.get(context.state, props.name)}
|
value={_.get(context, name)}
|
||||||
onChange={(event, index, value) => context.onChange(props.name, value)}
|
onChange={(event, index, value) => context.onChange(name, value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
SelectField.contextTypes = contextTypes;
|
_SelectField.contextTypes = contextTypes;
|
||||||
|
_SelectField.propTypes = propTypes;
|
||||||
|
export const SelectField = muiThemeable()(_SelectField);
|
||||||
|
|
||||||
export const TextField = (props, context) => (
|
const _TextField = ({ name, muiTheme: { palette }, ...props }, context) => (
|
||||||
<MaterialUITextField
|
<TextFieldIcon
|
||||||
{...props}
|
{...props}
|
||||||
|
icon={context.advancedFields.includes(name) && <RefreshIcon
|
||||||
|
style={{ fill: palette.textColor }}
|
||||||
|
onTouchTap={() => context.onChange(name, null)}
|
||||||
|
/>}
|
||||||
|
floatingLabelStyle={{
|
||||||
|
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
|
||||||
|
}}
|
||||||
disabled={context.disabled}
|
disabled={context.disabled}
|
||||||
value={_.get(context.state, props.name)}
|
value={_.get(context, name)}
|
||||||
onChange={(event, value) => context.onChange(props.name, value)}
|
onChange={(event, value) => context.onChange(name, value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
TextField.contextTypes = contextTypes;
|
_TextField.contextTypes = contextTypes;
|
||||||
|
_TextField.propTypes = propTypes;
|
||||||
|
export const TextField = muiThemeable()(_TextField);
|
||||||
|
|
||||||
export const Checkbox = (props, context) => (
|
const _NumberField = ({ name, min, max, muiTheme: { palette }, ...props }, context) => (
|
||||||
|
<TextFieldIcon
|
||||||
|
{...props}
|
||||||
|
type="number"
|
||||||
|
icon={context.advancedFields.includes(name) && <RefreshIcon
|
||||||
|
style={{ fill: palette.textColor }}
|
||||||
|
onTouchTap={() => context.onChange(name, null)} />
|
||||||
|
}
|
||||||
|
floatingLabelStyle={{
|
||||||
|
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
|
||||||
|
}}
|
||||||
|
disabled={context.disabled}
|
||||||
|
value={_.get(context, name.toString())}
|
||||||
|
onChange={(event, value) => {
|
||||||
|
value = parseFloat(value);
|
||||||
|
context.onChange(name, value);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const value = _.get(context, name.toString());
|
||||||
|
let newValue = value;
|
||||||
|
if (typeof min === 'number') newValue = Math.max(newValue, min);
|
||||||
|
if (typeof max === 'number') newValue = Math.min(newValue, max);
|
||||||
|
if (newValue !== value) context.onChange(name, newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
_NumberField.contextTypes = contextTypes;
|
||||||
|
_NumberField.propTypes = propTypes;
|
||||||
|
export const NumberField = muiThemeable()(_NumberField);
|
||||||
|
|
||||||
|
const _Checkbox = ({ name, muiTheme: { palette }, ...props }, context) => (
|
||||||
|
<span style={{ display: 'flex', position: 'relative' }}>
|
||||||
<MaterialUICheckbox
|
<MaterialUICheckbox
|
||||||
{...props}
|
{...props}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
iconStyle={{
|
||||||
|
fill: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
|
||||||
|
}}
|
||||||
disabled={context.disabled}
|
disabled={context.disabled}
|
||||||
checked={_.get(context.state, props.name)}
|
checked={_.get(context, name)}
|
||||||
onCheck={(event, value) => context.onChange(props.name, value)}
|
onCheck={(event, value) => context.onChange(name, value)}
|
||||||
/>
|
/>
|
||||||
|
{context.advancedFields.includes(name) && <RefreshIcon
|
||||||
|
onTouchTap={() => context.onChange(name, null)}
|
||||||
|
/>}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
Checkbox.contextTypes = contextTypes;
|
_Checkbox.contextTypes = contextTypes;
|
||||||
|
_Checkbox.propTypes = propTypes;
|
||||||
|
export const Checkbox = muiThemeable()(_Checkbox);
|
||||||
|
61
src/interface/MalyanControl.js
Normal file
61
src/interface/MalyanControl.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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));
|
@ -4,98 +4,420 @@ 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 { SelectField, TextField, Checkbox } from './FormComponents.js';
|
import { SelectField, TextField, NumberField, Checkbox } from './FormComponents.js';
|
||||||
import { grey800, cyan500 } from 'material-ui/styles/colors';
|
import { grey800, red500 } from 'material-ui/styles/colors';
|
||||||
|
import Divider from 'material-ui/Divider';
|
||||||
|
import Dialog from 'material-ui/Dialog';
|
||||||
|
import FlatButton from 'material-ui/FlatButton';
|
||||||
|
import RaisedButton from 'material-ui/RaisedButton';
|
||||||
|
import { LOCAL_STORAGE_KEY } from '../constants.js';
|
||||||
|
import shortid from 'shortid';
|
||||||
|
import defaultSettings from '../settings/default.yml';
|
||||||
|
import printerSettings from '../settings/printer.yml';
|
||||||
|
import materialSettings from '../settings/material.yml';
|
||||||
|
import qualitySettings from '../settings/quality.yml';
|
||||||
|
import infillSettings from '../settings/infill.yml';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import SettingsIcon from 'material-ui-icons/Settings';
|
||||||
|
import ExitToAppIcon from 'material-ui-icons/ExitToApp';
|
||||||
|
import validateIp from 'validate-ip';
|
||||||
|
import { Doodle3DManager } from 'doodle3d-api';
|
||||||
|
|
||||||
|
const DOODLE_3D_MANAGER = new Doodle3DManager();
|
||||||
|
DOODLE_3D_MANAGER.checkNonServerBoxes = false;
|
||||||
|
DOODLE_3D_MANAGER.setAutoUpdate(true, 5000);
|
||||||
|
|
||||||
|
const CONNECT_URL = 'http://connect.doodle3d.com/';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
textFieldRow: {
|
textFieldRow: {
|
||||||
display: 'flex'
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
'& p, h3': {
|
'& p': {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
margin: '30px 0 0 0'
|
margin: '30px 0 0 0'
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginTop: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: red500
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalStorage = () => {
|
||||||
|
let localStorage = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (!localStorage) {
|
||||||
|
localStorage = { printers: {}, active: null };
|
||||||
|
updateLocalStorage(localStorage);
|
||||||
|
} else {
|
||||||
|
localStorage = JSON.parse(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 childContextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
|
static propTypes = {
|
||||||
|
selectedPrinter: PropTypes.string,
|
||||||
|
classes: PropTypes.objectOf(PropTypes.string),
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
disabled: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
static defaultProps: {
|
static defaultProps: {
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
static propTypes = {
|
static childContextTypes = {
|
||||||
classes: PropTypes.objectOf(PropTypes.string),
|
settings: PropTypes.object.isRequired,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func.isRequired,
|
||||||
printers: PropTypes.object.isRequired,
|
disabled: PropTypes.bool.isRequired,
|
||||||
defaultPrinter: PropTypes.string,
|
addPrinter: PropTypes.object.isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
managePrinter: PropTypes.object.isRequired,
|
||||||
defaultQuality: PropTypes.string.isRequired,
|
activePrinter: PropTypes.string,
|
||||||
material: PropTypes.object.isRequired,
|
advancedFields: PropTypes.array.isRequired
|
||||||
defaultMaterial: PropTypes.string.isRequired,
|
|
||||||
initialSettings: PropTypes.object.isRequired,
|
|
||||||
disabled: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
constructor(props) {
|
|
||||||
super();
|
state = {
|
||||||
this.state = {
|
localStorage: getLocalStorage(),
|
||||||
settings: props.initialSettings,
|
wifiBoxes: [],
|
||||||
printers: props.defaultPrinter,
|
addPrinter: {
|
||||||
quality: props.defaultQuality,
|
open: false,
|
||||||
material: props.defaultMaterial
|
name: '',
|
||||||
|
printer: '',
|
||||||
|
ip: '',
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
managePrinter: {
|
||||||
|
open: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { onChange, selectedPrinter } = this.props;
|
||||||
|
const { localStorage } = this.state;
|
||||||
|
|
||||||
|
if (selectedPrinter && localStorage.active) {
|
||||||
|
const activePrinter = selectedPrinter && Object.entries(localStorage.printers)
|
||||||
|
.map(([key, value]) => ({ key, value }))
|
||||||
|
.find(({ key, value: { ip } }) => ip === selectedPrinter);
|
||||||
|
|
||||||
|
if (activePrinter) {
|
||||||
|
const state = this.changeSettings('activePrinter', activePrinter.key);
|
||||||
|
if (onChange) onChange(this.constructSettings(state.localStorage));
|
||||||
|
} else {
|
||||||
|
this.openAddPrinterDialog({ ip: selectedPrinter });
|
||||||
|
}
|
||||||
|
} else if (!selectedPrinter && localStorage.active) {
|
||||||
|
if (onChange) onChange(this.constructSettings(localStorage));
|
||||||
|
} else if (selectedPrinter && !localStorage.active) {
|
||||||
|
this.openAddPrinterDialog({ ip: selectedPrinter });
|
||||||
|
} else if (!selectedPrinter && !localStorage.active) {
|
||||||
|
this.openAddPrinterDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventListener = ({ boxes }) => this.setState({ wifiBoxes: boxes });
|
||||||
|
this.setState({ wifiBoxes: DOODLE_3D_MANAGER.boxes, eventListener });
|
||||||
|
DOODLE_3D_MANAGER.addEventListener('boxeschanged', eventListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const { eventListener } = this.state;
|
||||||
|
DOODLE_3D_MANAGER.removeEventListener('boxeschanged', eventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeSettings = (fieldName, value) => {
|
changeSettings = (fieldName, value) => {
|
||||||
const { onChange } = this.props;
|
const { onChange } = this.props;
|
||||||
|
const { localStorage } = this.state;
|
||||||
|
|
||||||
|
let state = _.cloneDeep(this.state);
|
||||||
|
|
||||||
let state;
|
|
||||||
switch (fieldName) {
|
switch (fieldName) {
|
||||||
case 'printers':
|
case 'managePrinter.printer':
|
||||||
case 'quality':
|
case 'managePrinter.name':
|
||||||
case 'material':
|
case 'managePrinter.ip':
|
||||||
state = {
|
state = _.set(state, fieldName, value);
|
||||||
[fieldName]: value,
|
state = update(state, { managePrinter: { error: { $set: null } } });
|
||||||
settings: _.merge({}, this.state.settings, this.props[fieldName][value])
|
break;
|
||||||
};
|
|
||||||
|
case 'addPrinter.printer':
|
||||||
|
case 'addPrinter.name':
|
||||||
|
case 'addPrinter.ip':
|
||||||
|
state = _.set(state, fieldName, value);
|
||||||
|
if (fieldName === 'addPrinter.printer') {
|
||||||
|
state = update(state, { addPrinter: { name: { $set: printerSettings[value].title } } });
|
||||||
|
}
|
||||||
|
state = update(state, { addPrinter: { error: { $set: null } } });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'activePrinter':
|
||||||
|
if (value !== 'add_printer') state = update(state, { localStorage: { active: { $set: value } } });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'settings.infill':
|
||||||
|
case 'settings.quality':
|
||||||
|
case 'settings.material':
|
||||||
|
if (!localStorage.active) return this.openAddPrinterDialog();
|
||||||
|
|
||||||
|
state = _.set(state, `localStorage.printers[${localStorage.active}].${fieldName}`, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'settings.layerHeight':
|
||||||
|
case 'settings.dimensions.x':
|
||||||
|
case 'settings.dimensions.y':
|
||||||
|
case 'settings.dimensions.z':
|
||||||
|
case 'settings.nozzleDiameter':
|
||||||
|
case 'settings.bedTemperature':
|
||||||
|
case 'settings.heatedBed':
|
||||||
|
case 'settings.filamentThickness':
|
||||||
|
case 'settings.temperature':
|
||||||
|
case 'settings.thickness.top':
|
||||||
|
case 'settings.thickness.bottom':
|
||||||
|
case 'settings.thickness.shell':
|
||||||
|
case 'settings.retraction.enabled':
|
||||||
|
case 'settings.retraction.amount':
|
||||||
|
case 'settings.retraction.speed':
|
||||||
|
case 'settings.retraction.minDistance':
|
||||||
|
case 'settings.travel.speed':
|
||||||
|
case 'settings.combing':
|
||||||
|
case 'settings.innerShell.speed':
|
||||||
|
case 'settings.innerShell.flowRate':
|
||||||
|
case 'settings.outerShell.speed':
|
||||||
|
case 'settings.outerShell.flowRate':
|
||||||
|
case 'settings.innerInfill.density':
|
||||||
|
case 'settings.innerInfill.speed':
|
||||||
|
case 'settings.innerInfill.flowRate':
|
||||||
|
case 'settings.outerInfill.speed':
|
||||||
|
case 'settings.outerInfill.flowRate':
|
||||||
|
case 'settings.brim.size':
|
||||||
|
case 'settings.brim.speed':
|
||||||
|
case 'settings.brim.flowRate':
|
||||||
|
case 'settings.firstLayer.speed':
|
||||||
|
case 'settings.firstLayer.flowRate':
|
||||||
|
case 'settings.support.enabled':
|
||||||
|
case 'settings.support.speed':
|
||||||
|
case 'settings.support.distanceY':
|
||||||
|
case 'settings.support.density':
|
||||||
|
case 'settings.support.minArea':
|
||||||
|
case 'settings.support.margin':
|
||||||
|
case 'settings.support.speed':
|
||||||
|
case 'settings.support.flowRate':
|
||||||
|
if (!localStorage.active) return this.openAddPrinterDialog();
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
const advanced = { ...state.localStorage.printers[localStorage.active].settings.advanced };
|
||||||
|
delete advanced[fieldName];
|
||||||
|
state = update(state, { localStorage: { printers: { [localStorage.active]: { settings: { advanced: { $set: advanced } } } } } });
|
||||||
|
} else {
|
||||||
|
state = _.set(state, `localStorage.printers[${localStorage.active}].settings.advanced[${JSON.stringify(fieldName)}]`, value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
state = _.set(_.cloneDeep(this.state), fieldName, value);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (onChange) onChange(state);
|
this.setState(state);
|
||||||
if (state) this.setState(state);
|
if (localStorage.active) {
|
||||||
};
|
if (onChange) onChange(this.constructSettings(state.localStorage));
|
||||||
|
updateLocalStorage(state.localStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
return { state: this.state, onChange: this.changeSettings, disabled: this.props.disabled };
|
const { localStorage, addPrinter, managePrinter } = this.state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
addPrinter,
|
||||||
|
managePrinter,
|
||||||
|
activePrinter: localStorage.active,
|
||||||
|
advancedFields: localStorage.active ? Object.keys(localStorage.printers[localStorage.active].settings.advanced) : [],
|
||||||
|
settings: this.constructSettings(localStorage),
|
||||||
|
onChange: this.changeSettings,
|
||||||
|
disabled: this.props.disabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructSettings(localStorage) {
|
||||||
|
if (!localStorage.active) return defaultSettings;
|
||||||
|
|
||||||
|
const { ip, settings: { printer, material, quality, infill, advanced } } = localStorage.printers[localStorage.active];
|
||||||
|
let settings = {
|
||||||
|
...defaultSettings,
|
||||||
|
printer,
|
||||||
|
material,
|
||||||
|
quality,
|
||||||
|
infill,
|
||||||
|
ip
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = _.merge({}, settings, printerSettings[printer]);
|
||||||
|
settings = _.merge({}, settings, qualitySettings[quality]);
|
||||||
|
settings = _.merge({}, settings, infillSettings[infill]);
|
||||||
|
settings = _.merge({}, settings, materialSettings[material]);
|
||||||
|
|
||||||
|
for (const key in advanced) {
|
||||||
|
const value = advanced[key];
|
||||||
|
settings = _.set(_.cloneDeep(settings), key.replace('settings.', ''), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPrinter = () => {
|
||||||
|
const { name, printer, ip } = this.state.addPrinter;
|
||||||
|
|
||||||
|
if (!name || !printer) {
|
||||||
|
this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a name and printer' } } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (printer === 'doodle3d_printer' && ip !== '' && !validateIp(ip)) {
|
||||||
|
this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a valid IP adress' } } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = shortid.generate();
|
||||||
|
const localStorage = {
|
||||||
|
active: id,
|
||||||
|
printers: {
|
||||||
|
...this.state.localStorage.printers,
|
||||||
|
[id]: { name, ip, settings: { printer, material: 'pla', infill: '20pct', quality: 'medium', advanced: {} } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.setState({ localStorage });
|
||||||
|
updateLocalStorage(localStorage);
|
||||||
|
|
||||||
|
this.closeAddPrinterDialog();
|
||||||
|
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (onChange) onChange(this.constructSettings(localStorage));
|
||||||
|
};
|
||||||
|
|
||||||
|
editPrinter = () => {
|
||||||
|
const { localStorage: { active, printers }, managePrinter: { printer, name, ip } } = this.state;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a name' } } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (printer === 'doodle3d_printer' && !validateIp(ip)) {
|
||||||
|
this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a valid IP adress' } } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localStorage = update(this.state.localStorage, {
|
||||||
|
printers: {
|
||||||
|
[active]: {
|
||||||
|
name: { $set: name },
|
||||||
|
ip: { $set: ip },
|
||||||
|
settings: {
|
||||||
|
printer: { $set: printer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.closeManagePrinterDialog();
|
||||||
|
this.setState({ localStorage });
|
||||||
|
updateLocalStorage(localStorage);
|
||||||
|
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (onChange) onChange(this.constructSettings(localStorage));
|
||||||
|
};
|
||||||
|
|
||||||
|
removeActivePrinter = () => {
|
||||||
|
let { localStorage: { active, printers } } = this.state;
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
printers = { ...printers };
|
||||||
|
delete printers[active];
|
||||||
|
active = Object.keys(printers)[0] || null;
|
||||||
|
const localStorage = { active, printers };
|
||||||
|
|
||||||
|
this.closeManagePrinterDialog();
|
||||||
|
this.setState({ localStorage });
|
||||||
|
updateLocalStorage(localStorage);
|
||||||
|
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (onChange) onChange(this.constructSettings(localStorage));
|
||||||
|
};
|
||||||
|
|
||||||
|
closeAddPrinterDialog = (override) => this.setAddPrinterDialog(false, override);
|
||||||
|
openAddPrinterDialog = (override) => this.setAddPrinterDialog(true, override);
|
||||||
|
setAddPrinterDialog = (open, override = {}) => this.setState({
|
||||||
|
addPrinter: {
|
||||||
|
ip: '',
|
||||||
|
name: '',
|
||||||
|
printer: '',
|
||||||
|
error: null,
|
||||||
|
open,
|
||||||
|
...override
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeManagePrinterDialog = () => this.setManagePrinterDialog(false);
|
||||||
|
openManagePrinterDialog = () => this.setManagePrinterDialog(true);
|
||||||
|
setManagePrinterDialog = (open) => {
|
||||||
|
const { localStorage: { active, printers } } = this.state;
|
||||||
|
if (!active) return this.setState({ managePrinter: { open: false } });
|
||||||
|
this.setState({
|
||||||
|
managePrinter: {
|
||||||
|
open,
|
||||||
|
name: printers[active].name,
|
||||||
|
ip: printers[active].ip,
|
||||||
|
printer: printers[active].settings.printer,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, printers, quality, material, disabled } = this.props;
|
const { addPrinter, managePrinter, localStorage, wifiBoxes } = this.state;
|
||||||
|
const { classes, disabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.container}>
|
<div className={classes.container}>
|
||||||
<SelectField name="printers" floatingLabelText="Printer" fullWidth>
|
<div className={classes.textFieldRow}>
|
||||||
{Object.entries(printers).map(([value, { title }]) => (
|
<SelectField name="activePrinter" floatingLabelText="Printer" fullWidth>
|
||||||
|
{Object.entries(localStorage.printers).map(([id, { name }]) => (
|
||||||
|
<MenuItem key={id} value={id} primaryText={name} />
|
||||||
|
))}
|
||||||
|
<Divider />
|
||||||
|
<MenuItem onTouchTap={this.openAddPrinterDialog} value="add_printer" primaryText="Add Printer" />
|
||||||
|
</SelectField>
|
||||||
|
{localStorage.active && <SettingsIcon
|
||||||
|
onTouchTap={this.openManagePrinterDialog}
|
||||||
|
style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
<SelectField name="settings.material" floatingLabelText="Material" fullWidth>
|
||||||
|
{Object.entries(materialSettings).map(([value, { title }]) => (
|
||||||
<MenuItem key={value} value={value} primaryText={title} />
|
<MenuItem key={value} value={value} primaryText={title} />
|
||||||
))}
|
))}
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<SelectField name="material" floatingLabelText="Material" fullWidth>
|
<h3>Print Setup</h3>
|
||||||
{Object.entries(material).map(([value, { title }]) => (
|
<Tabs>
|
||||||
<MenuItem key={value} value={value} primaryText={title} />
|
|
||||||
))}
|
|
||||||
</SelectField>
|
|
||||||
<h3>Printer Setup</h3>
|
|
||||||
<Tabs inkBarStyle={{ backgroundColor: cyan500 }}>
|
|
||||||
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Basic">
|
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Basic">
|
||||||
<div>
|
<div>
|
||||||
<SelectField name="quality" floatingLabelText="Quality" fullWidth>
|
<SelectField name="settings.quality" floatingLabelText="Quality" fullWidth>
|
||||||
{Object.entries(quality).map(([value, { title }]) => (
|
{Object.entries(qualitySettings).map(([value, { title }]) => (
|
||||||
|
<MenuItem key={value} value={value} primaryText={title} />
|
||||||
|
))}
|
||||||
|
</SelectField>
|
||||||
|
<SelectField name="settings.infill" floatingLabelText="Infill" fullWidth>
|
||||||
|
{Object.entries(infillSettings).map(([value, { title }]) => (
|
||||||
<MenuItem key={value} value={value} primaryText={title} />
|
<MenuItem key={value} value={value} primaryText={title} />
|
||||||
))}
|
))}
|
||||||
</SelectField>
|
</SelectField>
|
||||||
@ -104,59 +426,119 @@ class Settings extends React.Component {
|
|||||||
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Advanced">
|
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Advanced">
|
||||||
<div>
|
<div>
|
||||||
<p>Layer</p>
|
<p>Layer</p>
|
||||||
<TextField name="settings.layerHeight" fullWidth floatingLabelText="Height" type="number" />
|
<NumberField name="settings.layerHeight" min={0.05} max={3} fullWidth floatingLabelText="Height" />
|
||||||
<p>Printer dimensions</p>
|
|
||||||
<div className={classes.textFieldRow}>
|
|
||||||
<TextField name="settings.dimensions.x" fullWidth floatingLabelText="X" type="number" />
|
|
||||||
<TextField name="settings.dimensions.y" fullWidth floatingLabelText="Y" type="number" />
|
|
||||||
<TextField name="settings.dimensions.z" fullWidth floatingLabelText="Z" type="number" />
|
|
||||||
</div>
|
|
||||||
<p>Nozzle</p>
|
|
||||||
<TextField name="settings.nozzleDiameter" fullWidth floatingLabelText="Diameter" type="number" />
|
|
||||||
<p>Bed</p>
|
|
||||||
<TextField name="settings.bedTemperature" fullWidth floatingLabelText="Temperature" type="number" />
|
|
||||||
<Checkbox name="settings.heatedBed" label="Heated" />
|
|
||||||
<p>Material</p>
|
|
||||||
<TextField name="settings.filamentThickness" fullWidth floatingLabelText="Thickness" type="number" />
|
|
||||||
<TextField name="settings.temperature" fullWidth floatingLabelText="Temperature" type="number" />
|
|
||||||
<p>Thickness</p>
|
<p>Thickness</p>
|
||||||
<TextField name="settings.thickness.top" fullWidth floatingLabelText="top" type="number" />
|
<NumberField name="settings.thickness.top" min={0} fullWidth floatingLabelText="top" />
|
||||||
<TextField name="settings.thickness.bottom" fullWidth floatingLabelText="bottom" type="number" />
|
<NumberField name="settings.thickness.bottom" min={0} fullWidth floatingLabelText="bottom" />
|
||||||
<TextField name="settings.thickness.shell" fullWidth floatingLabelText="shell" type="number" />
|
<NumberField name="settings.thickness.shell" min={0} fullWidth floatingLabelText="shell" />
|
||||||
|
<p>Material</p>
|
||||||
|
<NumberField name="settings.filamentThickness" min={0.1} max={10} fullWidth floatingLabelText="Thickness" />
|
||||||
|
<NumberField name="settings.temperature" min={100} max={400} fullWidth floatingLabelText="Temperature" />
|
||||||
|
<p>Bed</p>
|
||||||
|
<NumberField name="settings.bedTemperature" min={30} max={150} fullWidth floatingLabelText="Temperature" />
|
||||||
|
<Checkbox name="settings.heatedBed" label="Heated" />
|
||||||
|
<p>Brim</p>
|
||||||
|
<NumberField name="settings.brim.size" min={0} max={20} fullWidth floatingLabelText="Size" />
|
||||||
|
<NumberField name="settings.brim.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.brim.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Support</p>
|
||||||
|
<Checkbox name="settings.support.enabled" label="Enabled" />
|
||||||
|
<NumberField name="settings.support.distanceY" min={0.1} fullWidth floatingLabelText="Distance Y" />
|
||||||
|
<NumberField name="settings.support.density" min={0} max={100} fullWidth floatingLabelText="Density" />
|
||||||
|
<NumberField name="settings.support.margin" min={0.1} fullWidth floatingLabelText="Margin" />
|
||||||
|
<NumberField name="settings.support.minArea" min={1} fullWidth floatingLabelText="Min Area" />
|
||||||
|
<NumberField name="settings.support.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.support.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>First layer</p>
|
||||||
|
<NumberField name="settings.firstLayer.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.firstLayer.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Inner shell</p>
|
||||||
|
<NumberField name="settings.innerShell.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.innerShell.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Outer shell</p>
|
||||||
|
<NumberField name="settings.outerShell.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.outerShell.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Inner infill</p>
|
||||||
|
<NumberField name="settings.innerInfill.density" min={0} max={100} fullWidth floatingLabelText="Density" />
|
||||||
|
<NumberField name="settings.innerInfill.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.innerInfill.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Outer infill</p>
|
||||||
|
<NumberField name="settings.outerInfill.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<NumberField name="settings.outerInfill.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
|
||||||
|
<p>Travel</p>
|
||||||
|
<NumberField name="settings.travel.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
|
<Checkbox name="settings.combing" label="Combing" />
|
||||||
<p>Retraction</p>
|
<p>Retraction</p>
|
||||||
<Checkbox name="settings.retraction.enabled" label="Enabled" />
|
<Checkbox name="settings.retraction.enabled" label="Enabled" />
|
||||||
<TextField name="settings.retraction.amount" fullWidth floatingLabelText="Amount" type="number" />
|
<NumberField name="settings.retraction.amount" min={0} max={10} fullWidth floatingLabelText="Amount" />
|
||||||
<TextField name="settings.retraction.speed" fullWidth floatingLabelText="Speed" type="number" />
|
<NumberField name="settings.retraction.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
|
||||||
<TextField name="settings.retraction.minDistance" fullWidth floatingLabelText="Min distance" type="number" />
|
<NumberField name="settings.retraction.minDistance" min={0} fullWidth floatingLabelText="Min distance" />
|
||||||
<p>Travel</p>
|
<p>Printer dimensions</p>
|
||||||
<TextField name="settings.travel.speed" fullWidth floatingLabelText="Speed" type="number" />
|
<div className={classes.textFieldRow}>
|
||||||
<Checkbox name="settings.combing" label="Combing" />
|
<NumberField name="settings.dimensions.x" min={1} fullWidth floatingLabelText="X" />
|
||||||
<p>Inner shell</p>
|
<NumberField name="settings.dimensions.y" min={1} fullWidth floatingLabelText="Y" />
|
||||||
<TextField name="settings.innerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
|
<NumberField name="settings.dimensions.z" min={1} fullWidth floatingLabelText="Z" />
|
||||||
<TextField name="settings.innerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
</div>
|
||||||
<p>Outer shell</p>
|
<p>Nozzle</p>
|
||||||
<TextField name="settings.outerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
|
<NumberField name="settings.nozzleDiameter" min={0.1} max={5} fullWidth floatingLabelText="Diameter" />
|
||||||
<TextField name="settings.outerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
|
||||||
<p>Inner infill</p>
|
|
||||||
<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.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
|
||||||
<p>Outer infill</p>
|
|
||||||
<TextField name="settings.outerInfill.speed" fullWidth floatingLabelText="Speed" type="number" />
|
|
||||||
<TextField name="settings.outerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
|
||||||
<p>Brim</p>
|
|
||||||
<TextField name="settings.brim.offset" fullWidth floatingLabelText="Offset" type="number" />
|
|
||||||
<TextField name="settings.brim.speed" fullWidth floatingLabelText="Speed" type="number" />
|
|
||||||
<TextField name="settings.brim.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
|
||||||
<p>First layer</p>
|
|
||||||
<TextField name="settings.firstLayer.speed" fullWidth floatingLabelText="Speed" type="number" />
|
|
||||||
<TextField name="settings.firstLayer.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
{printDialog(this.props, this.state, 'Add Printer', 'addPrinter', 'Add', addPrinter, localStorage.active && this.closeAddPrinterDialog, null, this.addPrinter)}
|
||||||
|
{printDialog(this.props, this.state, 'Manage Printer', 'managePrinter', 'Save', managePrinter, this.closeManagePrinterDialog, this.removeActivePrinter, this.editPrinter)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printDialog(props, state, title, form, submitText, data, closeDialog, removeActivePrinter, save) {
|
||||||
|
const { classes } = props;
|
||||||
|
const { wifiBoxes } = state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={title}
|
||||||
|
open={data.open}
|
||||||
|
onRequestClose={closeDialog ? closeDialog : null}
|
||||||
|
contentStyle={{ maxWidth: '400px' }}
|
||||||
|
autoScrollBodyContent
|
||||||
|
actions={[
|
||||||
|
closeDialog && <FlatButton
|
||||||
|
label="Close"
|
||||||
|
onTouchTap={closeDialog}
|
||||||
|
/>,
|
||||||
|
removeActivePrinter && <FlatButton
|
||||||
|
label="Remove Printer"
|
||||||
|
onTouchTap={removeActivePrinter}
|
||||||
|
/>,
|
||||||
|
<RaisedButton
|
||||||
|
label={submitText}
|
||||||
|
primary
|
||||||
|
onTouchTap={save}
|
||||||
|
/>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SelectField name={`${form}.printer`} floatingLabelText="Printer" fullWidth>
|
||||||
|
{Object.entries(printerSettings).map(([value, { title }]) => (
|
||||||
|
<MenuItem key={value} value={value} primaryText={title} />
|
||||||
|
))}
|
||||||
|
</SelectField>
|
||||||
|
<TextField name={`${form}.name`} floatingLabelText="Name" fullWidth />
|
||||||
|
{(data.printer === 'doodle3d_printer') ?
|
||||||
|
<TextField name={`${form}.ip`} floatingLabelText="IP Adress" fullWidth /> :
|
||||||
|
<div className={classes.textFieldRow}>
|
||||||
|
<SelectField name={`${form}.ip`} floatingLabelText="Doodle3D WiFi-Box" fullWidth>
|
||||||
|
{wifiBoxes.map(({ localip, id, wifiboxid }) => (<MenuItem key={id} value={localip} primaryText={wifiboxid} />))}
|
||||||
|
</SelectField>
|
||||||
|
{data.ip && <ExitToAppIcon
|
||||||
|
onTouchTap={() => window.open(`${CONNECT_URL}/?uuid=0#control?localip=${data.ip}`, '_blank')}
|
||||||
|
style={{ fill: grey800, marginLeft: '10px', cursor: 'pointer' }}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{data.error && <p className={classes.error}>{data.error}</p>}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default injectSheet(styles)(Settings);
|
export default injectSheet(styles)(Settings);
|
||||||
|
59
src/interface/WifiBoxControl.js
Normal file
59
src/interface/WifiBoxControl.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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));
|
@ -1,10 +1,8 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Quaternion } from 'three/src/math/Quaternion.js';
|
|
||||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
|
||||||
import { Mesh } from 'three/src/objects/Mesh.js';
|
|
||||||
import PropTypes from 'proptypes';
|
import PropTypes from 'proptypes';
|
||||||
import { placeOnGround, createScene, fetchProgress, 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 FlatButton from 'material-ui/FlatButton';
|
||||||
@ -16,11 +14,15 @@ 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 defaultSettings from '../settings/default.yml';
|
// import MalyanControl from './MalyanControl.js';
|
||||||
import printerSettings from '../settings/printer.yml';
|
// import WifiBoxControl from './WifiBoxControl.js';
|
||||||
import materialSettings from '../settings/material.yml';
|
|
||||||
import qualitySettings from '../settings/quality.yml';
|
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import JSONToSketchData from 'doodle3d-core/shape/JSONToSketchData';
|
||||||
|
import createSceneData from 'doodle3d-core/d3/createSceneData.js';
|
||||||
|
import { generateExportMesh } from 'doodle3d-core/utils/exportUtils.js';
|
||||||
|
import muiThemeable from 'material-ui/styles/muiThemeable';
|
||||||
|
import Dialog from 'material-ui/Dialog';
|
||||||
|
import logo from '../../img/logo.png';
|
||||||
|
|
||||||
const MAX_FULLSCREEN_WIDTH = 720;
|
const MAX_FULLSCREEN_WIDTH = 720;
|
||||||
|
|
||||||
@ -49,15 +51,22 @@ const styles = {
|
|||||||
settingsBar: {
|
settingsBar: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
maxWidth: '380px',
|
maxWidth: '320px',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
padding: '10px',
|
padding: '10px 20px',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderLeft: `1px solid ${grey300}`
|
borderLeft: `1px solid ${grey300}`
|
||||||
},
|
},
|
||||||
sliceActions: {
|
sliceActions: {
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
|
sliceInfo: {
|
||||||
|
margin: '10px 0',
|
||||||
|
'& p': {
|
||||||
|
marginBottom: '5px',
|
||||||
|
fontSize: '11px'
|
||||||
|
}
|
||||||
|
},
|
||||||
sliceButtons: {
|
sliceButtons: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
display: 'flex'
|
display: 'flex'
|
||||||
@ -66,7 +75,7 @@ const styles = {
|
|||||||
margin: '5px 0 5px 5px'
|
margin: '5px 0 5px 5px'
|
||||||
},
|
},
|
||||||
controlButton: {
|
controlButton: {
|
||||||
marginRight: '2px'
|
marginRight: '5px'
|
||||||
},
|
},
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -76,77 +85,112 @@ const styles = {
|
|||||||
color: red500
|
color: red500
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
position: 'absolute'
|
userSelect: 'none',
|
||||||
|
position: 'absolute',
|
||||||
|
left: '10px'
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
userSelect: 'none',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '10px'
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: '20px',
|
||||||
|
top: '20px',
|
||||||
|
width: '150px',
|
||||||
|
height: '51px'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Interface extends React.Component {
|
class Interface extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mesh: PropTypes.shape({ isMesh: PropTypes.oneOf([true]) }).isRequired,
|
fileUrl: PropTypes.string,
|
||||||
|
selectedPrinter: PropTypes.string,
|
||||||
|
mesh: PropTypes.shape({ isMesh: PropTypes.oneOf([true]) }),
|
||||||
classes: PropTypes.objectOf(PropTypes.string),
|
classes: PropTypes.objectOf(PropTypes.string),
|
||||||
defaultSettings: PropTypes.object.isRequired,
|
|
||||||
printers: PropTypes.object.isRequired,
|
|
||||||
defaultPrinter: PropTypes.string,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
defaultQuality: PropTypes.string.isRequired,
|
|
||||||
material: PropTypes.object.isRequired,
|
|
||||||
defaultMaterial: PropTypes.string.isRequired,
|
|
||||||
pixelRatio: PropTypes.number.isRequired,
|
pixelRatio: PropTypes.number.isRequired,
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired,
|
||||||
|
muiTheme: PropTypes.object.isRequired,
|
||||||
|
allowDragDrop: PropTypes.bool.isRequired,
|
||||||
|
actions: PropTypes.arrayOf(PropTypes.shape({ target: PropTypes.string }))
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
defaultSettings: defaultSettings,
|
actions: [{
|
||||||
printers: printerSettings,
|
target: 'WIFI_PRINT',
|
||||||
quality: qualitySettings,
|
title: 'Print over WiFi'
|
||||||
defaultQuality: 'medium',
|
}, {
|
||||||
material: materialSettings,
|
target: 'DOWNLOAD',
|
||||||
defaultMaterial: 'pla',
|
title: 'Download GCode'
|
||||||
|
}],
|
||||||
pixelRatio: 1,
|
pixelRatio: 1,
|
||||||
name: 'Doodle3D'
|
name: 'Doodle3D',
|
||||||
|
allowDragDrop: true
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { defaultPrinter, defaultQuality, defaultMaterial, printers, quality, material, defaultSettings } = props;
|
|
||||||
|
const scene = createScene(this.props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showFullScreen: false,
|
scene,
|
||||||
|
settings: null,
|
||||||
|
showFullScreen: window.innerWidth > MAX_FULLSCREEN_WIDTH,
|
||||||
isSlicing: false,
|
isSlicing: false,
|
||||||
error: null,
|
error: null,
|
||||||
printers: defaultPrinter,
|
mesh: null,
|
||||||
quality: defaultQuality,
|
objectDimensions: '0x0x0mm',
|
||||||
material: defaultMaterial,
|
popover: { open: false, element: null }
|
||||||
popover: {
|
|
||||||
element: null,
|
|
||||||
open: false
|
|
||||||
},
|
|
||||||
settings: _.merge(
|
|
||||||
{},
|
|
||||||
defaultSettings,
|
|
||||||
printers[defaultPrinter],
|
|
||||||
quality[defaultQuality],
|
|
||||||
material[defaultMaterial]
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { canvas } = this.refs;
|
const { canvas } = this.refs;
|
||||||
|
const { scene } = this.state;
|
||||||
|
scene.updateCanvas(canvas);
|
||||||
|
|
||||||
const scene = createScene(canvas, this.props, this.state);
|
const { mesh, fileUrl } = this.props;
|
||||||
this.setState({ ...scene });
|
if (mesh) {
|
||||||
|
this.updateMesh(mesh, scene);
|
||||||
|
} else if (fileUrl) {
|
||||||
|
this.loadFile(fileUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFile = (fileUrl) => {
|
||||||
|
const { origin, pathname, password, username, port } = new URL(fileUrl);
|
||||||
|
const headers = {};
|
||||||
|
if (password && username) headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
|
||||||
|
|
||||||
|
fetch(`${origin}${port}${pathname}`, { headers })
|
||||||
|
.then(resonse => resonse.json())
|
||||||
|
.then(json => JSONToSketchData(json))
|
||||||
|
.then(file => createSceneData(file))
|
||||||
|
.then(sketch => generateExportMesh(sketch, { offsetSingleWalls: false, matrix: new THREE.Matrix4() }))
|
||||||
|
.then(mesh => this.updateMesh(mesh));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMesh(mesh, scene = this.state.scene) {
|
||||||
|
scene.mesh.geometry = mesh.geometry;
|
||||||
|
centerGeometry(scene.mesh);
|
||||||
|
placeOnGround(scene.mesh);
|
||||||
|
this.calculateDimensions();
|
||||||
|
scene.render();
|
||||||
|
|
||||||
|
this.setState({ mesh });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const { editorControls, mesh: { material }, renderer } = this.state;
|
const { scene: { editorControls, mesh: { material }, renderer } } = this.state;
|
||||||
editorControls.dispose();
|
editorControls.dispose();
|
||||||
material.dispose();
|
material.dispose();
|
||||||
renderer.dispose();
|
renderer.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetMesh = () => {
|
resetMesh = () => {
|
||||||
const { mesh, render, isSlicing } = this.state;
|
const { scene: { mesh, render }, isSlicing } = this.state;
|
||||||
if (isSlicing) return;
|
if (isSlicing) return;
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
mesh.position.set(0, 0, 0);
|
mesh.position.set(0, 0, 0);
|
||||||
@ -154,6 +198,7 @@ class Interface extends React.Component {
|
|||||||
mesh.rotation.set(0, 0, 0);
|
mesh.rotation.set(0, 0, 0);
|
||||||
mesh.updateMatrix();
|
mesh.updateMatrix();
|
||||||
placeOnGround(mesh);
|
placeOnGround(mesh);
|
||||||
|
this.calculateDimensions();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -161,46 +206,58 @@ class Interface extends React.Component {
|
|||||||
scaleUp = () => this.scaleMesh(0.9);
|
scaleUp = () => this.scaleMesh(0.9);
|
||||||
scaleDown = () => this.scaleMesh(1.0 / 0.9);
|
scaleDown = () => this.scaleMesh(1.0 / 0.9);
|
||||||
scaleMesh = (factor) => {
|
scaleMesh = (factor) => {
|
||||||
const { mesh, render, isSlicing } = this.state;
|
const { scene: { mesh, render }, isSlicing } = this.state;
|
||||||
if (isSlicing) return;
|
if (isSlicing) return;
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
mesh.scale.multiplyScalar(factor);
|
mesh.scale.multiplyScalar(factor);
|
||||||
mesh.updateMatrix();
|
mesh.updateMatrix();
|
||||||
placeOnGround(mesh);
|
placeOnGround(mesh);
|
||||||
|
this.calculateDimensions();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
rotateX = () => this.rotate(new Vector3(0, 0, 1), Math.PI / 2.0);
|
rotateX = () => this.rotate(new THREE.Vector3(0, 0, 1), Math.PI / 2.0);
|
||||||
rotateY = () => this.rotate(new Vector3(1, 0, 0), Math.PI / 2.0);
|
rotateY = () => this.rotate(new THREE.Vector3(1, 0, 0), Math.PI / 2.0);
|
||||||
rotateZ = () => this.rotate(new Vector3(0, 1, 0), Math.PI / 2.0);
|
rotateZ = () => this.rotate(new THREE.Vector3(0, 1, 0), Math.PI / 2.0);
|
||||||
rotate = (axis, angle) => {
|
rotate = (axis, angle) => {
|
||||||
const { mesh, render, isSlicing } = this.state;
|
const { scene: { mesh, render }, isSlicing } = this.state;
|
||||||
if (isSlicing) return;
|
if (isSlicing) return;
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
mesh.rotateOnWorldAxis(axis, angle);
|
mesh.rotateOnWorldAxis(axis, angle);
|
||||||
placeOnGround(mesh);
|
placeOnGround(mesh);
|
||||||
|
this.calculateDimensions();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
slice = async (target) => {
|
slice = async (action) => {
|
||||||
const { isSlicing, settings, printers, quality, material, mesh: { matrix } } = this.state;
|
const { isSlicing, settings, mesh, scene: { material, mesh: { matrix } } } = this.state;
|
||||||
const { name, mesh } = this.props;
|
const { name } = this.props;
|
||||||
|
|
||||||
if (isSlicing) return;
|
if (isSlicing) return;
|
||||||
|
if (!settings) {
|
||||||
|
this.setState({ error: 'please select a printer first' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.target === 'WIFI_PRINT' && !settings.ip) {
|
||||||
|
this.setState({ error: 'please connect to a WiFi enabled printer' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mesh) {
|
||||||
|
this.setState({ error: 'there is no file to slice' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.closePopover();
|
this.closePopover();
|
||||||
|
|
||||||
this.setState({ isSlicing: true, progress: { action: '', percentage: 0, step: 0 }, error: null });
|
this.setState({ isSlicing: true, progress: { action: '', percentage: 0, step: 0 }, error: null });
|
||||||
|
|
||||||
const exportMesh = new Mesh(mesh.geometry, mesh.material);
|
const exportMesh = new THREE.Mesh(mesh.geometry, mesh.material);
|
||||||
exportMesh.applyMatrix(matrix);
|
exportMesh.applyMatrix(matrix);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await slice(target, name, exportMesh, settings, printers, quality, material, progress => {
|
const updateProgres = progress => this.setState({ progress: { ...this.state.progress, ...progress } });
|
||||||
this.setState({ progress: { ...this.state.progress, ...progress } });
|
await slice(action, name, exportMesh, settings, updateProgres);
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ error: error.message });
|
this.setState({ error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
@ -228,31 +285,15 @@ class Interface extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeSettings = (settings) => {
|
|
||||||
this.setState(settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUpdate(nextProps, nextState) {
|
|
||||||
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);
|
|
||||||
box.updateMatrix();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) render();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const { updateCanvas } = this.state;
|
const { scene: { updateCanvas } } = this.state;
|
||||||
const { canvas } = this.refs;
|
const { canvas } = this.refs;
|
||||||
if (updateCanvas && canvas) updateCanvas(canvas);
|
if (updateCanvas && canvas) updateCanvas(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize3dView = (width, height) => {
|
onResize3dView = (width, height) => {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const { setSize } = this.state;
|
const { scene: { setSize } } = this.state;
|
||||||
const { pixelRatio } = this.props;
|
const { pixelRatio } = this.props;
|
||||||
if (setSize) setSize(width, height, pixelRatio);
|
if (setSize) setSize(width, height, pixelRatio);
|
||||||
});
|
});
|
||||||
@ -262,35 +303,82 @@ class Interface extends React.Component {
|
|||||||
this.setState({ showFullScreen: width > MAX_FULLSCREEN_WIDTH });
|
this.setState({ showFullScreen: width > MAX_FULLSCREEN_WIDTH });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onChangeSettings = (settings) => {
|
||||||
|
const { scene: { box, render } } = this.state;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
if (!this.state.settings || this.state.settings.dimensions !== settings.dimensions) {
|
||||||
|
box.scale.set(settings.dimensions.y, settings.dimensions.z, settings.dimensions.x);
|
||||||
|
box.updateMatrix();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) render();
|
||||||
|
|
||||||
|
this.setState({ settings, error: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateDimensions = () => {
|
||||||
|
const { scene: { mesh } } = this.state;
|
||||||
|
const { x, y, z } = new THREE.Box3().setFromObject(mesh).getSize();
|
||||||
|
this.setState({ objectDimensions: `${Math.round(y)}x${Math.round(z)}x${Math.round(x)}mm` });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDrop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this.props.allowDragDrop) return;
|
||||||
|
|
||||||
|
for (const file of event.dataTransfer.files) {
|
||||||
|
const extentions = file.name.split('.').pop();
|
||||||
|
|
||||||
|
switch (extentions.toUpperCase()) {
|
||||||
|
case 'D3SKETCH':
|
||||||
|
this.loadFile(URL.createObjectURL(file));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, defaultPrinter, defaultQuality, defaultMaterial, onCancel } = this.props;
|
const { classes, onCancel, selectedPrinter, actions } = this.props;
|
||||||
const { isSlicing, progress, settings, printers, quality, material, showFullScreen, error } = this.state;
|
const { isSlicing, progress, showFullScreen, error, objectDimensions, settings } = this.state;
|
||||||
|
|
||||||
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
|
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
|
||||||
|
|
||||||
const settingsPanel = (
|
const settingsPanel = (
|
||||||
<div className={classes.settingsBar} style={style}>
|
<div className={classes.settingsBar} style={style}>
|
||||||
<Settings
|
<Settings
|
||||||
|
selectedPrinter={selectedPrinter}
|
||||||
disabled={isSlicing}
|
disabled={isSlicing}
|
||||||
printers={printerSettings}
|
|
||||||
defaultPrinter={defaultPrinter}
|
|
||||||
quality={qualitySettings}
|
|
||||||
defaultQuality={defaultQuality}
|
|
||||||
material={materialSettings}
|
|
||||||
defaultMaterial={defaultMaterial}
|
|
||||||
initialSettings={settings}
|
|
||||||
onChange={this.onChangeSettings}
|
onChange={this.onChangeSettings}
|
||||||
/>
|
/>
|
||||||
<div className={classes.sliceActions}>
|
<div className={classes.sliceActions}>
|
||||||
|
<div className={classes.sliceInfo}>
|
||||||
{error && <p className={classes.error}>{error}</p>}
|
{error && <p className={classes.error}>{error}</p>}
|
||||||
{isSlicing && <p>{progress.action}</p>}
|
{isSlicing && <p>{progress.action}</p>}
|
||||||
{isSlicing && <LinearProgress mode="determinate" value={progress.percentage * 100.0} />}
|
{isSlicing && <LinearProgress mode="determinate" value={progress.percentage * 100.0} />}
|
||||||
|
</div>
|
||||||
<div className={classes.sliceButtons}>
|
<div className={classes.sliceButtons}>
|
||||||
{onCancel && <RaisedButton
|
{onCancel && <RaisedButton
|
||||||
label="Cancel"
|
label="Close"
|
||||||
className={`${classes.button}`}
|
className={`${classes.button}`}
|
||||||
onTouchTap={onCancel}
|
onTouchTap={onCancel}
|
||||||
/>}
|
/>}
|
||||||
|
{/* (settings && settings.ip) && ((settings.printer === 'doodle3d_printer') ?
|
||||||
|
<MalyanControl ip={settings.ip} /> :
|
||||||
|
<WifiBoxControl ip={settings.ip} />
|
||||||
|
) */}
|
||||||
|
{actions.length === 1 ? (
|
||||||
|
<RaisedButton
|
||||||
|
primary
|
||||||
|
label={actions[0].title}
|
||||||
|
onTouchTap={() => this.slice(actions[0])}
|
||||||
|
className={`${classes.button}`}
|
||||||
|
disabled={isSlicing}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
<RaisedButton
|
<RaisedButton
|
||||||
label="Print"
|
label="Print"
|
||||||
ref="button"
|
ref="button"
|
||||||
@ -307,10 +395,13 @@ class Interface extends React.Component {
|
|||||||
onRequestClose={this.closePopover}
|
onRequestClose={this.closePopover}
|
||||||
>
|
>
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem primaryText="Send over WiFi" onTouchTap={() => this.slice('WIFI')} />
|
{actions.map((action) => (
|
||||||
<MenuItem primaryText="Download GCode" onTouchTap={() => this.slice('DOWNLOAD')} />
|
<MenuItem key={action.target} primaryText={action.title} onTouchTap={() => this.slice(action)} />
|
||||||
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +412,9 @@ class Interface extends React.Component {
|
|||||||
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
|
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
|
||||||
<canvas className={classes.canvas} ref="canvas" />
|
<canvas className={classes.canvas} ref="canvas" />
|
||||||
<div className={classes.controlBar}>
|
<div className={classes.controlBar}>
|
||||||
|
<div className={classes.detail}>
|
||||||
|
<p>Dimensions: {objectDimensions}</p>
|
||||||
|
</div>
|
||||||
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" />
|
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" />
|
||||||
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" />
|
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" />
|
||||||
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" />
|
<RaisedButton disabled={isSlicing} className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" />
|
||||||
@ -333,16 +427,32 @@ class Interface extends React.Component {
|
|||||||
|
|
||||||
if (showFullScreen) {
|
if (showFullScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={classes.container}>
|
<div
|
||||||
|
className={classes.container}
|
||||||
|
ref={(container) => {
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('dragover', event => event.preventDefault());
|
||||||
|
container.addEventListener('drop', this.onDrop);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
|
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
|
||||||
<h1 className={classes.title}>Print</h1>
|
<img src={logo} className={classes.logo} />
|
||||||
{d3Panel}
|
{d3Panel}
|
||||||
{settingsPanel}
|
{settingsPanel}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={classes.container}>
|
<div
|
||||||
|
className={classes.container}
|
||||||
|
ref={(container) => {
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('dragover', event => event.preventDefault());
|
||||||
|
container.addEventListener('drop', this.onDrop);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
|
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
|
||||||
<Tabs
|
<Tabs
|
||||||
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
|
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
@ -364,4 +474,4 @@ class Interface extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectSheet(styles)(Interface);
|
export default muiThemeable()(injectSheet(styles)(Interface));
|
||||||
|
@ -1,67 +1,58 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { Box3 } from 'three/src/math/Box3.js';
|
|
||||||
import { Matrix4 } from 'three/src/math/Matrix4.js';
|
|
||||||
import { Scene } from 'three/src/scenes/Scene.js';
|
|
||||||
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
|
|
||||||
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
|
|
||||||
import { DirectionalLight } from 'three/src/lights/DirectionalLight.js';
|
|
||||||
import { MeshPhongMaterial } from 'three/src/materials/MeshPhongMaterial.js';
|
|
||||||
import { BoxGeometry } from 'three/src/geometries/BoxGeometry.js';
|
|
||||||
import { Mesh } from 'three/src/objects/Mesh.js';
|
|
||||||
import { BoxHelper } from 'three/src/helpers/BoxHelper.js';
|
|
||||||
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
|
|
||||||
import { DoubleSide } from 'three/src/constants.js';
|
|
||||||
import 'three/examples/js/controls/EditorControls';
|
import 'three/examples/js/controls/EditorControls';
|
||||||
import printerSettings from '../settings/printer.yml';
|
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';
|
||||||
|
import { Doodle3DBox } from 'doodle3d-api';
|
||||||
|
|
||||||
export function placeOnGround(mesh) {
|
export function placeOnGround(mesh) {
|
||||||
const boundingBox = new Box3().setFromObject(mesh);
|
const boundingBox = new THREE.Box3().setFromObject(mesh);
|
||||||
|
|
||||||
mesh.position.y -= boundingBox.min.y;
|
mesh.position.y -= boundingBox.min.y;
|
||||||
mesh.updateMatrix();
|
mesh.updateMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createScene(canvas, props, state) {
|
export function centerGeometry(mesh) {
|
||||||
const { pixelRatio, mesh: { geometry } } = props;
|
|
||||||
const { settings } = state;
|
|
||||||
|
|
||||||
// center geometry
|
// center geometry
|
||||||
geometry.computeBoundingBox();
|
mesh.geometry.computeBoundingBox();
|
||||||
const center = geometry.boundingBox.getCenter();
|
const center = mesh.geometry.boundingBox.getCenter();
|
||||||
geometry.applyMatrix(new Matrix4().makeTranslation(-center.x, -center.y, -center.z));
|
mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
|
||||||
|
}
|
||||||
|
|
||||||
const scene = new Scene();
|
export function createScene({ pixelRatio, muiTheme }) {
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
const camera = new 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);
|
||||||
|
camera.lookAt(new THREE.Vector3(0, 0, 0));
|
||||||
|
|
||||||
const directionalLightA = new DirectionalLight(0xa2a2a2);
|
const directionalLightA = new THREE.DirectionalLight(0xa2a2a2);
|
||||||
directionalLightA.position.set(1, 1, 1);
|
directionalLightA.position.set(1, 1, 1);
|
||||||
scene.add(directionalLightA);
|
scene.add(directionalLightA);
|
||||||
|
|
||||||
const directionalLightB = new DirectionalLight(0xa2a2a2);
|
const directionalLightB = new THREE.DirectionalLight(0xa2a2a2);
|
||||||
directionalLightB.position.set(-1, 1, -1);
|
directionalLightB.position.set(-1, 1, -1);
|
||||||
scene.add(directionalLightB);
|
scene.add(directionalLightB);
|
||||||
|
|
||||||
const light = new AmbientLight(0x656565);
|
const light = new THREE.AmbientLight(0x656565);
|
||||||
scene.add(light);
|
scene.add(light);
|
||||||
|
|
||||||
const material = new MeshPhongMaterial({ color: 0x2194ce, side: DoubleSide, specular: 0xc5c5c5, shininess: 5 });
|
const material = new THREE.MeshPhongMaterial({ color: muiTheme.palette.primary2Color, side: THREE.DoubleSide, specular: 0xc5c5c5, shininess: 5, flatShading: false });
|
||||||
const mesh = new Mesh(geometry, material);
|
const mesh = new THREE.Mesh(new THREE.Geometry(), material);
|
||||||
placeOnGround(mesh);
|
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
|
|
||||||
const box = new BoxHelper(new Mesh(new BoxGeometry(1, 1, 1).applyMatrix(new Matrix4().makeTranslation(0, 0.5, 0))), 0x72bcd4);
|
const box = new THREE.BoxHelper(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1).applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5, 0))), muiTheme.palette.primary2Color);
|
||||||
scene.add(box);
|
scene.add(box);
|
||||||
|
|
||||||
const { dimensions } = settings;
|
let renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
||||||
box.scale.set(dimensions.y, dimensions.z, dimensions.x);
|
let editorControls = new THREE.EditorControls(camera, renderer.domElement);
|
||||||
|
|
||||||
|
box.scale.set(1., 1., 1.);
|
||||||
box.updateMatrix();
|
box.updateMatrix();
|
||||||
|
|
||||||
const render = () => renderer.render(scene, camera);
|
const render = () => renderer.render(scene, camera);
|
||||||
@ -74,64 +65,111 @@ export function createScene(canvas, props, state) {
|
|||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
||||||
let editorControls;
|
|
||||||
let renderer;
|
|
||||||
const updateCanvas = (canvas) => {
|
const updateCanvas = (canvas) => {
|
||||||
if (!renderer || renderer.domElement !== canvas) {
|
if (!renderer || renderer.domElement !== canvas) {
|
||||||
if (renderer) renderer.dispose();
|
if (renderer) renderer.dispose();
|
||||||
renderer = new WebGLRenderer({ canvas, alpha: true, antialias: true });
|
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
|
||||||
renderer.setClearColor(0xffffff, 0);
|
renderer.setClearColor(0xffffff, 0);
|
||||||
}
|
}
|
||||||
if (!editorControls || editorControls.domElement !== canvas) {
|
if (!editorControls || editorControls.domElement !== canvas) {
|
||||||
if (editorControls) editorControls.dispose();
|
if (editorControls) editorControls.dispose();
|
||||||
editorControls = new THREE.EditorControls(camera, canvas);
|
editorControls = new THREE.EditorControls(camera, canvas);
|
||||||
editorControls.focus(mesh);
|
|
||||||
editorControls.addEventListener('change', render);
|
editorControls.addEventListener('change', render);
|
||||||
}
|
}
|
||||||
|
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
updateCanvas(canvas);
|
|
||||||
|
|
||||||
const focus = () => editorControls.focus(mesh);
|
const focus = () => editorControls.focus(mesh);
|
||||||
|
|
||||||
return { editorControls, scene, mesh, camera, renderer, render, box, setSize, updateCanvas, focus };
|
return { editorControls, scene, mesh, camera, renderer, render, box, setSize, updateCanvas, focus };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchProgress(url, { method = 'get', headers = {}, body = {} } = {}, onProgress) {
|
export function fetchProgress(url, data = {}, onProgress) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = new Request(url, data);
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open(method, url);
|
|
||||||
if (headers) {
|
xhr.onload = () => {
|
||||||
for (const key in headers) {
|
resolve(new Response(xhr.response));
|
||||||
const header = headers[key];
|
// const headers = new Headers(xhr.getAllResponseHeaders() || '');
|
||||||
xhr.setRequestHeader(key, header);
|
// const { status, statusText, response, responseText, responseURL: url = headers.get('X-Request-URL') } = xhr;
|
||||||
|
// resolve(new Response(response || responseText, { headers, status, statusText, url }));
|
||||||
}
|
}
|
||||||
|
xhr.onerror = () => reject(new TypeError('Network request failed'));
|
||||||
|
xhr.ontimeout = () => reject(new TypeError('Network request failed'));
|
||||||
|
|
||||||
|
xhr.open(request.method, url, true);
|
||||||
|
|
||||||
|
if (request.credentials === 'include') {
|
||||||
|
xhr.withCredentials = true
|
||||||
|
} else if (request.credentials === 'omit') {
|
||||||
|
xhr.withCredentials = false
|
||||||
}
|
}
|
||||||
xhr.onload = event => resolve(event.target.responseText);
|
|
||||||
xhr.onerror = reject;
|
|
||||||
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress;
|
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress;
|
||||||
xhr.send(body);
|
if (xhr.responseType) xhr.responseType = 'blob';
|
||||||
|
|
||||||
|
// request.headers.forEach((value, name) => xhr.setRequestHeader(name, value));
|
||||||
|
|
||||||
|
xhr.send(data.body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMalyanStatus(ip) {
|
||||||
|
return fetch(`http://${ip}/inquiry`, { method: 'GET' })
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(statusText => {
|
||||||
|
const [nozzleTemperature, nozzleTargetTemperature, bedTemperature, bedTargetTemperature, progress] = statusText.match(/\d+/g);
|
||||||
|
const status = { nozzleTemperature, nozzleTargetTemperature, bedTemperature, bedTargetTemperature, progress };
|
||||||
|
|
||||||
|
switch (statusText.charAt(statusText.length - 1)) {
|
||||||
|
case 'I':
|
||||||
|
status.state = 'idle';
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
status.state = 'printing';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status.state = 'unknown';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(time) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, time));
|
||||||
|
}
|
||||||
|
|
||||||
const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com';
|
const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com';
|
||||||
const CONNECT_URL = 'http://connect.doodle3d.com/';
|
|
||||||
|
|
||||||
export async function slice(target, name, mesh, settings, printers, quality, material, updateProgress) {
|
|
||||||
if (!printers) throw new Error('Please select a printer');
|
|
||||||
|
|
||||||
|
export async function slice(action, name, mesh, settings, updateProgress) {
|
||||||
let steps;
|
let steps;
|
||||||
let currentStep = 0;
|
let currentStep = 0;
|
||||||
switch (target) {
|
let wifiBox;
|
||||||
|
switch (action.target) {
|
||||||
case 'DOWNLOAD':
|
case 'DOWNLOAD':
|
||||||
steps = 1;
|
steps = 1;
|
||||||
break;
|
break;
|
||||||
case 'WIFI':
|
case 'WIFI_PRINT':
|
||||||
|
if (settings.printer === 'doodle3d_printer') {
|
||||||
|
const { state } = await getMalyanStatus(settings.ip);
|
||||||
|
if (state !== 'idle') throw { message: 'printer is busy', code: 0 };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
wifiBox = new Doodle3DBox(settings.ip);
|
||||||
|
if (!await wifiBox.checkAlive()) throw { message: `can't connect to printer`, code: 4 }
|
||||||
|
|
||||||
|
const { state } = await wifiBox.info.status();
|
||||||
|
if (state !== 'idle') throw { message: 'printer is busy', code: 0 };
|
||||||
|
}
|
||||||
|
steps = 2;
|
||||||
|
break;
|
||||||
|
case 'CUSTOM_UPLOAD':
|
||||||
steps = 2;
|
steps = 2;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
steps = 1;
|
throw { message: 'unknown target', code: 1 };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,64 +177,99 @@ export async function slice(target, name, mesh, settings, printers, quality, mat
|
|||||||
const centerX = dimensions.x / 2;
|
const centerX = dimensions.x / 2;
|
||||||
const centerY = dimensions.y / 2;
|
const centerY = dimensions.y / 2;
|
||||||
|
|
||||||
const matrix = new Matrix4().makeTranslation(centerY, 0, centerX).multiply(mesh.matrix);
|
const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX)
|
||||||
const { gcode } = await sliceGeometry(settings, mesh.geometry, mesh.material, matrix, false, false, ({ progress }) => {
|
.multiply(new THREE.Matrix4().makeRotationY(-Math.PI / 2.0))
|
||||||
|
.multiply(mesh.matrix);
|
||||||
|
|
||||||
|
const { gcode } = await sliceGeometry({
|
||||||
|
...settings,
|
||||||
|
name: `${name}.gcode`,
|
||||||
|
printer: { type: settings.printers, title: printerSettings[settings.printer].title },
|
||||||
|
material: { type: settings.material, title: materialSettings[settings.material].title },
|
||||||
|
quality: { type: settings.quality, title: qualitySettings[settings.quality].title }
|
||||||
|
}, mesh.geometry, mesh.material, matrix, false, false, ({ progress }) => {
|
||||||
updateProgress({
|
updateProgress({
|
||||||
action: progress.action,
|
action: progress.action,
|
||||||
percentage: currentStep / steps + progress.done / progress.total / steps
|
percentage: (currentStep + progress.done / progress.total) / steps
|
||||||
});
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
throw { message: `error during slicing: ${error.message}`, code: 2 };
|
||||||
});
|
});
|
||||||
currentStep ++;
|
currentStep ++;
|
||||||
|
|
||||||
switch (target) {
|
switch (action.target) {
|
||||||
case 'DOWNLOAD': {
|
case 'DOWNLOAD': {
|
||||||
const blob = new File([gcode], `${name}.gcode`, { type: 'text/plain;charset=utf-8' });
|
fileSaver.saveAs(gcode, `${name}.gcode`);
|
||||||
fileSaver.saveAs(blob);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'WIFI': {
|
case 'WIFI_PRINT': {
|
||||||
|
if (settings.printer === 'doodle3d_printer') {
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('file', gcode, 'doodle.gcode');
|
||||||
|
|
||||||
|
// because fetch has no way of retrieving progress we fake progress
|
||||||
|
let loaded = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loaded += 15 * 1024;
|
||||||
|
updateProgress({
|
||||||
|
action: 'Uploading to printer',
|
||||||
|
percentage: (currentStep + loaded / file.size) / steps
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// await fetchProgress(`http://${settings.ip}/set?code=M563 S4`, { method: 'GET' });
|
||||||
|
await fetch(`http://${settings.ip}/upload`, { method: 'POST', body, mode: 'no-cors' }, (progress) => {
|
||||||
|
updateProgress({
|
||||||
|
action: 'Uploading to printer',
|
||||||
|
percentage: (currentStep + progress.loaded / progress.total) / steps
|
||||||
|
});
|
||||||
|
});
|
||||||
|
clearInterval(interval);
|
||||||
|
await fetch(`http://${settings.ip}/set?code=M566 ${name}.gcode`, { method: 'GET', mode: 'no-cors' });
|
||||||
|
await fetch(`http://${settings.ip}/set?code=M565`, { method: 'GET', mode: 'no-cors' });
|
||||||
|
|
||||||
|
currentStep ++;
|
||||||
|
} else {
|
||||||
// upload G-code file to AWS S3
|
// upload G-code file to AWS S3
|
||||||
const { data: { reservation, id } } = await fetch(`${GCODE_SERVER_URL}/upload`, { method: 'POST' })
|
const { data: { reservation: { fields, url }, id } } = await fetch(`${GCODE_SERVER_URL}/upload`, { method: 'POST' })
|
||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
|
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
const { fields } = reservation;
|
|
||||||
for (const key in fields) {
|
for (const key in fields) {
|
||||||
body.append(key, fields[key]);
|
body.append(key, fields[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = ';' + JSON.stringify({
|
body.append('file', gcode, 'doodle.gcode');
|
||||||
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) => {
|
await fetchProgress(url, { method: 'POST', body }, progress => {
|
||||||
updateProgress({
|
updateProgress({
|
||||||
action: 'Uploading',
|
action: 'Uploading',
|
||||||
percentage: currentStep / steps + progess.loaded / progess.total / steps
|
percentage: (currentStep + progress.loaded / progress.total) / steps
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
currentStep ++;
|
currentStep ++;
|
||||||
|
|
||||||
const popup = window.open(`${CONNECT_URL}?uuid=${id}`, '_blank');
|
const result = await wifiBox.printer.fetch(id);
|
||||||
if (!popup) throw new Error('popup was blocked by browser');
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'CUSTOM_UPLOAD': {
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('file', gcode, 'doodle.gcode');
|
||||||
|
|
||||||
|
await fetchProgress(action.url, { method: 'POST', body }, progress => {
|
||||||
|
updateProgress({
|
||||||
|
action: 'Uploading',
|
||||||
|
percentage: (currentStep + progress.loaded / progress.total) / steps
|
||||||
|
});
|
||||||
|
});
|
||||||
|
currentStep ++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
throw { message: 'unknown target', code: 1 };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,30 @@
|
|||||||
zOffset: 0.3
|
startCode: |-
|
||||||
|
M109 S{temperature} ;set target temperature
|
||||||
|
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
|
||||||
|
G21 ;metric values
|
||||||
|
M107 ;start with the fan off
|
||||||
|
G28 X0 Y0 ;move X/Y to min endstops
|
||||||
|
G28 Z0 ;move Z to min endstops
|
||||||
|
G1 Z15 F9000 ;move the platform down 15mm
|
||||||
|
G92 E0 ;zero the extruded length
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 F200 E10 ;extrude 10mm of feed stock
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G1 F9000
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M117 Printing Doodle...
|
||||||
|
endCode: |-
|
||||||
|
M107 ;fan off
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
|
||||||
|
G1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
|
||||||
|
G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
|
||||||
|
M84 ;disable axes / steppers
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M104 S0
|
||||||
|
{if heatedBed}M140 S0
|
||||||
|
M117 Done
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 200
|
x: 200
|
||||||
y: 200
|
y: 200
|
||||||
@ -7,7 +33,7 @@ heatedBed: false
|
|||||||
nozzleDiameter: 0.4
|
nozzleDiameter: 0.4
|
||||||
filamentThickness: 2.85
|
filamentThickness: 2.85
|
||||||
temperature: 210
|
temperature: 210
|
||||||
bedTemperature: 70
|
bedTemperature: 50
|
||||||
layerHeight: 0.15
|
layerHeight: 0.15
|
||||||
combing: true
|
combing: true
|
||||||
thickness:
|
thickness:
|
||||||
@ -23,11 +49,10 @@ travel:
|
|||||||
speed: 200.0
|
speed: 200.0
|
||||||
support:
|
support:
|
||||||
enabled: false
|
enabled: false
|
||||||
acceptanceMargin: 1.5
|
minArea: 2
|
||||||
distanceY: 0.4
|
distanceY: 0.4
|
||||||
gridSize: 6.0
|
density: 5.0
|
||||||
margin: 2.0
|
margin: 2.0
|
||||||
plateSize: 4.0
|
|
||||||
flowRate: 0.8
|
flowRate: 0.8
|
||||||
speed: 40.0
|
speed: 40.0
|
||||||
innerShell:
|
innerShell:
|
||||||
@ -39,12 +64,12 @@ outerShell:
|
|||||||
innerInfill:
|
innerInfill:
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 80.0
|
speed: 80.0
|
||||||
gridSize: 15.0
|
density: 20.0
|
||||||
outerInfill:
|
outerInfill:
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 50.0
|
speed: 50.0
|
||||||
brim:
|
brim:
|
||||||
offset: 4.0
|
size: 8.0
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 40.0
|
speed: 40.0
|
||||||
firstLayer:
|
firstLayer:
|
||||||
|
21
src/settings/infill.yml
Normal file
21
src/settings/infill.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
0pct:
|
||||||
|
title: Hollow (0%)
|
||||||
|
innerInfill:
|
||||||
|
density: 0.0
|
||||||
|
10pct:
|
||||||
|
title: Light (10%)
|
||||||
|
innerInfill:
|
||||||
|
density: 10.0
|
||||||
|
20pct:
|
||||||
|
title: Normal (20%)
|
||||||
|
innerInfill:
|
||||||
|
density: 20.0
|
||||||
|
50pct:
|
||||||
|
title: Dense (50%)
|
||||||
|
innerInfill:
|
||||||
|
density: 50.0
|
||||||
|
100pct:
|
||||||
|
title: Solid (100%)
|
||||||
|
innerInfill:
|
||||||
|
density: 100.0
|
||||||
|
|
@ -1,3 +1,33 @@
|
|||||||
|
doodle3d_printer:
|
||||||
|
startCode: |-
|
||||||
|
M140 S{bedTemperature}
|
||||||
|
{if heatedBed}M104 S{temperature}
|
||||||
|
G28
|
||||||
|
M109 S{temperature}
|
||||||
|
{if heatedBed}M190 S{bedTemperature}
|
||||||
|
G90
|
||||||
|
M82
|
||||||
|
G1 Z10.0 F6000
|
||||||
|
G92 E0
|
||||||
|
G1 F200 E3
|
||||||
|
G92 E0
|
||||||
|
endCode: |-
|
||||||
|
M104 S0
|
||||||
|
M140 S0
|
||||||
|
G92 E1
|
||||||
|
G1 E-1 F300
|
||||||
|
G28 X0 Y0
|
||||||
|
M84
|
||||||
|
M82
|
||||||
|
M104 S0
|
||||||
|
title: Doodle3D Printer
|
||||||
|
heatedBed: true
|
||||||
|
bedTemperature: 50
|
||||||
|
filamentThickness: 1.75
|
||||||
|
dimensions:
|
||||||
|
x: 120
|
||||||
|
y: 120
|
||||||
|
z: 120
|
||||||
_3Dison_plus:
|
_3Dison_plus:
|
||||||
title: 3Dison plus
|
title: 3Dison plus
|
||||||
heatedBed: false
|
heatedBed: false
|
||||||
@ -267,11 +297,50 @@ ultimaker2_plus:
|
|||||||
title: Ultimaker 2+
|
title: Ultimaker 2+
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
filamentThickness: 2.85
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 223
|
||||||
|
y: 223
|
||||||
|
z: 205
|
||||||
|
ultimaker2_plus_extended:
|
||||||
|
title: Ultimaker 2+ Extended
|
||||||
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 223
|
x: 223
|
||||||
y: 223
|
y: 223
|
||||||
z: 305
|
z: 305
|
||||||
ultimaker2go:
|
ultimaker2go:
|
||||||
|
startCode: |-
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X8 Y28 SDoodle3D heat up...
|
||||||
|
M109 S{temperature} ;set target temperature
|
||||||
|
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
|
||||||
|
G21 ;metric values
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M107 ;start with the fan off
|
||||||
|
G28 ; home to endstops
|
||||||
|
G1 Z15 F9000 ;move the platform down 15mm
|
||||||
|
G92 E0 ;zero the extruded length
|
||||||
|
G1 F200 E10 ;extrude 10mm of feed stock
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G1 F9000
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X8 Y28 SDoodle3D printing...
|
||||||
|
endCode: |-
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X20 Y28 SDoodle3D done!
|
||||||
|
M107 ;fan off
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
|
||||||
|
G1 Z+5.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
|
||||||
|
G28 ;home the printer
|
||||||
|
M84 ;disable axes / steppers
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M104 S0
|
||||||
|
{if heatedBed}M140 S0
|
||||||
title: Ultimaker 2 Go
|
title: Ultimaker 2 Go
|
||||||
heatedBed: false
|
heatedBed: false
|
||||||
filamentThickness: 2.85
|
filamentThickness: 2.85
|
||||||
|
@ -15,7 +15,7 @@ low:
|
|||||||
speed: 70.0
|
speed: 70.0
|
||||||
innerInfill:
|
innerInfill:
|
||||||
speed: 80.0
|
speed: 80.0
|
||||||
gridSize: 25.0
|
density: 10.0
|
||||||
medium:
|
medium:
|
||||||
title: "Medium"
|
title: "Medium"
|
||||||
layerHeight: .15
|
layerHeight: .15
|
||||||
@ -33,7 +33,7 @@ medium:
|
|||||||
speed: 40.0
|
speed: 40.0
|
||||||
innerInfill:
|
innerInfill:
|
||||||
speed: 80.0
|
speed: 80.0
|
||||||
gridSize: 25.0
|
density: 10.0
|
||||||
high:
|
high:
|
||||||
title: "High"
|
title: "High"
|
||||||
thickness:
|
thickness:
|
||||||
@ -51,4 +51,4 @@ high:
|
|||||||
speed: 30.0
|
speed: 30.0
|
||||||
innerInfill:
|
innerInfill:
|
||||||
speed: 70.0
|
speed: 70.0
|
||||||
gridSize: 10.0
|
density: 20.0
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION } from '../constants.js';
|
||||||
|
|
||||||
const offsetOptions = {
|
const OFFSET_OPTIONS = {
|
||||||
jointType: 'jtRound',
|
jointType: 'jtRound',
|
||||||
miterLimit: 2.0,
|
miterLimit: 2.0,
|
||||||
roundPrecision: 0.25
|
roundPrecision: 0.25,
|
||||||
|
endType: 'etClosedPolygon'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function addBrim(slices, settings) {
|
export default function addBrim(slices, settings) {
|
||||||
let {
|
let {
|
||||||
brim: { offset: brimOffset }
|
brim: { size: brimSize },
|
||||||
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
brimOffset /= PRECISION;
|
|
||||||
|
nozzleDiameter /= PRECISION;
|
||||||
|
brimSize /= PRECISION;
|
||||||
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
|
|
||||||
const [firstLayer] = slices;
|
const [firstLayer] = slices;
|
||||||
|
|
||||||
firstLayer.brim = firstLayer.parts.reduce((brim, { shape }) => (
|
const brim = firstLayer.parts.reduce((brim, { shape }) => (
|
||||||
brim.join(shape.offset(brimOffset, {
|
brim.join(shape.offset(nozzleRadius, {
|
||||||
...offsetOptions,
|
...OFFSET_OPTIONS,
|
||||||
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
||||||
}))
|
}))
|
||||||
), new Shape([], true)).simplify('pftNonZero');
|
), new Shape([], true)).simplify('pftNonZero');
|
||||||
|
|
||||||
|
firstLayer.brim = new Shape([], true);
|
||||||
|
|
||||||
|
for (let offset = 0; offset < brimSize; offset += nozzleDiameter) {
|
||||||
|
const brimPart = brim.offset(offset, OFFSET_OPTIONS);
|
||||||
|
firstLayer.brim = firstLayer.brim.join(brimPart);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { devide } from './helpers/VectorUtils.js';
|
import { divide } from './helpers/vector2.js';
|
||||||
import { PRECISION } from '../constants.js'
|
import { PRECISION } from '../constants.js'
|
||||||
|
|
||||||
export default function applyPrecision(layers) {
|
export default function applyPrecision(layers) {
|
||||||
@ -16,7 +16,7 @@ function scaleUpShape(shape) {
|
|||||||
const path = shape[i];
|
const path = shape[i];
|
||||||
|
|
||||||
for (let i = 0; i < path.length; i ++) {
|
for (let i = 0; i < path.length; i ++) {
|
||||||
path[i] = devide(path[i], PRECISION);
|
path[i] = divide(path[i], PRECISION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
|
import { Z_OFFSET } from '../constants.js';
|
||||||
|
|
||||||
export default function calculateLayersIntersections(lines, settings) {
|
export default function calculateLayersIntersections(lines, settings) {
|
||||||
const {
|
const {
|
||||||
dimensions: { z: dimensionsZ },
|
dimensions: { z: dimensionsZ },
|
||||||
layerHeight,
|
layerHeight
|
||||||
zOffset
|
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const numLayers = Math.floor((dimensionsZ - zOffset) / layerHeight);
|
const numLayers = Math.floor((dimensionsZ - Z_OFFSET) / layerHeight);
|
||||||
|
|
||||||
const layers = Array.from(Array(numLayers)).map(() => ({
|
const layerPoints = Array.from(Array(numLayers)).map(() => ({}));
|
||||||
points: {},
|
const layerFaceIndexes = Array.from(Array(numLayers)).map(() => []);
|
||||||
faceIndexes: []
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
|
||||||
const { line, faces } = lines[lineIndex];
|
const { line, faces } = lines[lineIndex];
|
||||||
|
|
||||||
const min = Math.ceil((Math.min(line.start.y, line.end.y) - zOffset) / layerHeight);
|
const min = Math.ceil((Math.min(line.start.y, line.end.y) - Z_OFFSET) / layerHeight);
|
||||||
const max = Math.floor((Math.max(line.start.y, line.end.y) - zOffset) / layerHeight);
|
const max = Math.floor((Math.max(line.start.y, line.end.y) - Z_OFFSET) / layerHeight);
|
||||||
|
|
||||||
for (let layerIndex = min; layerIndex <= max; layerIndex ++) {
|
for (let layerIndex = min; layerIndex <= max; layerIndex ++) {
|
||||||
if (layerIndex >= 0 && layerIndex < numLayers) {
|
if (layerIndex >= 0 && layerIndex < numLayers) {
|
||||||
const y = layerIndex * layerHeight + zOffset;
|
const y = layerIndex * layerHeight + Z_OFFSET;
|
||||||
|
|
||||||
let x, z;
|
let x, z;
|
||||||
if (line.start.y === line.end.y) {
|
if (line.start.y === line.end.y) {
|
||||||
@ -33,20 +32,14 @@ export default function calculateLayersIntersections(lines, settings) {
|
|||||||
z = line.end.z * alpha + line.start.z * alpha1;
|
z = line.end.z * alpha + line.start.z * alpha1;
|
||||||
}
|
}
|
||||||
|
|
||||||
layers[layerIndex].points[lineIndex] = { x: z, y: x };
|
layerPoints[layerIndex][lineIndex] = { x: z, y: x };
|
||||||
layers[layerIndex].faceIndexes.push(...faces);
|
for (const faceIndex of faces) {
|
||||||
|
const layerFaceIndex = layerFaceIndexes[layerIndex];
|
||||||
|
if (!layerFaceIndex.includes(faceIndex)) layerFaceIndex.push(faceIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < layers.length; i ++) {
|
return { layerPoints, layerFaceIndexes };
|
||||||
const layer = layers[i];
|
|
||||||
|
|
||||||
layer.faceIndexes = layer.faceIndexes.reduce((result, faceIndex) => {
|
|
||||||
if (!result.includes(faceIndex)) result.push(faceIndex);
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return layers;
|
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,70 @@
|
|||||||
import { Line3 } from 'three/src/math/Line3.js';
|
import * as vector2 from './helpers/vector2.js';
|
||||||
import { normalize } from './helpers/VectorUtils.js';
|
import * as vector3 from './helpers/vector3.js';
|
||||||
|
|
||||||
function addLine(geometry, lineLookup, lines, a, b, faceIndex) {
|
export default function createLines(geometry, settings) {
|
||||||
|
const faces = [];
|
||||||
|
const lines = [];
|
||||||
|
const lineLookup = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < geometry.objectIndexes.length; i ++) {
|
||||||
|
const objectIndex = geometry.objectIndexes[i];
|
||||||
|
const { x: a, y: b, z: c } = getVertex(geometry.faces, i);
|
||||||
|
const normal = calculateNormal(geometry.vertices, a, b, c);
|
||||||
|
|
||||||
|
// skip faces that point up or down
|
||||||
|
if (normal.y > .999 || normal.y < -.999) {
|
||||||
|
faces.push(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexA = addLine(geometry.vertices, lineLookup, lines, a, b, i);
|
||||||
|
const indexB = addLine(geometry.vertices, lineLookup, lines, b, c, i);
|
||||||
|
const indexC = addLine(geometry.vertices, lineLookup, lines, c, a, i);
|
||||||
|
|
||||||
|
const flatNormal = vector2.normalize({ x: normal.z, y: normal.x });
|
||||||
|
const lineIndexes = [indexA, indexB, indexC];
|
||||||
|
|
||||||
|
faces.push({ lineIndexes, flatNormal, objectIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lines, faces };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLine(vertices, lineLookup, lines, a, b, faceIndex) {
|
||||||
let index;
|
let index;
|
||||||
if (typeof lineLookup[`${b}_${a}`] !== 'undefined') {
|
if (typeof lineLookup[`${b}_${a}`] !== 'undefined') {
|
||||||
index = lineLookup[`${b}_${a}`];
|
index = lineLookup[`${b}_${a}`];
|
||||||
} else {
|
} else {
|
||||||
|
const start = getVertex(vertices, a);
|
||||||
|
const end = getVertex(vertices, b);
|
||||||
|
const line = { start, end };
|
||||||
|
const faces = [];
|
||||||
|
|
||||||
index = lines.length;
|
index = lines.length;
|
||||||
lineLookup[`${a}_${b}`] = index;
|
lineLookup[`${a}_${b}`] = index;
|
||||||
|
lines.push({ line, faces });
|
||||||
const line = new Line3(geometry.vertices[a], geometry.vertices[b]);
|
|
||||||
lines.push({ line, faces: [] });
|
|
||||||
}
|
}
|
||||||
|
lines[index].faces.push(faceIndex);
|
||||||
const { faces } = lines[index];
|
|
||||||
faces.push(faceIndex);
|
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createLines(geometry, settings) {
|
function calculateNormal(vertices, a, b, c) {
|
||||||
const lines = [];
|
a = getVertex(vertices, a);
|
||||||
const lineLookup = {};
|
b = getVertex(vertices, b);
|
||||||
|
c = getVertex(vertices, c);
|
||||||
|
|
||||||
const faces = geometry.faces.map((face, i) => {
|
const cb = vector3.subtract(c, b);
|
||||||
const { normal, materialIndex: objectIndex, a, b, c } = geometry.faces[i];
|
const ab = vector3.subtract(a, b);
|
||||||
|
const normal = vector3.normalize(vector3.cross(cb, ab));
|
||||||
|
|
||||||
// skip faces that point up or down
|
return normal;
|
||||||
if (normal.y > .999 || normal.y < -.999) return;
|
}
|
||||||
|
|
||||||
const indexA = addLine(geometry, lineLookup, lines, a, b, i);
|
function getVertex(vertices, i) {
|
||||||
const indexB = addLine(geometry, lineLookup, lines, b, c, i);
|
const i3 = i * 3;
|
||||||
const indexC = addLine(geometry, lineLookup, lines, c, a, i);
|
return {
|
||||||
|
x: vertices[i3],
|
||||||
const flatNormal = normalize({ x: normal.z, y: normal.x });
|
y: vertices[i3 + 1],
|
||||||
const lineIndexes = [indexA, indexB, indexC];
|
z: vertices[i3 + 2]
|
||||||
return { lineIndexes, flatNormal, objectIndex };
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return { lines, faces };
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import Shape from 'clipper-js';
|
|||||||
export default function generateInfills(slices, settings) {
|
export default function generateInfills(slices, settings) {
|
||||||
let {
|
let {
|
||||||
layerHeight,
|
layerHeight,
|
||||||
innerInfill: { gridSize: infillGridSize },
|
innerInfill: { density },
|
||||||
thickness: {
|
thickness: {
|
||||||
top: topThickness,
|
top: topThickness,
|
||||||
bottom: bottomThickness
|
bottom: bottomThickness
|
||||||
@ -13,13 +13,16 @@ export default function generateInfills(slices, settings) {
|
|||||||
nozzleDiameter
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
infillGridSize /= PRECISION;
|
density /= 100;
|
||||||
nozzleDiameter /= PRECISION;
|
nozzleDiameter /= PRECISION;
|
||||||
|
|
||||||
|
const bidirectionalInfill = density < 0.8;
|
||||||
|
const infillGridSize = nozzleDiameter * (bidirectionalInfill ? 2 : 1) / density;
|
||||||
|
|
||||||
const bottomSkinCount = Math.ceil(bottomThickness / layerHeight);
|
const bottomSkinCount = Math.ceil(bottomThickness / layerHeight);
|
||||||
const topSkinCount = Math.ceil(topThickness / layerHeight);
|
const topSkinCount = Math.ceil(topThickness / layerHeight);
|
||||||
const nozzleRadius = nozzleDiameter / 2;
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
const outerFillTemplateSize = Math.sqrt(2 * Math.pow(nozzleDiameter, 2));
|
const outerFillTemplateSize = nozzleDiameter;
|
||||||
|
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
@ -32,6 +35,7 @@ export default function generateInfills(slices, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < slice.parts.length; i ++) {
|
for (let i = 0; i < slice.parts.length; i ++) {
|
||||||
|
const even = (layer % 2 === 0);
|
||||||
const part = slice.parts[i];
|
const part = slice.parts[i];
|
||||||
|
|
||||||
if (!part.closed) continue;
|
if (!part.closed) continue;
|
||||||
@ -52,14 +56,13 @@ export default function generateInfills(slices, settings) {
|
|||||||
|
|
||||||
if (innerFillArea && innerFillArea.paths.length > 0) {
|
if (innerFillArea && innerFillArea.paths.length > 0) {
|
||||||
const bounds = innerFillArea.shapeBounds();
|
const bounds = innerFillArea.shapeBounds();
|
||||||
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, true, true);
|
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, bidirectionalInfill || even, bidirectionalInfill || !even);
|
||||||
|
|
||||||
part.innerFill.join(innerFillTemplate.intersect(innerFillArea));
|
part.innerFill.join(innerFillTemplate.intersect(innerFillArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outerFillArea.paths.length > 0) {
|
if (outerFillArea.paths.length > 0) {
|
||||||
const bounds = outerFillArea.shapeBounds();
|
const bounds = outerFillArea.shapeBounds();
|
||||||
const even = (layer % 2 === 0);
|
|
||||||
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
|
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
|
||||||
|
|
||||||
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));
|
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PRECISION } from '../constants.js'
|
import { PRECISION } from '../constants.js'
|
||||||
|
|
||||||
const offsetOptions = {
|
const OFFSET_OPTIONS = {
|
||||||
jointType: 'jtSquare',
|
jointType: 'jtSquare',
|
||||||
endType: 'etClosedPolygon',
|
endType: 'etClosedPolygon',
|
||||||
miterLimit: 2.0,
|
miterLimit: 2.0,
|
||||||
@ -29,7 +29,7 @@ export default function generateInnerLines(slices, settings) {
|
|||||||
|
|
||||||
if (!part.closed) continue;
|
if (!part.closed) continue;
|
||||||
|
|
||||||
const outerLine = part.shape.offset(-nozzleRadius, offsetOptions);
|
const outerLine = part.shape.offset(-nozzleRadius, OFFSET_OPTIONS);
|
||||||
|
|
||||||
if (outerLine.paths.length === 0) continue;
|
if (outerLine.paths.length === 0) continue;
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export default function generateInnerLines(slices, settings) {
|
|||||||
for (let inset = 1; inset < numShells; inset += 1) {
|
for (let inset = 1; inset < numShells; inset += 1) {
|
||||||
const offset = inset * nozzleDiameter;
|
const offset = inset * nozzleDiameter;
|
||||||
|
|
||||||
const shell = outerLine.offset(-offset, offsetOptions);
|
const shell = outerLine.offset(-offset, OFFSET_OPTIONS);
|
||||||
|
|
||||||
if (shell.paths.length === 0) {
|
if (shell.paths.length === 0) {
|
||||||
break;
|
break;
|
||||||
|
@ -2,75 +2,40 @@ import getFillTemplate from './getFillTemplate.js';
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION } from '../constants.js';
|
||||||
|
|
||||||
|
const PRECISION_SQUARED = Math.pow(PRECISION, 2);
|
||||||
|
|
||||||
export default function generateSupport(slices, settings) {
|
export default function generateSupport(slices, settings) {
|
||||||
if (!settings.support.enabled) return;
|
if (!settings.support.enabled) return;
|
||||||
|
|
||||||
let {
|
let {
|
||||||
layerHeight,
|
layerHeight,
|
||||||
support: {
|
support: { density, margin, minArea, distanceY },
|
||||||
gridSize: supportGridSize,
|
|
||||||
margin: supportMargin,
|
|
||||||
plateSize: plateSize,
|
|
||||||
distanceY: supportDistanceY
|
|
||||||
},
|
|
||||||
nozzleDiameter
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
supportGridSize /= PRECISION;
|
density /= 100;
|
||||||
supportMargin /= PRECISION;
|
margin /= PRECISION;
|
||||||
plateSize /= PRECISION;
|
|
||||||
nozzleDiameter /= PRECISION;
|
nozzleDiameter /= PRECISION;
|
||||||
var supportDistanceLayers = Math.max(Math.ceil(supportDistanceY / layerHeight), 1);
|
|
||||||
|
|
||||||
var supportAreas = new Shape([], true);
|
const infillGridSize = nozzleDiameter * 2 / density;
|
||||||
|
const supportDistanceLayers = Math.max(Math.ceil(distanceY / layerHeight), 1);
|
||||||
|
|
||||||
for (var layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) {
|
let supportArea = new Shape([], true);
|
||||||
var currentSlice = slices[layer];
|
|
||||||
|
|
||||||
if (supportAreas.length > 0) {
|
for (let layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) {
|
||||||
|
const currentLayer = slices[layer + supportDistanceLayers - 1];
|
||||||
|
const upSkin = slices[layer + supportDistanceLayers];
|
||||||
|
const downSkin = slices[layer - supportDistanceLayers];
|
||||||
|
|
||||||
if (layer >= supportDistanceLayers) {
|
const neededSupportArea = upSkin.outline.difference(currentLayer.outline.offset(margin));
|
||||||
var sliceSkin = slices[layer - supportDistanceLayers].outline;
|
|
||||||
sliceSkin = sliceSkin;
|
|
||||||
|
|
||||||
var supportAreasSlimmed = supportAreas.difference(sliceSkin.offset(supportMargin));
|
if (neededSupportArea.totalArea() * PRECISION_SQUARED > minArea) supportArea = supportArea.union(neededSupportArea);
|
||||||
if (supportAreasSlimmed.area() < 100.0) {
|
if (downSkin) supportArea = supportArea.difference(downSkin.outline.offset(margin));
|
||||||
supportAreas = supportAreas.difference(sliceSkin);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
supportAreas = supportAreasSlimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportTemplate = getFillTemplate(supportAreas.bounds(), supportGridSize, true, true);
|
const bounds = supportArea.shapeBounds();
|
||||||
var supportFill = supportTemplate.intersect(supportAreas);
|
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, true, true);
|
||||||
if (supportFill.length === 0) {
|
|
||||||
currentSlice.support = supportAreas.clone();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
currentSlice.support = supportFill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportSkin = slices[layer + supportDistanceLayers - 1].outline;
|
slices[layer].support = supportArea.clone().join(supportArea.intersect(innerFillTemplate));
|
||||||
|
slices[layer].supportOutline = supportArea;
|
||||||
var slice = slices[layer + supportDistanceLayers];
|
|
||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
|
||||||
var slicePart = slice.parts[i];
|
|
||||||
|
|
||||||
if (slicePart.intersect.closed) {
|
|
||||||
var outerLine = slicePart.outerLine;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var outerLine = slicePart.intersect.offset(supportMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
var overlap = supportSkin.offset(supportMargin).intersect(outerLine);
|
|
||||||
var overhang = outerLine.difference(overlap);
|
|
||||||
|
|
||||||
if (overlap.length === 0 || overhang.length > 0) {
|
|
||||||
supportAreas = supportAreas.join(overhang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
|
|
||||||
export default function getFillTemplate(bounds, size, even, uneven) {
|
export default function getFillTemplate(bounds, gridSize, even, uneven) {
|
||||||
const paths = [];
|
const paths = [];
|
||||||
|
|
||||||
|
const size = Math.sqrt(2 * Math.pow(gridSize, 2));
|
||||||
|
|
||||||
const left = Math.floor(bounds.left / size) * size;
|
const left = Math.floor(bounds.left / size) * size;
|
||||||
const right = Math.ceil(bounds.right / size) * size;
|
const right = Math.ceil(bounds.right / size) * size;
|
||||||
const top = Math.floor(bounds.top / size) * size;
|
const top = Math.floor(bounds.top / size) * size;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Vector2 } from 'three/src/math/Vector2.js';
|
import { scale, distanceTo } from './vector2.js';
|
||||||
import { PRECISION } from '../../constants.js';
|
import { PRECISION, VERSION } from '../../constants.js';
|
||||||
|
|
||||||
export const MOVE = 'G';
|
export const MOVE = 'G';
|
||||||
export const M_COMMAND = 'M';
|
export const M_COMMAND = 'M';
|
||||||
@ -10,13 +10,15 @@ export const POSITION_X = 'X';
|
|||||||
export const POSITION_Y = 'Y';
|
export const POSITION_Y = 'Y';
|
||||||
export const POSITION_Z = 'Z';
|
export const POSITION_Z = 'Z';
|
||||||
|
|
||||||
export default class {
|
export default class GCode {
|
||||||
constructor(nozzleToFilamentRatio) {
|
constructor(settings) {
|
||||||
this._nozzleToFilamentRatio = nozzleToFilamentRatio;
|
this._nozzleToFilamentRatio = 1;
|
||||||
|
this._gcode = [
|
||||||
this._gcode = [];
|
`; ${JSON.stringify(settings).trim()}`,
|
||||||
|
`; Generated with Doodle3D Slicer V${VERSION}`
|
||||||
|
];
|
||||||
this._currentValues = {};
|
this._currentValues = {};
|
||||||
this._nozzlePosition = new Vector2(0, 0);
|
this._nozzlePosition = { x: 0, y: 0 };
|
||||||
this._extruder = 0.0;
|
this._extruder = 0.0;
|
||||||
this._duration = 0.0;
|
this._duration = 0.0;
|
||||||
this._isRetracted = false;
|
this._isRetracted = false;
|
||||||
@ -27,10 +29,16 @@ export default class {
|
|||||||
this._gcode.push(command);
|
this._gcode.push(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness) {
|
||||||
|
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
|
||||||
|
const lineSurfaceArea = nozzleDiameter * layerHeight;
|
||||||
|
this._nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
|
||||||
|
}
|
||||||
|
|
||||||
turnFanOn(fanSpeed) {
|
turnFanOn(fanSpeed) {
|
||||||
this._isFanOn = true;
|
this._isFanOn = true;
|
||||||
|
|
||||||
const gcode = { [M_COMMAND]: 106 }
|
const gcode = { [M_COMMAND]: 106 };
|
||||||
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
|
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
|
||||||
|
|
||||||
this._addGCode(gcode);
|
this._addGCode(gcode);
|
||||||
@ -47,41 +55,41 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveTo(x, y, z, { speed }) {
|
moveTo(x, y, z, { speed }) {
|
||||||
const newNozzlePosition = new Vector2(x, y).multiplyScalar(PRECISION);
|
const newNozzlePosition = scale({ x, y }, PRECISION);
|
||||||
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
|
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
|
||||||
|
|
||||||
this._duration += lineLength / speed;
|
this._duration += lineLength / speed;
|
||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[POSITION_X]: newNozzlePosition.x.toFixed(3),
|
[POSITION_X]: newNozzlePosition.x,
|
||||||
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
|
[POSITION_Y]: newNozzlePosition.y,
|
||||||
[POSITION_Z]: z.toFixed(3),
|
[POSITION_Z]: z,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
|
|
||||||
this._nozzlePosition.copy(newNozzlePosition);
|
this._nozzlePosition = newNozzlePosition;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
lineTo(x, y, z, { speed, flowRate }) {
|
lineTo(x, y, z, { speed, flowRate }) {
|
||||||
const newNozzlePosition = new Vector2(x, y).multiplyScalar(PRECISION);
|
const newNozzlePosition = scale({ x, y }, PRECISION);
|
||||||
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
|
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
|
||||||
|
|
||||||
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
|
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
|
||||||
this._duration += lineLength / speed;
|
this._duration += lineLength / speed;
|
||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 1,
|
[MOVE]: 1,
|
||||||
[POSITION_X]: newNozzlePosition.x.toFixed(3),
|
[POSITION_X]: newNozzlePosition.x,
|
||||||
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
|
[POSITION_Y]: newNozzlePosition.y,
|
||||||
[POSITION_Z]: z.toFixed(3),
|
[POSITION_Z]: z,
|
||||||
[SPEED]: (speed * 60).toFixed(3),
|
[SPEED]: speed * 60,
|
||||||
[EXTRUDER]: this._extruder.toFixed(3)
|
[EXTRUDER]: this._extruder
|
||||||
});
|
});
|
||||||
|
|
||||||
this._nozzlePosition.copy(newNozzlePosition);
|
this._nozzlePosition = newNozzlePosition;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -95,8 +103,8 @@ export default class {
|
|||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[EXTRUDER]: this._extruder.toFixed(3),
|
[EXTRUDER]: this._extruder,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,8 +121,8 @@ export default class {
|
|||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[EXTRUDER]: (this._extruder - amount).toFixed(3),
|
[EXTRUDER]: this._extruder - amount,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,6 +130,15 @@ export default class {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addGCode(gcode, { temperature, bedTemperature, heatedBed }) {
|
||||||
|
gcode = gcode
|
||||||
|
.replace(/{temperature}/gi, temperature)
|
||||||
|
.replace(/{bedTemperature}/gi, bedTemperature)
|
||||||
|
.replace(/{if heatedBed}/gi, heatedBed ? '' : ';');
|
||||||
|
|
||||||
|
this._addGCode(gcode);
|
||||||
|
}
|
||||||
|
|
||||||
getGCode() {
|
getGCode() {
|
||||||
return {
|
return {
|
||||||
gcode: this._gcode,
|
gcode: this._gcode,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
|
|
||||||
export default class {
|
export default class Slice {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.parts = [];
|
this.parts = [];
|
||||||
}
|
}
|
||||||
|
24
src/sliceActions/helpers/color.js
Normal file
24
src/sliceActions/helpers/color.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export function hslToRgb(h, s, l){
|
||||||
|
let r, g, b;
|
||||||
|
|
||||||
|
if (s === 0) {
|
||||||
|
r = g = b = lightness;
|
||||||
|
} else {
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hueToRgb(p, q, h + 1 / 3);
|
||||||
|
g = hueToRgb(p, q, h);
|
||||||
|
b = hueToRgb(p, q, h - 1 / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hueToRgb(p, q, t){
|
||||||
|
if (t < 0) t += 1;
|
||||||
|
if (t > 1) t -= 1;
|
||||||
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1 / 2) return q;
|
||||||
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
import { subtract, add, scale, normalize, dot, length, distanceTo } from './VectorUtils.js';
|
import { subtract, add, scale, normalize, dot, length, distanceTo } from './vector2.js';
|
||||||
import { PRECISION } from '../../constants.js';
|
import { PRECISION } from '../../constants.js';
|
||||||
|
|
||||||
const TOLERANCE = 5 / PRECISION;
|
const TOLERANCE = 1 / PRECISION;
|
||||||
|
|
||||||
export default function comb(outline, start, end) {
|
export default function comb(outline, start, end) {
|
||||||
if (distanceTo(start, end) < TOLERANCE) {
|
if (distanceTo(start, end) < TOLERANCE) {
|
||||||
@ -45,9 +45,9 @@ export default function comb(outline, start, end) {
|
|||||||
|
|
||||||
if (snappedCombPaths.length === 0) {
|
if (snappedCombPaths.length === 0) {
|
||||||
snappedCombPaths.push([start], [end]);
|
snappedCombPaths.push([start], [end]);
|
||||||
} else if (distanceTo(firstPath[0], start) > 1.0) {
|
} else if (distanceTo(firstPath[0], start) > 1.) {
|
||||||
snappedCombPaths.unshift([start]);
|
snappedCombPaths.unshift([start]);
|
||||||
} else if (distanceTo(lastPath[lastPath.length - 1], end) > 1.0) {
|
} else if (distanceTo(lastPath[lastPath.length - 1], end) > 1.) {
|
||||||
snappedCombPaths.push([end]);
|
snappedCombPaths.push([end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,18 +6,20 @@ export const add = (a, b) => ({
|
|||||||
x: a.x + b.x,
|
x: a.x + b.x,
|
||||||
y: a.y + b.y
|
y: a.y + b.y
|
||||||
});
|
});
|
||||||
export const scale = (a, factor) => ({
|
export const scale = (v, factor) => ({
|
||||||
x: a.x * factor,
|
x: v.x * factor,
|
||||||
y: a.y * factor
|
y: v.y * factor
|
||||||
});
|
});
|
||||||
export const devide = (a, factor) => ({
|
export const divide = (v, factor) => ({
|
||||||
x: a.x / factor,
|
x: v.x / factor,
|
||||||
y: a.y / factor
|
y: v.y / factor
|
||||||
});
|
});
|
||||||
export const normal = (a) => ({
|
export const normal = (v) => ({
|
||||||
x: -a.y,
|
x: -v.y,
|
||||||
y: a.x
|
y: v.x
|
||||||
});
|
});
|
||||||
|
export const equals = (a, b) => a.x === b.x && a.y === b.y;
|
||||||
|
export const almostEquals = (a, b) => Math.abs(a.x - b.x) < 0.001 && Math.abs(a.y - b.y) < 0.001;
|
||||||
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));
|
||||||
@ -28,5 +30,4 @@ export const normalize = (v) => {
|
|||||||
x: v.x / l,
|
x: v.x / l,
|
||||||
y: v.y / l
|
y: v.y / l
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
export const clone = (v) => ({ x: v.x, y: v.y });
|
|
38
src/sliceActions/helpers/vector3.js
Normal file
38
src/sliceActions/helpers/vector3.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
export const subtract = (a, b) => ({
|
||||||
|
x: a.x - b.x,
|
||||||
|
y: a.y - b.y,
|
||||||
|
z: a.z - b.z
|
||||||
|
});
|
||||||
|
export const add = (a, b) => ({
|
||||||
|
x: a.x + b.x,
|
||||||
|
y: a.y + b.y,
|
||||||
|
z: a.z + b.z
|
||||||
|
});
|
||||||
|
export const scale = (v, factor) => ({
|
||||||
|
x: v.x * factor,
|
||||||
|
y: v.y * factor,
|
||||||
|
z: v.z * factor
|
||||||
|
});
|
||||||
|
export const divide = (v, factor) => ({
|
||||||
|
x: v.x / factor,
|
||||||
|
y: v.y / factor,
|
||||||
|
z: v.z / factor
|
||||||
|
});
|
||||||
|
export const cross = (a, b) => ({
|
||||||
|
x: a.y * b.z - a.z * b.y,
|
||||||
|
y: a.z * b.x - a.x * b.z,
|
||||||
|
z: a.x * b.y - a.y * b.x
|
||||||
|
});
|
||||||
|
export const equals = (a, b) => a.x === b.x && a.y === b.y && a.z === b.z;
|
||||||
|
export const almostEquals = (a, b) => Math.abs(a.x - b.x) < 0.001 && Math.abs(a.y - b.y) < 0.001 && Math.abs(a.z - b.z) < 0.001;
|
||||||
|
export const length = (v) => Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
||||||
|
export const distanceTo = (a, b) => length(subtract(a, b));
|
||||||
|
export const normalize = (v) => {
|
||||||
|
const l = length(v);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: v.x / l,
|
||||||
|
y: v.y / l,
|
||||||
|
z: v.z / l
|
||||||
|
};
|
||||||
|
};
|
@ -1,136 +1,157 @@
|
|||||||
import { subtract, normal, normalize, dot, distanceTo, clone } from './helpers/VectorUtils.js';
|
import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js';
|
||||||
|
|
||||||
export default function intersectionsToShapes(intersectionLayers, faces, openObjectIndexes, settings) {
|
export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings) {
|
||||||
const layers = [];
|
const layers = [];
|
||||||
|
|
||||||
for (let layer = 0; layer < intersectionLayers.length; layer ++) {
|
for (let layer = 0; layer < layerPoints.length; layer ++) {
|
||||||
const fillShapes = [];
|
const fillShapes = [];
|
||||||
const lineShapesOpen = [];
|
const lineShapesOpen = [];
|
||||||
const lineShapesClosed = [];
|
const lineShapesClosed = [];
|
||||||
|
|
||||||
const { points, faceIndexes } = intersectionLayers[layer];
|
const points = layerPoints[layer];
|
||||||
|
const faceIndexes = layerFaceIndexes[layer];
|
||||||
|
|
||||||
if (faceIndexes.length === 0) continue;
|
if (faceIndexes.length === 0) continue;
|
||||||
|
|
||||||
const shapes = {};
|
const shapes = {};
|
||||||
|
|
||||||
|
const startConnects = {};
|
||||||
|
const endConnects = {};
|
||||||
|
|
||||||
for (let i = 0; i < faceIndexes.length; i ++) {
|
for (let i = 0; i < faceIndexes.length; i ++) {
|
||||||
const { lineIndexes, objectIndex, flatNormal } = faces[faceIndexes[i]];
|
const faceIndex = faceIndexes[i];
|
||||||
|
const { lineIndexes, flatNormal, objectIndex } = faces[faceIndex];
|
||||||
|
|
||||||
const a = points[lineIndexes[0]];
|
const a = lineIndexes[0];
|
||||||
const b = points[lineIndexes[1]];
|
const b = lineIndexes[1];
|
||||||
const c = points[lineIndexes[2]];
|
const c = lineIndexes[2];
|
||||||
|
|
||||||
const lineSegment = [];
|
let pointA;
|
||||||
if (a && b) {
|
let pointB;
|
||||||
lineSegment.push(a, b);
|
if (points[a] && points[b]) {
|
||||||
} else if (b && c) {
|
pointA = a;
|
||||||
lineSegment.push(b, c);
|
pointB = b;
|
||||||
} else if (c && a) {
|
} else if (points[b] && points[c]) {
|
||||||
lineSegment.push(c, a);
|
pointA = b;
|
||||||
|
pointB = c;
|
||||||
|
} else if (points[c] && points[a]) {
|
||||||
|
pointA = c;
|
||||||
|
pointB = a;
|
||||||
} else {
|
} else {
|
||||||
|
// should never happen
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentNormal = normalize(normal(subtract(lineSegment[1], lineSegment[0])));
|
const segmentNormal = normalize(normal(subtract(points[pointA], points[pointB])));
|
||||||
if (dot(segmentNormal, flatNormal) < 0) lineSegment.reverse();
|
if (dot(segmentNormal, flatNormal) < 0) {
|
||||||
|
const temp = pointB;
|
||||||
|
pointB = pointA;
|
||||||
|
pointA = temp;
|
||||||
|
}
|
||||||
|
|
||||||
if (!shapes[objectIndex]) shapes[objectIndex] = { lineSegments: [] };
|
if (endConnects[pointA]) {
|
||||||
const shape = shapes[objectIndex];
|
const lineSegment = endConnects[pointA];
|
||||||
|
delete endConnects[pointA];
|
||||||
|
if (startConnects[pointB]) {
|
||||||
|
if (startConnects[pointB] === lineSegment) {
|
||||||
|
delete startConnects[pointB];
|
||||||
|
lineSegment.push(pointB);
|
||||||
|
} else {
|
||||||
|
lineSegment.push(...startConnects[pointB]);
|
||||||
|
endConnects[lineSegment[lineSegment.length - 1]] = lineSegment;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lineSegment.push(pointB);
|
||||||
|
endConnects[pointB] = lineSegment;
|
||||||
|
}
|
||||||
|
} else if (startConnects[pointB]) {
|
||||||
|
const lineSegment = startConnects[pointB];
|
||||||
|
delete startConnects[pointB];
|
||||||
|
if (endConnects[pointA]) {
|
||||||
|
lineSegment.unshift(...endConnects[pointA]);
|
||||||
|
startConnects[lineSegment[0]] = lineSegment;
|
||||||
|
} else {
|
||||||
|
lineSegment.unshift(pointA);
|
||||||
|
startConnects[pointA] = lineSegment;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lineSegment = [pointA, pointB];
|
||||||
|
startConnects[pointA] = lineSegment;
|
||||||
|
endConnects[pointB] = lineSegment;
|
||||||
|
|
||||||
shape.lineSegments.push(lineSegment)
|
if (!shapes[objectIndex]) shapes[objectIndex] = [];
|
||||||
|
shapes[objectIndex].push(lineSegment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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]))
|
||||||
|
.filter(lineSegment => lineSegment.some(i => !almostEquals(lineSegment[0], lineSegment[1])));
|
||||||
const openShape = openObjectIndexes[objectIndex];
|
const openShape = openObjectIndexes[objectIndex];
|
||||||
|
|
||||||
const lines = [shape.lineSegments.pop()];
|
const connectPoints = [];
|
||||||
|
for (let pathIndex = 0; pathIndex < shape.length; pathIndex ++) {
|
||||||
|
const path = shape[pathIndex];
|
||||||
|
|
||||||
loop: while (shape.lineSegments.length !== 0) {
|
if (almostEquals(path[0], path[path.length - 1])) {
|
||||||
for (let i = 0; i < lines.length; i ++) {
|
if (openShape) {
|
||||||
const line = lines[i];
|
lineShapesClosed.push(path);
|
||||||
|
} else {
|
||||||
const lastPoint = line[line.length - 1];
|
fillShapes.push(path);
|
||||||
|
|
||||||
let closestSegmentEnd;
|
|
||||||
let endHit = false;
|
|
||||||
const distanceEnd = new WeakMap();
|
|
||||||
for (let i = 0; i < shape.lineSegments.length; i ++) {
|
|
||||||
const lineSegment = shape.lineSegments[i];
|
|
||||||
if (lastPoint === lineSegment[0]) {
|
|
||||||
closestSegmentEnd = lineSegment;
|
|
||||||
endHit = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
const distance = distanceTo(lastPoint, lineSegment[0]);
|
continue;
|
||||||
distanceEnd.set(lineSegment, distance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!endHit) {
|
let shapeStartPoint = path[0];
|
||||||
closestSegmentEnd = shape.lineSegments.sort((a, b) => {
|
const connectNext = connectPoints.find(({ point }) => almostEquals(point, shapeStartPoint));
|
||||||
const distanceA = distanceEnd.get(a);
|
if (connectNext) {
|
||||||
const distanceB = distanceEnd.get(b);
|
connectNext.next = pathIndex;
|
||||||
if (distanceA === distanceB) return distanceTo(a[0], a[1]) - distanceTo(b[0], b[1]);
|
} else {
|
||||||
return distanceA - distanceB;
|
connectPoints.push({ point: shapeStartPoint, next: pathIndex, previous: -1 });
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (distanceTo(closestSegmentEnd[0], lastPoint) < .001) endHit = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endHit) {
|
let shapeEndPoint = path[path.length - 1];
|
||||||
shape.lineSegments.splice(shape.lineSegments.indexOf(closestSegmentEnd), 1);
|
const connectPrevious = connectPoints.find(({ point }) => almostEquals(point, shapeEndPoint));
|
||||||
line.splice(line.length, 0, closestSegmentEnd[1]);
|
if (connectPrevious) {
|
||||||
continue loop;
|
connectPrevious.previous = pathIndex;
|
||||||
|
} else {
|
||||||
|
connectPoints.push({ point: shapeEndPoint, next: -1, previous: pathIndex });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstPoint = line[0];
|
connectPoints.sort(({ previous }) => -previous);
|
||||||
|
|
||||||
let closestSegmentStart;
|
while (connectPoints.length !== 0) {
|
||||||
let hitStart = false;
|
let { next, previous } = connectPoints.pop();
|
||||||
const distanceStart = new WeakMap();
|
|
||||||
for (let i = 0; i < shape.lineSegments.length; i ++) {
|
|
||||||
const lineSegment = shape.lineSegments[i];
|
|
||||||
if (firstPoint === lineSegment[1]) {
|
|
||||||
closestSegmentStart = lineSegment;
|
|
||||||
hitStart = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const distance = distanceTo(firstPoint, lineSegment[1]);
|
|
||||||
distanceStart.set(lineSegment, distance);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hitStart) {
|
const line = [];
|
||||||
closestSegmentStart = shape.lineSegments.sort((a, b) => {
|
if (previous !== -1) line.push(...shape[previous]);
|
||||||
const distanceA = distanceStart.get(a);
|
|
||||||
const distanceB = distanceStart.get(b);
|
|
||||||
if (distanceA === distanceB) return distanceTo(a[0], a[1]) - distanceTo(b[0], b[1]);
|
|
||||||
return distanceA - distanceB;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (distanceTo(closestSegmentStart[1], firstPoint) < .001) hitStart = true;
|
while (true) {
|
||||||
}
|
const pointIndex = connectPoints.findIndex(point => point.previous === next);
|
||||||
|
if (pointIndex === -1) break;
|
||||||
|
|
||||||
if (hitStart) {
|
const point = connectPoints[pointIndex];
|
||||||
shape.lineSegments.splice(shape.lineSegments.indexOf(closestSegmentStart), 1);
|
line.push(...shape[point.previous]);
|
||||||
line.splice(0, 0, closestSegmentStart[0]);
|
|
||||||
continue loop;
|
connectPoints.splice(pointIndex, 1);
|
||||||
}
|
|
||||||
}
|
if (point.next === -1) break;
|
||||||
lines.push(shape.lineSegments.pop());
|
if (point.next === previous) break;
|
||||||
|
|
||||||
|
next = point.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openShape) {
|
if (openShape) {
|
||||||
for (const line of lines) {
|
if (almostEquals(line[0], line[line.length - 1])) {
|
||||||
const closed = distanceTo(line[0], line[line.length - 1]) < .001;
|
|
||||||
if (closed) {
|
|
||||||
lineShapesClosed.push(line);
|
lineShapesClosed.push(line);
|
||||||
} else {
|
} else {
|
||||||
lineShapesOpen.push(line);
|
lineShapesOpen.push(line);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
fillShapes.push(...lines);
|
fillShapes.push(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { Vector2 } from 'three/src/math/Vector2.js';
|
import { length, 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, settings) {
|
||||||
const start = new Vector2(0, 0);
|
let start = { x: 0, y: 0 };
|
||||||
|
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
|
|
||||||
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
|
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
|
||||||
slice.brim = optimizeShape(slice.brim, start);
|
slice.brim = optimizeShape(slice.brim, start);
|
||||||
start.copy(slice.brim.lastPoint(true));
|
start = slice.brim.lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@ -54,36 +54,34 @@ export default function optimizePaths(slices, settings) {
|
|||||||
if (shell.paths.length === 0) continue;
|
if (shell.paths.length === 0) continue;
|
||||||
|
|
||||||
part.shell[i] = optimizeShape(shell, start);
|
part.shell[i] = optimizeShape(shell, start);
|
||||||
start.copy(part.shell[i].lastPoint(true));
|
start = part.shell[i].lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.outerFill.paths.length > 0) {
|
if (part.outerFill.paths.length > 0) {
|
||||||
part.outerFill = optimizeShape(part.outerFill, start);
|
part.outerFill = optimizeShape(part.outerFill, start);
|
||||||
start.copy(part.outerFill.lastPoint(true));
|
start = part.outerFill.lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.innerFill.paths.length > 0) {
|
if (part.innerFill.paths.length > 0) {
|
||||||
part.innerFill = optimizeShape(part.innerFill, start);
|
part.innerFill = optimizeShape(part.innerFill, start);
|
||||||
start.copy(part.innerFill.lastPoint(true));
|
start = part.innerFill.lastPoint(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
part.shape = optimizeShape(part.shape, start);
|
part.shape = optimizeShape(part.shape, start);
|
||||||
start.copy(part.shape.lastPoint(true));
|
start = part.shape.lastPoint(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slice.parts = parts;
|
slice.parts = parts;
|
||||||
|
|
||||||
if (typeof slice.support !== 'undefined' && slice.support.length > 0) {
|
if (typeof slice.support !== 'undefined' && slice.support.paths.length > 0) {
|
||||||
slice.support = optimizeShape(slice.support, start);
|
slice.support = optimizeShape(slice.support, start);
|
||||||
start.copy(slice.support.lastPoint(true));
|
start = slice.support.lastPoint(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function optimizeShape(shape, start) {
|
function optimizeShape(shape, start) {
|
||||||
start = start.clone();
|
|
||||||
|
|
||||||
const inputPaths = shape.mapToLower();
|
const inputPaths = shape.mapToLower();
|
||||||
const optimizedPaths = [];
|
const optimizedPaths = [];
|
||||||
const donePaths = [];
|
const donePaths = [];
|
||||||
@ -102,8 +100,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 point = new Vector2().copy(path[j]);
|
const length = distanceTo(path[j], start);
|
||||||
const length = point.sub(start).length();
|
|
||||||
if (minLength === false || length < minLength) {
|
if (minLength === false || length < minLength) {
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = length;
|
minLength = length;
|
||||||
@ -112,8 +109,7 @@ function optimizeShape(shape, start) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const startPoint = new Vector2().copy(path[0]);
|
const lengthToStart = distanceTo(path[0], start);
|
||||||
const lengthToStart = startPoint.sub(start).length();
|
|
||||||
if (minLength === false || lengthToStart < minLength) {
|
if (minLength === false || lengthToStart < minLength) {
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = lengthToStart;
|
minLength = lengthToStart;
|
||||||
@ -121,8 +117,7 @@ function optimizeShape(shape, start) {
|
|||||||
pathIndex = i;
|
pathIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endPoint = new Vector2().copy(path[path.length - 1]);
|
const lengthToEnd = distanceTo(path[path.length - 1], start);
|
||||||
const lengthToEnd = endPoint.sub(start).length();
|
|
||||||
if (lengthToEnd < minLength) {
|
if (lengthToEnd < minLength) {
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = lengthToEnd;
|
minLength = lengthToEnd;
|
||||||
@ -132,20 +127,15 @@ function optimizeShape(shape, start) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let point;
|
|
||||||
if (shape.closed) {
|
if (shape.closed) {
|
||||||
minPath = minPath.concat(minPath.splice(0, offset));
|
minPath = minPath.concat(minPath.splice(0, offset));
|
||||||
point = minPath[0];
|
start = minPath[0];
|
||||||
} else {
|
} else {
|
||||||
if (reverse) {
|
if (reverse) minPath.reverse();
|
||||||
minPath.reverse();
|
start = minPath[minPath.length - 1];
|
||||||
}
|
|
||||||
point = minPath[minPath.length - 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
donePaths.push(pathIndex);
|
donePaths.push(pathIndex);
|
||||||
start.copy(point);
|
|
||||||
|
|
||||||
optimizedPaths.push(minPath);
|
optimizedPaths.push(minPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from 'clipper-js';
|
||||||
import Slice from './helpers/Slice.js';
|
import Slice from './helpers/Slice.js';
|
||||||
|
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION, MIN_AREA } from '../constants.js';
|
||||||
|
|
||||||
export default function shapesToSlices(shapes, settings) {
|
export default function shapesToSlices(shapes, settings) {
|
||||||
const sliceLayers = [];
|
const sliceLayers = [];
|
||||||
@ -13,6 +13,7 @@ export default function shapesToSlices(shapes, settings) {
|
|||||||
.fixOrientation()
|
.fixOrientation()
|
||||||
.simplify('pftNonZero')
|
.simplify('pftNonZero')
|
||||||
.clean(1)
|
.clean(1)
|
||||||
|
.thresholdArea(MIN_AREA / Math.pow(PRECISION, 2))
|
||||||
.seperateShapes();
|
.seperateShapes();
|
||||||
|
|
||||||
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
|
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
|
||||||
@ -27,23 +28,16 @@ export default function shapesToSlices(shapes, settings) {
|
|||||||
|
|
||||||
for (let i = 0; i < fillShapes.length; i ++) {
|
for (let i = 0; i < fillShapes.length; i ++) {
|
||||||
const fillShape = fillShapes[i];
|
const fillShape = fillShapes[i];
|
||||||
|
if (fillShape.paths.length === 0) continue;
|
||||||
|
|
||||||
slice.add(fillShape, true);
|
slice.add(fillShape, true);
|
||||||
|
|
||||||
// if (lineShapesClosed.paths.length > 0) {
|
if (lineShapesClosed.paths.length > 0) lineShapesClosed = lineShapesClosed.difference(fillShape);
|
||||||
// lineShapesClosed = lineShapesClosed.difference(closedShape);
|
if (lineShapesOpen.paths.length > 0) lineShapesOpen = lineShapesOpen.difference(fillShape);
|
||||||
// }
|
|
||||||
// if (lineShapesOpen.paths.length > 0) {
|
|
||||||
// lineShapesOpen = lineShapesOpen.difference(closedShape);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lineShapesClosed.paths.length > 0) {
|
if (lineShapesClosed.paths.length > 0) slice.add(lineShapesClosed, false);
|
||||||
slice.add(lineShapesClosed, false);
|
if (lineShapesOpen.paths.length > 0) slice.add(lineShapesOpen, false);
|
||||||
}
|
|
||||||
|
|
||||||
if (lineShapesOpen.paths.length > 0) {
|
|
||||||
slice.add(lineShapesOpen, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
sliceLayers.push(slice);
|
sliceLayers.push(slice);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
import { Color } from 'three/src/math/Color.js';
|
|
||||||
import { BufferGeometry } from 'three/src/core/BufferGeometry.js';
|
|
||||||
import { BufferAttribute } from 'three/src/core/BufferAttribute.js';
|
|
||||||
import { LineBasicMaterial } from 'three/src/materials/LineBasicMaterial.js';
|
|
||||||
import { VertexColors } from 'three/src/constants.js';
|
|
||||||
import { LineSegments } from 'three/src/objects/LineSegments.js';
|
|
||||||
import calculateLayersIntersections from './calculateLayersIntersections.js';
|
import calculateLayersIntersections from './calculateLayersIntersections.js';
|
||||||
import createLines from './createLines.js';
|
import createLines from './createLines.js';
|
||||||
import generateInfills from './generateInfills.js';
|
import generateInfills from './generateInfills.js';
|
||||||
@ -16,32 +10,25 @@ import optimizePaths from './optimizePaths.js';
|
|||||||
import shapesToSlices from './shapesToSlices.js';
|
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 removePrecision from './removePrecision.js';
|
// // import removePrecision from './removePrecision.js';
|
||||||
|
|
||||||
export default function(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
export default function(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
const totalStages = 11;
|
const total = 11;
|
||||||
let current = -1;
|
let done = -1;
|
||||||
const updateProgress = (action) => {
|
const updateProgress = action => {
|
||||||
current ++;
|
done ++;
|
||||||
if (typeof onProgress !== 'undefined') {
|
if (onProgress) onProgress({ progress: { done, total, action } });
|
||||||
onProgress({
|
|
||||||
progress: {
|
|
||||||
done: current,
|
|
||||||
total: totalStages,
|
|
||||||
action
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateProgress('Constructing unique lines from geometry');
|
updateProgress('Constructing unique lines from geometry');
|
||||||
const { lines, faces } = createLines(geometry, settings);
|
const { lines, faces } = createLines(geometry, settings);
|
||||||
|
|
||||||
updateProgress('Calculating layer intersections');
|
updateProgress('Calculating layer intersections');
|
||||||
const layers = calculateLayersIntersections(lines, settings);
|
const { layerPoints, layerFaceIndexes } = calculateLayersIntersections(lines, settings);
|
||||||
|
|
||||||
updateProgress('Constructing shapes from intersections');
|
updateProgress('Constructing shapes from intersections');
|
||||||
const shapes = intersectionsToShapes(layers, faces, openObjectIndexes, settings);
|
const shapes = intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings);
|
||||||
|
|
||||||
applyPrecision(shapes);
|
applyPrecision(shapes);
|
||||||
|
|
||||||
@ -69,16 +56,26 @@ export default function(settings, geometry, openObjectIndexes, constructLinePrev
|
|||||||
updateProgress('Finished');
|
updateProgress('Finished');
|
||||||
|
|
||||||
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
|
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
|
||||||
gcode.gcode = gcodeToString(gcode.gcode);
|
|
||||||
|
gcode.gcode = new Blob([gcodeToString(gcode.gcode)], { type: 'text/plain' });
|
||||||
|
|
||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PRECISION = 1000;
|
||||||
|
function toFixedTrimmed(value) {
|
||||||
|
return (Math.round(value * PRECISION) / PRECISION).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function gcodeToString(gcode) {
|
function gcodeToString(gcode) {
|
||||||
const currentValues = {};
|
const currentValues = {};
|
||||||
return gcode.reduce((string, command) => {
|
return gcode.reduce((string, command) => {
|
||||||
|
if (typeof command === 'string') {
|
||||||
|
string += command;
|
||||||
|
} else {
|
||||||
let first = true;
|
let first = true;
|
||||||
for (const action in command) {
|
for (const action in command) {
|
||||||
const value = command[action];
|
const value = toFixedTrimmed(command[action]);
|
||||||
const currentValue = currentValues[action];
|
const currentValue = currentValues[action];
|
||||||
if (first) {
|
if (first) {
|
||||||
string += `${action}${value}`;
|
string += `${action}${value}`;
|
||||||
@ -88,41 +85,38 @@ function gcodeToString(gcode) {
|
|||||||
currentValues[action] = value;
|
currentValues[action] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
string += '\n';
|
string += '\n';
|
||||||
return string;
|
return string;
|
||||||
}, '');
|
}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_SPEED = 100 * 60;
|
const MAX_SPEED = 100 * 60;
|
||||||
const COLOR = new Color();
|
|
||||||
function createGcodeGeometry(gcode) {
|
function createGcodeGeometry(gcode) {
|
||||||
const positions = [];
|
const positions = [];
|
||||||
const colors = [];
|
const colors = [];
|
||||||
|
|
||||||
let lastPoint = [0, 0, 0];
|
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 command = gcode[i];
|
||||||
|
if (typeof command === 'string') continue;
|
||||||
|
|
||||||
|
const { G, F, X, Y, Z } = command;
|
||||||
|
|
||||||
if (X || Y || Z) {
|
if (X || Y || Z) {
|
||||||
if (G === 1) {
|
if (G === 1) {
|
||||||
positions.push(lastPoint.Y, lastPoint.Z, lastPoint.X);
|
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);
|
const color = (G === 0) ? [0, 1, 0] : hslToRgb(F / MAX_SPEED, 0.5, 0.5);
|
||||||
colors.push(color.r, color.g, color.b);
|
colors.push(...color, ...color);
|
||||||
colors.push(color.r, color.g, color.b);
|
|
||||||
}
|
}
|
||||||
lastPoint = { X, Y, Z };
|
lastPoint = { X, Y, Z };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const geometry = new BufferGeometry();
|
return {
|
||||||
|
positions: new Float32Array(positions),
|
||||||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(positions), 3));
|
colors: new Float32Array(colors)
|
||||||
geometry.addAttribute('color', new BufferAttribute(new Float32Array(colors), 3));
|
};
|
||||||
|
|
||||||
const material = new LineBasicMaterial({ vertexColors: VertexColors });
|
|
||||||
const linePreview = new LineSegments(geometry, material);
|
|
||||||
|
|
||||||
return linePreview;
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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 { PRECISION } from '../constants.js';
|
import { divide } from './helpers/vector2.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'];
|
||||||
|
|
||||||
@ -12,15 +13,13 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
travelSpeed,
|
travelSpeed,
|
||||||
retraction,
|
retraction,
|
||||||
travel,
|
travel,
|
||||||
combing,
|
combing
|
||||||
zOffset
|
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
|
const gcode = new GCode(settings);
|
||||||
const lineSurfaceArea = nozzleDiameter * layerHeight;
|
gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness)
|
||||||
const nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
|
|
||||||
|
|
||||||
const gcode = new GCode(nozzleToFilamentRatio);
|
if (settings.startCode) gcode.addGCode(settings.startCode, settings);
|
||||||
|
|
||||||
const defaultProfile = {
|
const defaultProfile = {
|
||||||
travelProfile: travel,
|
travelProfile: travel,
|
||||||
@ -30,9 +29,10 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
let isFirstLayer = true;
|
let isFirstLayer = true;
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
const z = layer * layerHeight + zOffset;
|
const z = layer * layerHeight + Z_OFFSET;
|
||||||
|
|
||||||
if (layer === 1) {
|
if (layer === 1) {
|
||||||
|
gcode.updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness);
|
||||||
gcode.turnFanOn();
|
gcode.turnFanOn();
|
||||||
isFirstLayer = false;
|
isFirstLayer = false;
|
||||||
}
|
}
|
||||||
@ -61,11 +61,11 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
|
|
||||||
const unRetract = isOuterShell;
|
const unRetract = isOuterShell;
|
||||||
const profile = isOuterShell ? profiles.outerShell : profiles.innerShell;
|
const profile = isOuterShell ? profiles.outerShell : profiles.innerShell;
|
||||||
pathToGCode(outline, combing && true, gcode, shell, false, unRetract, z, profile);
|
pathToGCode(outline, combing, gcode, shell, false, unRetract, z, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pathToGCode(outline, combing && true, gcode, part.outerFill, false, false, z, profiles.outerInfill);
|
pathToGCode(outline, combing, gcode, part.outerFill, false, false, z, profiles.outerInfill);
|
||||||
pathToGCode(outline, combing && true, gcode, part.innerFill, true, false, z, profiles.innerInfill);
|
pathToGCode(outline, combing, gcode, part.innerFill, true, false, z, profiles.innerInfill);
|
||||||
} else {
|
} else {
|
||||||
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
|
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
|
||||||
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
|
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
|
||||||
@ -73,10 +73,12 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof slice.support !== 'undefined') {
|
if (typeof slice.support !== 'undefined') {
|
||||||
pathToGCode(null, false, gcode, slice.support, true, true, z, profiles.support);
|
pathToGCode(slice.supportOutline, combing, gcode, slice.support, true, true, z, profiles.support);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.endCode) gcode.addGCode(settings.endCode, settings);
|
||||||
|
|
||||||
return gcode.getGCode();
|
return gcode.getGCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,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, gcode._nozzlePosition.divideScalar(PRECISION), point);
|
const combPath = comb(outline, divide(gcode._nozzlePosition, PRECISION), 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);
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import { VertexColors } from 'three/src/constants.js';
|
import * as THREE from 'three';
|
||||||
import { BufferAttribute } from 'three/src/core/BufferAttribute.js';
|
|
||||||
import { LineBasicMaterial } from 'three/src/materials/LineBasicMaterial.js';
|
|
||||||
import { LineSegments } from 'three/src/objects/LineSegments.js';
|
|
||||||
import slice from './sliceActions/slice.js';
|
import slice from './sliceActions/slice.js';
|
||||||
import SlicerWorker from './slicer.worker.js';
|
import SlicerWorker from './slicer.worker.js';
|
||||||
import { FrontSide, DoubleSide } from 'three/src/constants.js';
|
|
||||||
import { BufferGeometry } from 'three/src/core/BufferGeometry.js'
|
|
||||||
|
|
||||||
export function sliceMesh(settings, mesh, sync = false, constructLinePreview = false, onProgress) {
|
export function sliceMesh(settings, mesh, sync = false, constructLinePreview = false, onProgress) {
|
||||||
if (!mesh || !mesh.isMesh) {
|
if (!mesh || !mesh.isMesh) {
|
||||||
@ -21,26 +16,43 @@ export function sliceGeometry(settings, geometry, materials, matrix, sync = fals
|
|||||||
if (!geometry) {
|
if (!geometry) {
|
||||||
throw new Error('Missing required geometry argument');
|
throw new Error('Missing required geometry argument');
|
||||||
} else if (geometry.isBufferGeometry) {
|
} else if (geometry.isBufferGeometry) {
|
||||||
geometry = new Geometry().fromBufferGeometry(geometry);
|
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
|
||||||
} else if (geometry.isGeometry) {
|
} else if (geometry.isGeometry) {
|
||||||
geometry = geometry.clone();
|
geometry = geometry.clone();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
|
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geometry.faces.length === 0) {
|
if (matrix && matrix.isMatrix4) geometry.applyMatrix(matrix);
|
||||||
throw new Error('Geometry does not contain any data');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matrix && matrix.isMatrix4) {
|
const vertices = geometry.vertices.reduce((array, { x, y, z }, i) => {
|
||||||
geometry.applyMatrix(matrix);
|
const i3 = i * 3;
|
||||||
}
|
array[i3] = x;
|
||||||
|
array[i3 + 1] = y;
|
||||||
|
array[i3 + 2] = z;
|
||||||
|
return array;
|
||||||
|
}, new Float32Array(geometry.vertices.length * 3));
|
||||||
|
const faces = geometry.faces.reduce((array, { a, b, c }, i) => {
|
||||||
|
const i3 = i * 3;
|
||||||
|
array[i3] = a;
|
||||||
|
array[i3 + 1] = b;
|
||||||
|
array[i3 + 2] = c;
|
||||||
|
return array;
|
||||||
|
}, new Uint32Array(geometry.faces.length * 3));
|
||||||
|
const objectIndexes = geometry.faces.reduce((array, { materialIndex }, i) => {
|
||||||
|
array[i] = materialIndex;
|
||||||
|
return array;
|
||||||
|
}, new Uint8Array(geometry.faces.length));
|
||||||
|
|
||||||
|
if (faces.length === 0) throw new Error('Geometry does not contain any data');
|
||||||
|
|
||||||
|
geometry = { vertices, faces, objectIndexes };
|
||||||
|
|
||||||
const openObjectIndexes = materials instanceof Array ? materials.map(({ side }) => {
|
const openObjectIndexes = materials instanceof Array ? materials.map(({ side }) => {
|
||||||
switch (side) {
|
switch (side) {
|
||||||
case FrontSide:
|
case THREE.FrontSide:
|
||||||
return false;
|
return false;
|
||||||
case DoubleSide:
|
case THREE.DoubleSide:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@ -55,7 +67,9 @@ export function sliceGeometry(settings, geometry, materials, matrix, sync = fals
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
function sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
return slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
||||||
|
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
|
||||||
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
@ -63,10 +77,10 @@ function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview,
|
|||||||
// create the slicer worker
|
// create the slicer worker
|
||||||
const slicerWorker = new SlicerWorker();
|
const slicerWorker = new SlicerWorker();
|
||||||
|
|
||||||
slicerWorker.onerror = error => {
|
slicerWorker.addEventListener('error', event => {
|
||||||
slicerWorker.terminate();
|
slicerWorker.terminate();
|
||||||
reject(error);
|
reject(event);
|
||||||
};
|
});
|
||||||
|
|
||||||
// listen to messages send from worker
|
// listen to messages send from worker
|
||||||
slicerWorker.addEventListener('message', (event) => {
|
slicerWorker.addEventListener('message', (event) => {
|
||||||
@ -75,36 +89,36 @@ function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview,
|
|||||||
case 'SLICE': {
|
case 'SLICE': {
|
||||||
slicerWorker.terminate();
|
slicerWorker.terminate();
|
||||||
|
|
||||||
if (data.gcode.linePreview) {
|
const { gcode } = data;
|
||||||
const geometry = new BufferGeometry();
|
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
|
||||||
|
|
||||||
const { position, color } = data.gcode.linePreview;
|
resolve(gcode);
|
||||||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(position), 3));
|
|
||||||
geometry.addAttribute('color', new BufferAttribute(new Float32Array(color), 3));
|
|
||||||
|
|
||||||
const material = new LineBasicMaterial({ vertexColors: VertexColors });
|
|
||||||
const linePreview = new LineSegments(geometry, material);
|
|
||||||
|
|
||||||
data.gcode.linePreview = linePreview;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(data.gcode);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'PROGRESS': {
|
case 'PROGRESS': {
|
||||||
if (typeof onProgress !== 'undefined') {
|
if (typeof onProgress !== 'undefined') onProgress(data);
|
||||||
onProgress(data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// send geometry and settings to worker to start the slicing progress
|
const { vertices, faces, objectIndexes } = geometry;
|
||||||
geometry = geometry.toJSON();
|
const buffers = [vertices.buffer, faces.buffer, objectIndexes.buffer];
|
||||||
|
|
||||||
slicerWorker.postMessage({
|
slicerWorker.postMessage({
|
||||||
message: 'SLICE',
|
message: 'SLICE',
|
||||||
data: { settings, geometry, openObjectIndexes, constructLinePreview }
|
data: { settings, geometry, openObjectIndexes, constructLinePreview }
|
||||||
});
|
}, buffers);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function constructLineGeometry(linePreview) {
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
|
||||||
|
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(linePreview.positions), 3));
|
||||||
|
geometry.addAttribute('color', new THREE.BufferAttribute(new Float32Array(linePreview.colors), 3));
|
||||||
|
|
||||||
|
const material = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors });
|
||||||
|
const mesh = new THREE.LineSegments(geometry, material);
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'core-js'; // polyfills
|
import 'core-js'; // polyfills
|
||||||
import slice from './sliceActions/slice.js';
|
import slice from './sliceActions/slice.js';
|
||||||
import { Matrix4 } from 'three/src/math/Matrix4.js';
|
|
||||||
import { JSONLoader } from 'three/src/loaders/JSONLoader.js';
|
|
||||||
|
|
||||||
const onProgress = progress => {
|
const onProgress = progress => {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@ -10,23 +8,18 @@ const onProgress = progress => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = new JSONLoader();
|
self.addEventListener('message', (event) => {
|
||||||
|
|
||||||
self.addEventListener('message', async (event) => {
|
|
||||||
const { message, data } = event.data;
|
const { message, data } = event.data;
|
||||||
switch (message) {
|
switch (message) {
|
||||||
case 'SLICE': {
|
case 'SLICE': {
|
||||||
const { settings, geometry: JSONGeometry, constructLinePreview, openObjectIndexes } = data;
|
const { settings, geometry, constructLinePreview, openObjectIndexes } = data;
|
||||||
const { geometry } = loader.parse(JSONGeometry.data);
|
|
||||||
|
|
||||||
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
||||||
|
|
||||||
const buffers = [];
|
const buffers = [];
|
||||||
if (gcode.linePreview) {
|
if (gcode.linePreview) {
|
||||||
const position = gcode.linePreview.geometry.getAttribute('position').array;
|
buffers.push(gcode.linePreview.positions.buffer);
|
||||||
const color = gcode.linePreview.geometry.getAttribute('color').array;
|
buffers.push(gcode.linePreview.colors.buffer);
|
||||||
buffers.push(position.buffer, color.buffer);
|
|
||||||
gcode.linePreview = { position, color };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
const devMode = true;
|
const devMode = process.env.NODE_ENV !== 'production';
|
||||||
|
const analyzeBundle = process.env.ANALYZE_BUNDLE;
|
||||||
|
|
||||||
const babelLoader = {
|
const babelLoader = {
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
@ -34,6 +35,7 @@ module.exports = {
|
|||||||
'clipper-lib': '@doodle3d/clipper-lib',
|
'clipper-lib': '@doodle3d/clipper-lib',
|
||||||
'clipper-js': '@doodle3d/clipper-js',
|
'clipper-js': '@doodle3d/clipper-js',
|
||||||
'doodle3d-core': `@doodle3d/doodle3d-core/${devMode ? 'module' : 'lib'}`,
|
'doodle3d-core': `@doodle3d/doodle3d-core/${devMode ? 'module' : 'lib'}`,
|
||||||
|
'doodle3d-api': `@doodle3d/doodle3d-api/${devMode ? 'module' : 'lib'}`,
|
||||||
'cal': '@doodle3d/cal'
|
'cal': '@doodle3d/cal'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -51,7 +53,13 @@ module.exports = {
|
|||||||
use: 'yml-loader'
|
use: 'yml-loader'
|
||||||
}, {
|
}, {
|
||||||
test: /\.worker\.js$/,
|
test: /\.worker\.js$/,
|
||||||
use: ['worker-loader', babelLoader]
|
use: [{
|
||||||
|
loader: 'worker-loader',
|
||||||
|
options: {
|
||||||
|
inline: false,
|
||||||
|
name: '[name].js'
|
||||||
|
}
|
||||||
|
}, babelLoader],
|
||||||
}, {
|
}, {
|
||||||
test: /\.(png|jpg|gif)$/,
|
test: /\.(png|jpg|gif)$/,
|
||||||
use: ['url-loader?name=images/[name].[ext]']
|
use: ['url-loader?name=images/[name].[ext]']
|
||||||
@ -61,15 +69,16 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: analyzeBundle ? [new BundleAnalyzerPlugin()] : [
|
||||||
new HTMLWebpackPlugin({
|
new HTMLWebpackPlugin({
|
||||||
title: 'Doodle3D Slicer - Simple example',
|
favicon: 'favicon.ico',
|
||||||
|
title: 'Doodle3D Slicer',
|
||||||
template: require('html-webpack-template'),
|
template: require('html-webpack-template'),
|
||||||
inject: false,
|
inject: false,
|
||||||
appMountId: 'app'
|
appMountId: 'app'
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
devtool: "source-map",
|
devtool: devMode ? 'source-map' : false,
|
||||||
devServer: {
|
devServer: {
|
||||||
contentBase: 'dist'
|
contentBase: 'dist'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user