Compare commits

..

1 Commits
master ... RC

Author SHA1 Message Date
Simon Voordouw
b782872afa add low, medium and high slicer setting profiles 2017-05-19 12:02:51 +02:00
76 changed files with 188530 additions and 24769 deletions

View File

@ -1,26 +0,0 @@
{
"env": {
"module": {
"presets": [
["env", {
"targets": {
"node": "6",
"browsers": ["last 2 versions", "safari >= 7", "not ie < 11"]
},
"modules": false
}],
"stage-0",
"react"
]
},
"main": {
"presets": ["env", "stage-0", "react"]
}
},
"plugins": [
"transform-class-properties",
"transform-object-rest-spread",
"transform-runtime",
"transform-es2015-classes"
]
}

View File

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

8
.gitignore vendored
View File

@ -1,8 +1,8 @@
*.DS_Store
node_modules
jspm_packages/*
lib
module
dist
node_modules/*
bundle.js

View File

@ -1,2 +0,0 @@
node_modules
jspm_packages

136
DOCS.md
View File

@ -1,136 +0,0 @@
# 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

View File

@ -1,62 +1,22 @@
# Doodle3D-Slicer
JavaScript gcode slicer, Intended to use with the Doodle3D WiFi-Box
# Usage
```javascript
import * as THREE from 'three';
import { defaultSettings, sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
import THREE from 'three.js';
import * as SLICER from 'doodle3d-slicer';
const settings = {
...defaultSettings.default,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high
};
const settings = new SLICER.Settings({
...SLICER.printerSettings['ultimaker2go'],
...SLICER.userSettings
});
const geometry = new THREE.TorusGeometry(20, 10, 30, 30).clone();
const geometry = new THREE.TorusGeometry(20, 10, 30, 30);
const gcode = await sliceGeometry(settings, geometry);
const slicer = new SLICER.Slicer();
slicer.setGeometry(geometry);
slicer.slice(settings, false).then(gcode => {
document.getElementById('gcode').innerHTML = gcode.replace(/(?:\r\n|\r|\n)/g, '<br />');
});
```
# API
**Settings**
```javascript
import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer';
const settings = {
...defaultSettings.default,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high
};
```
Create settings object to be used by the slicer
**Slice Mesh**
```javascript
import { sliceMesh } from 'Doodle3D/Doodle3D-Slicer';
GCode: String = sliceMesh(settings: Object, mesh: THREE.Mesh, [sync: Boolean = false, onProgress: Func ])
```
Slice function that accepts Meshes
- Settings: settings object (see [settings](#settings))
- Mesh: THREE.Mesh instance that contains the geometry
- Sync: determines if the slicing progress will be sync (blocking) or async (non-blocking). A webworker is used to slice async
- onProgress: progress callback
**Slice Geometry**
```javascript
import { sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
GCode: String = sliceGeometry(settings: Object, geometry: THREE.Geometry | THREE.BufferGeometry, [matrix: THREE.Matrix, sync: Boolean = false, onProgress: Func ])
```
Slice function that accepts Geometry
- Settings: settings object (see [settings](#settings))
- Geometry: THREE.Geometry instance
- matrix: matrix that can control the scale, rotation and position of the model
- Sync: determines if the slicing progress will be sync (blocking) or async (non-blocking). A webworker is used to slice async
- onProgress: progress callback

97
comb.js
View File

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

Binary file not shown.

128
example/SlicerViewer.js Normal file
View File

@ -0,0 +1,128 @@
import React from 'react';
import { PRECISION } from '../src/constants.js';
export default class SlicerViewer extends React.Component {
state = {
layer: 0,
render: {
renderIntersectionPoints: false,
renderShape1: false,
renderShape2: true,
renderOuterLine: true,
renderInnerLine: true,
renderFill: true
}
};
changeSlider = (event) => {
this.setState({
layer: parseInt(event.target.value)
});
};
onControl = (event) => {
const section = event.target.value;
this.setState({
render: {
...this.state.render,
[section]: !this.state.render[section]
}
});
};
render() {
const { layer, render } = this.state;
const { layerIntersectionPoints, settings, layerShapes, slices } = this.props;
const numLayers = settings.dimensionsZ / settings.layerHeight;
const intersectionPoints = layerIntersectionPoints[layer + 1];
const shape = layerShapes[layer];
const slice = slices[layer];
return (
<div>
<svg viewBox={`-20 -20 ${settings.dimensionsX + 40} ${settings.dimensionsX + 40}`}>
<rect
width={settings.dimensionsX}
height={settings.dimensionsY}
fill="lightGrey"
/>
{render.renderIntersectionPoints && intersectionPoints.map(({ x, y }, i) => (
<circle key={i} cx={x} cy={y} r="0.3"/>
))}
{render.renderShape1 && shape && shape.closedShapes.map((closedShape, i) => (
<polygon
key={i}
points={closedShape.map(({ x, y }) => `${x} ${y}`).join(' ')}
fill="rgba(255, 0, 0, 0.5)"
/>
))}
{slice && slice.parts.map((slicePart, i) => (
<g key={i}>
{render.renderShape2 && <ClipperShapeSVG
shape={slicePart.shape}
scale={PRECISION}
color="rgba(0, 0, 0, 0.5)"
fill
/>}
{render.renderOuterLine && <ClipperShapeSVG
shape={slicePart.outerLine}
scale={1.0}
color="blue"
strokeWidth={settings.nozzleDiameter * 0.9}
/>}
{render.renderInnerLine && slicePart.innerLines.map((innerLine, i) => (
<ClipperShapeSVG
key={i}
shape={innerLine}
scale={1.0}
color="red"
strokeWidth={settings.nozzleDiameter * 0.9}
/>
))}
{render.renderFill && <ClipperShapeSVG
shape={slicePart.fill}
scale={1.0}
color="yellow"
strokeWidth={settings.nozzleDiameter * 0.9}
/>}
</g>
))}
</svg>
<div id="controls">
<input onChange={this.changeSlider} value={layer} type="range" min="0" max={numLayers} />
<p>Layer: {layer}</p>
<p><label><input type="checkbox" value="renderIntersectionPoints" onChange={this.onControl} checked={render.renderIntersectionPoints} />Render Intersection Points</label></p>
<p><label><input type="checkbox" value="renderShape1" onChange={this.onControl} checked={render.renderShape1} />Render Shape 1</label></p>
<p><label><input type="checkbox" value="renderShape2" onChange={this.onControl} checked={render.renderShape2} />Render Shape 2</label></p>
<p><label><input type="checkbox" value="renderOuterLine" onChange={this.onControl} checked={render.renderOuterLine} />Render Out Line</label></p>
<p><label><input type="checkbox" value="renderInnerLine" onChange={this.onControl} checked={render.renderInnerLine} />Render Inner Lines</label></p>
<p><label><input type="checkbox" value="renderFill" onChange={this.onControl} checked={render.renderFill} />Render Fill</label></p>
</div>
</div>
);
}
}
class ClipperShapeSVG extends React.Component {
render() {
const { shape, color = 'black', strokeWidth, scale, fill } = this.props;
const data = shape.paths.map(path => {
const pathData = path.map(({ X, Y }, i) => `${i === 0 ? 'M' : 'L '}${X * scale} ${Y * scale}`);
if (shape.closed) pathData.push('Z');
return pathData.join(' ');
}).join(' ');
return (
<path
d={data}
strokeWidth={typeof strokeWidth === 'number' ? strokeWidth : 1.0}
vectorEffect={typeof strokeWidth === 'number' ? 'none' : 'non-scaling-stroke'}
fill={fill ? color : 'none'}
stroke={!fill ? color : 'none'}
/>
);
}
}

View File

@ -0,0 +1,54 @@
import calculateLayersIntersections from 'src/sliceActions/calculateLayersIntersections.js';
import createLines from 'src/sliceActions/createLines.js';
import generateInfills from 'src/sliceActions/generateInfills.js';
import generateInnerLines from 'src/sliceActions/generateInnerLines.js';
import generateSupport from 'src/sliceActions/generateSupport.js';
import intersectionsToShapes from 'src/sliceActions/intersectionsToShapes.js';
import addBrim from 'src/sliceActions/addBrim.js';
import optimizePaths from 'src/sliceActions/optimizePaths.js';
import shapesToSlices from 'src/sliceActions/shapesToSlices.js';
import slicesToGCode from 'src/sliceActions/slicesToGCode.js';
import applyPrecision from 'src/sliceActions/applyPrecision.js';
import removePrecision from 'src/sliceActions/removePrecision.js';
export default function generateRawData(geometry, settings) {
const rawData = {};
const lines = createLines(geometry, settings);
const {
layerIntersectionIndexes,
layerIntersectionPoints
} = calculateLayersIntersections(lines, settings);
rawData.layerIntersectionPoints = layerIntersectionPoints
.map(intersectionPoints => intersectionPoints.map(intersectionPoint => intersectionPoint.clone()));
const layerShapes = intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings);
rawData.layerShapes = layerShapes
.map(({ closedShapes, openShapes }) => ({
closedShapes: closedShapes.map(closedShape => closedShape.map(vector => vector.clone())),
openShapes: openShapes.map(openShape => openShape.map(vector => vector.clone()))
}));
applyPrecision(layerShapes);
const slices = shapesToSlices(layerShapes, settings);
generateInnerLines(slices, settings);
generateInfills(slices, settings);
generateSupport(slices, settings);
addBrim(slices, settings);
optimizePaths(slices, settings);
removePrecision(slices);
rawData.slices = slices;
const gcode = slicesToGCode(slices, settings);
rawData.gcode = gcode;
return rawData;
}

11
example/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE>
<html>
<head>
<title>Doodle3D Slicer</title>
</head>
<body>
<p><a href="./viewer.html">Viewer</a></p>
<p><a href="./save.html">Save</a></p>
</body>
</html>

21
example/main.css Normal file
View File

@ -0,0 +1,21 @@
* {
margin: 0;
padding: 0;
}
#container {
position: relative;
}
#container, svg {
width: 100%;
height: 100%;
}
svg, #controls {
position: absolute;
}
input[type=range] {
width: 100%;
}

19
example/save.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE>
<html>
<head>
<title>Doodle3D Slicer - Save</title>
<script type="text/javascript" src="../jspm_packages/system.js"></script>
<script type="text/javascript" src="../jspm.config.js"></script>
<script type="text/javascript">
System.import('example/save.js');
</script>
</head>
<body>
</body>
</html>

25
example/save.js Normal file
View File

@ -0,0 +1,25 @@
import 'three.js';
import 'three.js/loaders/STLLoader';
import { Settings, printerSettings, userSettings, Slicer } from 'src/index.js';
import { saveAs } from 'file-saver';
const settings = new Settings({
...printerSettings['ultimaker2go'],
...userSettings
});
const stlLoader = new THREE.STLLoader();
stlLoader.load('stl/traktor.stl', async (geometry) => {
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.1, 50)));
geometry.mergeVertices();
geometry.computeFaceNormals();
const slicer = new Slicer().setGeometry(geometry);
const gcode = await slicer.slice(settings);
const file = new File([gcode], 'traktor.gcode', { type: 'text/plain' });
saveAs(file);
});

55582
example/stl/Airplane.stl Normal file

File diff suppressed because it is too large Load Diff

94222
example/stl/Rocket.stl Normal file

File diff suppressed because it is too large Load Diff

BIN
example/stl/castle.stl Normal file

Binary file not shown.

36906
example/stl/traktor.stl Normal file

File diff suppressed because it is too large Load Diff

29
example/viewer.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE>
<html>
<head>
<title>Doodle3D Slicer - Viewer</title>
<style>
#gcode {
font-family: monospace;
}
</style>
<script type="text/javascript" src="../jspm_packages/system.js"></script>
<script type="text/javascript" src="../jspm.config.js"></script>
<link href="main.css" rel="stylesheet"/>
<script type="text/javascript">
System.import('example/viewer.js');
</script>
</head>
<body>
<div id="container"></div>
</body>
</html>

35
example/viewer.js Normal file
View File

@ -0,0 +1,35 @@
import 'three.js';
import 'three.js/loaders/STLLoader';
import React from 'react';
import ReactDOM, { render } from 'react-dom';
import * as SLICER from 'src/index.js';
import generateRawData from './generateRawData.js';
import SlicerViewer from './SlicerViewer.js';
const settings = new SLICER.Settings({
...SLICER.printerSettings['ultimaker2go'],
...SLICER.userSettings
});
const stlLoader = new THREE.STLLoader();
stlLoader.load('stl/Airplane.stl', (geometry) => {
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.1, 50)));
// geometry.applyMatrix(new THREE.Matrix4().scale(0.8));
geometry.mergeVertices();
geometry.computeFaceNormals();
const rawData = generateRawData(geometry, settings);
render(
<SlicerViewer
layerIntersectionPoints={rawData.layerIntersectionPoints}
layerShapes={rawData.layerShapes}
slices={rawData.slices}
settings={settings.config}
/>,
document.getElementById('container')
);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,47 +0,0 @@
import 'babel-polyfill';
import React from 'react';
import { Interface } from './src/index.js';
import { render } from 'react-dom';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import jss from 'jss';
import preset from 'jss-preset-default';
import normalize from 'normalize-jss';
import queryString from 'query-string';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import { grey400, blue500, blue700 } from 'material-ui/styles/colors';
import bunny_url from './data/bunny.stl';
import * as THREE from 'three';
import 'three/examples/js/loaders/STLLoader.js';
import fileSaver from 'file-saver';
const muiTheme = getMuiTheme({
palette: {
primary1Color: blue500,
primary2Color: blue700,
accent1Color: blue500,
}
});
jss.setup(preset());
jss.createStyleSheet(normalize).attach();
jss.createStyleSheet({
'@global': {
'*': { margin: 0, padding: 0 },
'#app, body, html': { height: '100%', fontFamily: 'sans-serif' },
body: { overflow: 'auto' },
html: { overflow: 'hidden' }
}
}).attach();
new THREE.STLLoader().load(bunny_url, geometry => {
const material = new THREE.MeshPhongMaterial({ color: 0xff5533, specular: 0x111111, shininess: 200 });
const mesh = new THREE.Mesh(geometry, material);
render((
<MuiThemeProvider muiTheme={muiTheme}>
<Interface
mesh={mesh}
onSliceSucces={({ gcode }) => fileSaver.saveAs(gcode, 'bunny.gcode')}
/>
</MuiThemeProvider>
), document.getElementById('app'));
});

426
jspm.config.js Normal file
View File

@ -0,0 +1,426 @@
SystemJS.config({
paths: {
"github:": "jspm_packages/github/",
"npm:": "jspm_packages/npm/",
"example/": "example/",
"slicer/": "src/"
},
browserConfig: {
"baseURL": "/"
},
devConfig: {
"map": {
"babel-runtime": "npm:babel-runtime@5.8.38",
"core-js": "npm:core-js@1.2.7",
"plugin-babel": "npm:systemjs-plugin-babel@0.0.12",
"react": "npm:react@15.3.2",
"domain": "npm:jspm-nodelibs-domain@0.2.0",
"zlib": "npm:jspm-nodelibs-zlib@0.2.0",
"https": "npm:jspm-nodelibs-https@0.2.0",
"react-dom": "npm:react-dom@15.3.2",
"babel-plugin-transform-react-jsx": "npm:babel-plugin-transform-react-jsx@6.8.0",
"three.js/loaders/STLLoader": "github:mrdoob/three.js@r83/examples/js/loaders/STLLoader.js",
"file-saver": "npm:file-saver@1.3.3"
},
"packages": {
"npm:babel-runtime@5.8.38": {
"map": {}
},
"npm:react@15.3.2": {
"map": {
"object-assign": "npm:object-assign@4.1.0",
"loose-envify": "npm:loose-envify@1.2.0",
"fbjs": "npm:fbjs@0.8.5"
}
},
"npm:fbjs@0.8.5": {
"map": {
"loose-envify": "npm:loose-envify@1.2.0",
"object-assign": "npm:object-assign@4.1.0",
"promise": "npm:promise@7.1.1",
"isomorphic-fetch": "npm:isomorphic-fetch@2.2.1",
"ua-parser-js": "npm:ua-parser-js@0.7.10",
"immutable": "npm:immutable@3.8.1",
"core-js": "npm:core-js@1.2.7"
}
},
"npm:loose-envify@1.2.0": {
"map": {
"js-tokens": "npm:js-tokens@1.0.3"
}
},
"npm:promise@7.1.1": {
"map": {
"asap": "npm:asap@2.0.5"
}
},
"npm:isomorphic-fetch@2.2.1": {
"map": {
"whatwg-fetch": "npm:whatwg-fetch@1.0.0",
"node-fetch": "npm:node-fetch@1.6.3"
}
},
"npm:node-fetch@1.6.3": {
"map": {
"encoding": "npm:encoding@0.1.12",
"is-stream": "npm:is-stream@1.1.0"
}
},
"npm:encoding@0.1.12": {
"map": {
"iconv-lite": "npm:iconv-lite@0.4.13"
}
},
"npm:browserify-zlib@0.1.4": {
"map": {
"readable-stream": "npm:readable-stream@2.1.5",
"pako": "npm:pako@0.2.9"
}
},
"npm:babel-plugin-transform-react-jsx@6.8.0": {
"map": {
"babel-helper-builder-react-jsx": "npm:babel-helper-builder-react-jsx@6.9.0",
"babel-plugin-syntax-jsx": "npm:babel-plugin-syntax-jsx@6.13.0",
"babel-runtime": "npm:babel-runtime@6.11.6"
}
},
"npm:babel-helper-builder-react-jsx@6.9.0": {
"map": {
"babel-runtime": "npm:babel-runtime@6.11.6",
"esutils": "npm:esutils@2.0.2",
"babel-types": "npm:babel-types@6.16.0",
"lodash": "npm:lodash@4.16.4"
}
},
"npm:babel-runtime@6.11.6": {
"map": {
"core-js": "npm:core-js@2.4.1",
"regenerator-runtime": "npm:regenerator-runtime@0.9.5"
}
},
"npm:babel-types@6.16.0": {
"map": {
"lodash": "npm:lodash@4.16.4",
"babel-runtime": "npm:babel-runtime@6.11.6",
"esutils": "npm:esutils@2.0.2",
"to-fast-properties": "npm:to-fast-properties@1.0.2"
}
},
"npm:jspm-nodelibs-zlib@0.2.0": {
"map": {
"zlib-browserify": "npm:browserify-zlib@0.1.4"
}
},
"npm:jspm-nodelibs-domain@0.2.0": {
"map": {
"domain-browserify": "npm:domain-browser@1.1.7"
}
}
}
},
transpiler: "plugin-babel",
packages: {
"slicer": {
"main": "index.js"
},
"example": {
"main": "example/index.js",
"format": "esm",
"meta": {
"*.js": {
"loader": "plugin-babel",
"babelOptions": {
"stage1": true,
"plugins": [
"babel-plugin-transform-react-jsx"
]
}
}
}
}
},
meta: {
"three.js/loaders/STLLoader": {
"deps": [
"three.js"
]
}
},
map: {
"babel": "npm:babel-core@5.8.38",
"three.js/loaders/STLLoader": "github:mrdoob/three.js@r75/examples/js/loaders/STLLoader.js"
}
});
SystemJS.config({
packageConfigPaths: [
"npm:@*/*.json",
"npm:*.json",
"github:*/*.json"
],
map: {
"clipper-js": "github:Doodle3D/clipper-js@1.0.2",
"three.js": "github:mrdoob/three.js@r83",
"assert": "npm:jspm-nodelibs-assert@0.2.0",
"buffer": "npm:jspm-nodelibs-buffer@0.2.0",
"child_process": "npm:jspm-nodelibs-child_process@0.2.0",
"constants": "npm:jspm-nodelibs-constants@0.2.0",
"crypto": "npm:jspm-nodelibs-crypto@0.2.0",
"events": "npm:jspm-nodelibs-events@0.2.0",
"fs": "npm:jspm-nodelibs-fs@0.2.0",
"http": "npm:jspm-nodelibs-http@0.2.0",
"json": "github:systemjs/plugin-json@0.1.2",
"Doodle3D/clipper-js": "github:Doodle3D/clipper-js@master",
"module": "npm:jspm-nodelibs-module@0.2.0",
"os": "npm:jspm-nodelibs-os@0.2.0",
"path": "npm:jspm-nodelibs-path@0.2.0",
"process": "npm:jspm-nodelibs-process@0.2.0",
"stream": "npm:jspm-nodelibs-stream@0.2.0",
"string_decoder": "npm:jspm-nodelibs-string_decoder@0.2.0",
"tty": "npm:jspm-nodelibs-tty@0.2.0",
"url": "npm:jspm-nodelibs-url@0.2.0",
"util": "npm:jspm-nodelibs-util@0.2.0",
"vm": "npm:jspm-nodelibs-vm@0.2.0",
"worker": "github:casperlamboo/plugin-worker@master"
},
packages: {
"github:Doodle3D/clipper-js@master": {
"map": {
"clipper-lib": "npm:clipper-lib@1.0.0"
}
},
"npm:clipper-lib@1.0.0": {
"map": {}
},
"npm:stream-browserify@2.0.1": {
"map": {
"inherits": "npm:inherits@2.0.3",
"readable-stream": "npm:readable-stream@2.1.5"
}
},
"npm:buffer@4.9.1": {
"map": {
"base64-js": "npm:base64-js@1.2.0",
"isarray": "npm:isarray@1.0.0",
"ieee754": "npm:ieee754@1.1.8"
}
},
"npm:url@0.11.0": {
"map": {
"querystring": "npm:querystring@0.2.0",
"punycode": "npm:punycode@1.3.2"
}
},
"npm:readable-stream@2.1.5": {
"map": {
"string_decoder": "npm:string_decoder@0.10.31",
"inherits": "npm:inherits@2.0.3",
"isarray": "npm:isarray@1.0.0",
"buffer-shims": "npm:buffer-shims@1.0.0",
"core-util-is": "npm:core-util-is@1.0.2",
"process-nextick-args": "npm:process-nextick-args@1.0.7",
"util-deprecate": "npm:util-deprecate@1.0.2"
}
},
"npm:crypto-browserify@3.11.0": {
"map": {
"inherits": "npm:inherits@2.0.3",
"browserify-cipher": "npm:browserify-cipher@1.0.0",
"create-hash": "npm:create-hash@1.1.2",
"browserify-sign": "npm:browserify-sign@4.0.0",
"pbkdf2": "npm:pbkdf2@3.0.9",
"public-encrypt": "npm:public-encrypt@4.0.0",
"randombytes": "npm:randombytes@2.0.3",
"diffie-hellman": "npm:diffie-hellman@5.0.2",
"create-ecdh": "npm:create-ecdh@4.0.0",
"create-hmac": "npm:create-hmac@1.1.4"
}
},
"npm:browserify-sign@4.0.0": {
"map": {
"create-hash": "npm:create-hash@1.1.2",
"inherits": "npm:inherits@2.0.3",
"create-hmac": "npm:create-hmac@1.1.4",
"parse-asn1": "npm:parse-asn1@5.0.0",
"elliptic": "npm:elliptic@6.3.2",
"bn.js": "npm:bn.js@4.11.6",
"browserify-rsa": "npm:browserify-rsa@4.0.1"
}
},
"npm:create-hash@1.1.2": {
"map": {
"inherits": "npm:inherits@2.0.3",
"ripemd160": "npm:ripemd160@1.0.1",
"cipher-base": "npm:cipher-base@1.0.3",
"sha.js": "npm:sha.js@2.4.5"
}
},
"npm:public-encrypt@4.0.0": {
"map": {
"randombytes": "npm:randombytes@2.0.3",
"create-hash": "npm:create-hash@1.1.2",
"parse-asn1": "npm:parse-asn1@5.0.0",
"bn.js": "npm:bn.js@4.11.6",
"browserify-rsa": "npm:browserify-rsa@4.0.1"
}
},
"npm:diffie-hellman@5.0.2": {
"map": {
"randombytes": "npm:randombytes@2.0.3",
"bn.js": "npm:bn.js@4.11.6",
"miller-rabin": "npm:miller-rabin@4.0.0"
}
},
"npm:create-hmac@1.1.4": {
"map": {
"create-hash": "npm:create-hash@1.1.2",
"inherits": "npm:inherits@2.0.3"
}
},
"npm:browserify-cipher@1.0.0": {
"map": {
"browserify-des": "npm:browserify-des@1.0.0",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0",
"browserify-aes": "npm:browserify-aes@1.0.6"
}
},
"npm:create-ecdh@4.0.0": {
"map": {
"elliptic": "npm:elliptic@6.3.2",
"bn.js": "npm:bn.js@4.11.6"
}
},
"npm:parse-asn1@5.0.0": {
"map": {
"create-hash": "npm:create-hash@1.1.2",
"pbkdf2": "npm:pbkdf2@3.0.9",
"browserify-aes": "npm:browserify-aes@1.0.6",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0",
"asn1.js": "npm:asn1.js@4.8.1"
}
},
"npm:browserify-des@1.0.0": {
"map": {
"inherits": "npm:inherits@2.0.3",
"cipher-base": "npm:cipher-base@1.0.3",
"des.js": "npm:des.js@1.0.0"
}
},
"npm:evp_bytestokey@1.0.0": {
"map": {
"create-hash": "npm:create-hash@1.1.2"
}
},
"npm:browserify-aes@1.0.6": {
"map": {
"create-hash": "npm:create-hash@1.1.2",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0",
"inherits": "npm:inherits@2.0.3",
"cipher-base": "npm:cipher-base@1.0.3",
"buffer-xor": "npm:buffer-xor@1.0.3"
}
},
"npm:sha.js@2.4.5": {
"map": {
"inherits": "npm:inherits@2.0.3"
}
},
"npm:browserify-rsa@4.0.1": {
"map": {
"bn.js": "npm:bn.js@4.11.6",
"randombytes": "npm:randombytes@2.0.3"
}
},
"npm:miller-rabin@4.0.0": {
"map": {
"bn.js": "npm:bn.js@4.11.6",
"brorand": "npm:brorand@1.0.6"
}
},
"npm:des.js@1.0.0": {
"map": {
"inherits": "npm:inherits@2.0.3",
"minimalistic-assert": "npm:minimalistic-assert@1.0.0"
}
},
"npm:hash.js@1.0.3": {
"map": {
"inherits": "npm:inherits@2.0.3"
}
},
"npm:pbkdf2@3.0.9": {
"map": {
"create-hmac": "npm:create-hmac@1.1.4"
}
},
"npm:elliptic@6.3.2": {
"map": {
"bn.js": "npm:bn.js@4.11.6",
"inherits": "npm:inherits@2.0.3",
"brorand": "npm:brorand@1.0.6",
"hash.js": "npm:hash.js@1.0.3"
}
},
"npm:cipher-base@1.0.3": {
"map": {
"inherits": "npm:inherits@2.0.3"
}
},
"npm:asn1.js@4.8.1": {
"map": {
"bn.js": "npm:bn.js@4.11.6",
"inherits": "npm:inherits@2.0.3",
"minimalistic-assert": "npm:minimalistic-assert@1.0.0"
}
},
"npm:stream-http@2.4.0": {
"map": {
"inherits": "npm:inherits@2.0.3",
"readable-stream": "npm:readable-stream@2.1.5",
"to-arraybuffer": "npm:to-arraybuffer@1.0.1",
"builtin-status-codes": "npm:builtin-status-codes@2.0.0",
"xtend": "npm:xtend@4.0.1"
}
},
"npm:jspm-nodelibs-crypto@0.2.0": {
"map": {
"crypto-browserify": "npm:crypto-browserify@3.11.0"
}
},
"npm:jspm-nodelibs-http@0.2.0": {
"map": {
"http-browserify": "npm:stream-http@2.4.0"
}
},
"npm:jspm-nodelibs-os@0.2.0": {
"map": {
"os-browserify": "npm:os-browserify@0.2.1"
}
},
"npm:jspm-nodelibs-buffer@0.2.0": {
"map": {
"buffer-browserify": "npm:buffer@4.9.1"
}
},
"npm:jspm-nodelibs-stream@0.2.0": {
"map": {
"stream-browserify": "npm:stream-browserify@2.0.1"
}
},
"npm:jspm-nodelibs-string_decoder@0.2.0": {
"map": {
"string_decoder-browserify": "npm:string_decoder@0.10.31"
}
},
"npm:jspm-nodelibs-url@0.2.0": {
"map": {
"url-browserify": "npm:url@0.11.0"
}
},
"github:Doodle3D/clipper-js@1.0.2": {
"map": {
"Breush/clipper-lib": "github:Breush/clipper-lib@patch-1"
}
}
}
});

View File

@ -1 +0,0 @@
{"data":"{\"spaces\":[{\"matrix\":{\"metadata\":{\"type\":\"Matrix4\",\"library\":\"three.js\"},\"elements\":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},\"objects\":[{\"height\":9.266873708001008,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,26.586102719033242,0,1,-4.229607250755304]},\"z\":10.733126291998994,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":true,\"solid\":false,\"star\":{\"rays\":5,\"innerRadius\":20.54380664652568,\"outerRadius\":40.48338368580059},\"color\":6873597,\"type\":\"STAR\"},{\"height\":20,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,-12.688821752265852,0,1,-12.68882175226588]},\"z\":0,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":true,\"solid\":true,\"star\":{\"rays\":5,\"innerRadius\":20.54380664652568,\"outerRadius\":40.48338368580059},\"color\":6873597,\"type\":\"STAR\"}]}]}","appVersion":"0.17.4"}

View File

@ -1 +0,0 @@
{"vertices":[47.12482452392578,42.35624313354492,-2.593571992041396e-15,-7.293127536773682,29.7335205078125,-1.820653115308456e-15,-4.207573413848877,-19.915847778320312,1.219494014914264e-15,54.97896194458008,-14.586255073547363,8.931505683412107e-16,53.85694122314453,-42.07573699951172,2.576395944332666e-15,-35.624122619628906,-25.245441436767578,1.5458375198830296e-15,-38.99018096923828,51.6129035949707,-3.1603789970181283e-15,47.12482452392578,61.71107864379883,-3.7787139115382e-15,47.12482452392578,42.35624313354492,19.999999999999996,-7.293127536773682,29.7335205078125,19.999999999999996,-4.207573413848877,-19.915847778320312,20,54.97896194458008,-14.586255073547362,20,53.85694122314453,-42.07573699951172,20.000000000000004,-35.624122619628906,-25.245441436767578,20,-38.99018096923828,51.6129035949707,19.999999999999996,47.12482452392578,61.71107864379883,19.999999999999996],"normals":[0,-6.123234262925839e-17,-1,0,6.123234262925839e-17,1,0.22595957126202507,-0.9741366804278966,5.964867098368936e-17,0.22595959003447755,-0.9741366760734609,5.964867071705706e-17,0.9980744575480833,0.062027229424660574,-3.798072564474434e-18,0.9980744575147102,0.06202722996166134,-3.798072597356249e-18,-0.08968451372047671,0.995970224453885,-6.098559003229967e-17,-0.0896845139633491,0.9959702244320148,-6.098559003096051e-17,0.9991680515439634,-0.04078240765133586,2.4972023585526864e-18,0.9991680513997995,-0.040782411183346925,2.4972025748259983e-18,-0.18484654771165182,-0.9827673955718536,6.017714989051966e-17,-0.1848465343760269,-0.9827673980801217,6.017715004410679e-17,-0.9990423495327988,-0.04375367230285246,2.679139853736555e-18,-0.9990423494957035,-0.04375367314986121,2.6791399056008846e-18,-0.11646581097902166,0.993194701391927,-6.081563825319445e-17,-0.11646581460851489,0.9931947009663187,-6.081563822713346e-17,1,0,0],"faces":[50,7,0,1,0,0,0,0,0,50,2,3,4,0,0,0,0,0,50,6,7,1,0,0,0,0,0,50,2,4,5,0,0,0,0,0,50,5,6,1,0,0,0,0,0,50,1,2,5,0,0,0,0,0,50,13,10,9,0,1,1,1,1,50,9,14,13,0,1,1,1,1,50,13,12,10,0,1,1,1,1,50,9,15,14,0,1,1,1,1,50,12,11,10,0,1,1,1,1,50,9,8,15,0,1,1,1,1,50,0,8,1,0,2,3,3,3,50,8,9,1,0,2,3,3,3,50,1,9,2,0,4,5,5,5,50,9,10,2,0,4,5,5,5,50,2,10,3,0,6,7,7,7,50,10,11,3,0,6,7,7,7,50,3,11,4,0,8,9,9,9,50,11,12,4,0,8,9,9,9,50,4,12,5,0,10,11,11,11,50,12,13,5,0,10,11,11,11,50,5,13,6,0,12,13,13,13,50,13,14,6,0,12,13,13,13,50,6,14,7,0,14,15,15,15,50,14,15,7,0,14,15,15,15,50,7,15,0,0,16,16,16,16,50,15,8,0,0,16,16,16,16]}

View File

@ -1 +0,0 @@
{"data":"{\"spaces\":[{\"matrix\":{\"metadata\":{\"type\":\"Matrix4\",\"library\":\"three.js\"},\"elements\":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},\"objects\":[{\"height\":20,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,-32.27848101265822,0,1,5.3797468354430436]},\"z\":0,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":false,\"solid\":true,\"star\":{\"rays\":5,\"innerRadius\":10,\"outerRadius\":25},\"color\":6873597,\"type\":\"STAR\"},{\"height\":20,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,47.784810126582286,0,1,0.6329113924050631]},\"z\":0,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":true,\"solid\":true,\"star\":{\"rays\":5,\"innerRadius\":22.468354430379748,\"outerRadius\":25.9493670886076},\"color\":6873597,\"type\":\"STAR\"},{\"height\":20,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,-46.83544303797467,0,1,9.810126582278485]},\"z\":0,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":false,\"solid\":false,\"rectSize\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Vector\"},\"x\":120.8860759493671,\"y\":34.49367088607595},\"color\":6873597,\"type\":\"RECT\"},{\"height\":20,\"transform\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Matrix\"},\"matrix\":[1,0,-47.1518987341772,0,1,-37.341772151898724]},\"z\":0,\"sculpt\":[{\"pos\":0,\"scale\":1},{\"pos\":1,\"scale\":1}],\"twist\":0,\"fill\":true,\"solid\":false,\"rectSize\":{\"metadata\":{\"library\":\"CAL\",\"type\":\"Vector\"},\"x\":120.8860759493671,\"y\":34.49367088607595},\"color\":6873597,\"type\":\"RECT\"}]}]}","appVersion":"0.17.4"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

21158
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,81 +1,84 @@
{
"name": "@doodle3d/doodle3d-slicer",
"version": "0.0.18",
"description": "JavaScript gcode slicer for Doodle3D Transform",
"main": "lib/index.js",
"module": "module/index.js",
"esnext": "src/index.js",
"scripts": {
"start": "webpack-dev-server -w",
"dist": "NODE_ENV=production webpack -p",
"lint": "eslint src",
"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:main": "BABEL_ENV=main babel src -s -d lib",
"build:module": "BABEL_ENV=module babel src -s -d module",
"build:main:settings": "cp -r src/settings lib",
"build:module:settings": "cp -r src/settings module"
},
"dependencies": {
"@doodle3d/clipper-js": "^1.0.10",
"lodash": "^4.17.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",
"react": "^16.0.0",
"react-addons-update": "^15.6.2",
"react-dom": "^16.0.0",
"react-jss": "^7.2.0",
"react-resize-detector": "^1.1.0",
"shortid": "^2.2.8",
"three": "^0.88.0",
"validate-ip": "^1.0.1"
"jspm": {
"name": "slicer",
"main": "index.js",
"directories": {
"lib": "src"
},
"dependencies": {
"Doodle3D/clipper-js": "github:Doodle3D/clipper-js@master",
"clipper-js": "github:Doodle3D/clipper-js@1.0.2",
"json": "github:systemjs/plugin-json@^0.1.2",
"three.js": "github:mrdoob/three.js@r83",
"worker": "github:casperlamboo/plugin-worker@master"
},
"devDependencies": {
"babel-plugin-transform-react-jsx": "npm:babel-plugin-transform-react-jsx@^6.8.0",
"babel-runtime": "npm:babel-runtime@^5.1.13",
"core-js": "npm:core-js@^1.2.0",
"file-saver": "npm:file-saver@^1.3.3",
"domain": "npm:jspm-nodelibs-domain@^0.2.0",
"https": "npm:jspm-nodelibs-https@^0.2.0",
"plugin-babel": "npm:systemjs-plugin-babel@^0.0.12",
"react": "npm:react@^15.3.2",
"react-dom": "npm:react-dom@^15.3.2",
"zlib": "npm:jspm-nodelibs-zlib@^0.2.0"
},
"peerDependencies": {
"assert": "npm:jspm-nodelibs-assert@^0.2.0",
"buffer": "npm:jspm-nodelibs-buffer@^0.2.0",
"child_process": "npm:jspm-nodelibs-child_process@^0.2.0",
"constants": "npm:jspm-nodelibs-constants@^0.2.0",
"crypto": "npm:jspm-nodelibs-crypto@^0.2.0",
"events": "npm:jspm-nodelibs-events@^0.2.0",
"fs": "npm:jspm-nodelibs-fs@^0.2.0",
"http": "npm:jspm-nodelibs-http@^0.2.0",
"module": "npm:jspm-nodelibs-module@^0.2.0",
"os": "npm:jspm-nodelibs-os@^0.2.0",
"path": "npm:jspm-nodelibs-path@^0.2.0",
"process": "npm:jspm-nodelibs-process@^0.2.0",
"stream": "npm:jspm-nodelibs-stream@^0.2.0",
"string_decoder": "npm:jspm-nodelibs-string_decoder@^0.2.0",
"tty": "npm:jspm-nodelibs-tty@^0.2.0",
"url": "npm:jspm-nodelibs-url@^0.2.0",
"util": "npm:jspm-nodelibs-util@^0.2.0",
"vm": "npm:jspm-nodelibs-vm@^0.2.0"
},
"overrides": {
"github:mrdoob/three.js@r83": {
"format": "global"
},
"npm:babel-runtime@5.8.38": {
"main": false,
"dependencies": {},
"optionalDependencies": {
"core-js": "^1.2.0"
}
},
"npm:browserify-zlib@0.1.4": {
"dependencies": {
"readable-stream": "^2.0.2",
"pako": "~0.2.0"
},
"map": {
"_stream_transform": "readable-stream/transform"
}
},
"npm:inherits@2.0.3": {
"ignore": [
"test.js"
]
},
"npm:lodash@4.16.4": {
"map": {
"buffer": "@empty",
"process": "@empty"
}
}
}
},
"dependencies": {},
"devDependencies": {
"file-saver": "^1.3.3",
"babel-cli": "6.24.1",
"babel-eslint": "^5.0.4",
"babel-loader": "7.0.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"eslint": "^1.10.3",
"eslint-config-airbnb": "^3.1.0",
"eslint-plugin-react": "^3.16.1",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^2.29.0",
"html-webpack-template": "^6.0.2",
"query-string": "^5.0.1",
"image-webpack-loader": "^4.2.0",
"imports-loader": "^0.7.1",
"material-ui": "^0.19.4",
"normalize-jss": "^4.0.0",
"raw-loader": "^0.5.1",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.2",
"webpack-dev-server": "^2.5.1",
"worker-loader": "^0.8.1",
"yml-loader": "^2.1.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git"
},
"author": "Casper @Doodle3D",
"license": "MIT",
"private": false,
"bugs": {
"url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues"
},
"homepage": "https://github.com/Doodle3D/Doodle3D-Slicer#readme"
"jspm": "^0.17.0-beta.28"
}
}

187
src/GCode.js Normal file
View File

@ -0,0 +1,187 @@
import * as THREE from 'three.js';
const G_COMMAND = 'G';
const M_COMMAND = 'M';
const FAN_SPEED = 'S';
const SPEED = 'F';
const EXTRUDER = 'E';
const POSITION_X = 'X';
const POSITION_Y = 'Y';
const POSITION_Z = 'Z';
export default class {
constructor(settings) {
this.gcode = '';
this.current = {};
this.extruder = 0.0;
this.bottom = true;
this.isRetracted = false;
this.isFanOn = false;
this._nozzlePosition = new THREE.Vector2(0, 0);
if (settings !== undefined) {
this.setSettings(settings);
}
}
_addGCode(command) {
let str = '';
let first = true;
for (const action in command) {
const value = command[action];
const currentValue = this.current[action];
if (first) {
str = action + value;
first = false;
} else if (currentValue !== value) {
str += ` ${action}${value}`;
this.current[action] = value;
}
}
this.gcode += `${str}\n`;
}
setSettings(settings) {
this.settings = settings;
return this;
}
turnFanOn(fanSpeed) {
this.isFanOn = true;
const gcode = { [M_COMMAND]: 106 }
if (fanSpeed !== undefined) gcode[FAN_SPEED] = fanSpeed;
this._addGCode(gcode);
return this;
}
turnFanOff() {
this.isFanOn = false;
this._addGCode({ [M_COMMAND]: 107 });
return this;
}
moveTo(x, y, layer) {
const {
layerHeight,
travelSpeed
} = this.settings.config;
const z = layer * layerHeight + 0.2;
const speed = travelSpeed * 60;
this._addGCode({
[G_COMMAND]: 0,
[POSITION_X]: x.toFixed(3),
[POSITION_Y]: y.toFixed(3),
[POSITION_Z]: z.toFixed(3),
[SPEED]: speed.toFixed(3)
});
this._nozzlePosition.set(x, y);
return this;
}
lineTo(x, y, layer, type) {
const newNozzlePosition = new THREE.Vector2(x, y);
const {
layerHeight,
nozzleDiameter,
filamentThickness,
travelSpeed
} = this.settings.config;
const profile = this.settings.config[(this.bottom ? 'bottom' : type)];
let {
speed,
flowRate
} = profile;
speed *= 60;
const z = layer * layerHeight + 0.2;
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
this.extruder += lineLength * nozzleDiameter * layerHeight / filamentSurfaceArea * flowRate;
this._addGCode({
[G_COMMAND]: 1,
[POSITION_X]: x.toFixed(3),
[POSITION_Y]: y.toFixed(3),
[POSITION_Z]: z.toFixed(3),
[SPEED]: speed.toFixed(3),
[EXTRUDER]: this.extruder.toFixed(3)
});
this._nozzlePosition.copy(newNozzlePosition);
return this;
}
unRetract() {
const {
retractionEnabled,
retractionMinDistance,
retractionSpeed
} = this.settings.config;
if (this.isRetracted && retractionEnabled) {
this.isRetracted = false;
const speed = retractionSpeed * 60;
if (this.extruder > retractionMinDistance) {
this._addGCode({
[G_COMMAND]: 0,
[EXTRUDER]: this.extruder.toFixed(3),
[SPEED]: speed.toFixed(3)
});
}
}
return this;
}
retract() {
const {
retractionAmount,
retractionEnabled,
retractionMinDistance,
retractionSpeed
} = this.settings.config;
if (!this.isRetracted && retractionEnabled) {
this.isRetracted = true;
const speed = retractionSpeed * 60;
if (this.extruder > retractionMinDistance && retractionEnabled) {
this._addGCode({
[G_COMMAND]: 0,
[EXTRUDER]: (this.extruder - retractionAmount).toFixed(3),
[SPEED]: speed.toFixed(3)
});
}
}
return this;
}
getGCode() {
return this.settings.startCode() + this.gcode + this.settings.endCode();
}
}

53
src/Settings.js Normal file
View File

@ -0,0 +1,53 @@
export default class {
constructor(config = {}) {
this.config = config;
}
updateConfig(config) {
this.config = { ...this.config, ...config };
return this;
}
startCode() {
const { startCode } = this.config;
const gcode = this._subsituteVariables(startCode);
return gcode;
}
endCode() {
const { endCode } = this.config;
const gcode = this._subsituteVariables(endCode);
return gcode;
}
_subsituteVariables(gcode) {
let {
temperature,
bedTemperature,
heatTemperature,
heatBedTemperature,
travelSpeed,
printerType,
heatedbed
} = this.config;
travelSpeed *= 60;
switch (printerType) {
case 'makerbot_replicator2': printerType = 'r2'; break;
case 'makerbot_replicator2x': printerType = 'r2x'; break;
case 'makerbot_thingomatic': printerType = 't6'; break;
case 'makerbot_generic': printerType = 'r2'; break;
case '_3Dison_plus': printerType = 'r2'; break;
}
const heatedBedReplacement = heatedbed ? '' : ';';
gcode = gcode.replace(/{printingTemp}/gi, temperature);
gcode = gcode.replace(/{printingBedTemp}/gi, bedTemperature);
gcode = gcode.replace(/{preheatTemp}/gi, heatTemperature);
gcode = gcode.replace(/{preheatBedTemp}/gi, heatBedTemperature);
gcode = gcode.replace(/{printerType}/gi, printerType);
gcode = gcode.replace(/{travelSpeed}/gi, travelSpeed);
gcode = gcode.replace(/{if heatedBed}/gi, heatedBedReplacement);
return gcode;
}
}

24
src/Slice.js Normal file
View File

@ -0,0 +1,24 @@
import Shape from 'Doodle3D/clipper-js';
export default class {
constructor() {
this.parts = [];
}
getOutline() {
return this.parts.reduce((shape, part) => {
if (part.outerLine) shape.join(part.outerLine);
return shape;
}, new Shape([], true));
}
add(shape) {
const part = { shape };
if (shape.closed) {
part.innerLines = [];
part.outerLine = new Shape([], true);
part.fill = new Shape([], false);
}
this.parts.push(part);
}
}

62
src/Slicer.js Normal file
View File

@ -0,0 +1,62 @@
import * as THREE from 'three.js';
import slice from './sliceActions/slice.js';
import SlicerWorker from './slicerWorker.js!worker';
export default class {
setMesh(mesh) {
mesh.updateMatrix();
this.setGeometry(mesh.geometry, mesh.matrix);
return this;
}
setGeometry(geometry, matrix) {
if (geometry.type === 'BufferGeometry') {
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
} else if (geometry.type === 'Geometry') {
geometry = geometry.clone();
} else {
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
}
if (matrix) {
geometry.applyMatrix(matrix);
}
geometry.mergeVertices();
geometry.computeFaceNormals();
this.geometry = geometry;
return this;
}
sliceSync(settings) {
return slice(this.geometry, settings);
}
slice(settings) {
const slicerWorker = new SlicerWorker();
const geometry = this.geometry.toJSON();
const { config } = settings;
return new Promise((resolve, reject) => {
slicerWorker.onerror = reject;
slicerWorker.addEventListener('message', (event) => {
const { message, data } = event.data;
switch (message) {
case 'SLICE': {
slicerWorker.terminate();
resolve(data.gcode);
break;
}
}
});
slicerWorker.postMessage({
message: 'SLICE',
data: { geometry, config }
});
});
}
}

View File

@ -1,5 +1,2 @@
export const CLEAN_DELTA = 0.05;
export const PRECISION = 0.01;
export const VERSION = '0.0.19';
export const LOCAL_STORAGE_KEY = 'PRINTER_SETTINGS';
export const MIN_AREA = 1; // holes smaller as 1mm2 get removed
export const Z_OFFSET = 0.2;

View File

@ -1,22 +1,6 @@
import { sliceGeometry, sliceMesh } from './slicer.js';
import Interface from './interface/index.js';
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 Slicer from './Slicer.js';
import Settings from './Settings.js';
import printerSettings from './settings/printer_settings.json!json';
import userSettings from './settings/user_settings.json!json';
const defaultSettings = {
default: _defaultSettings,
printer: printerSettings,
material: materialSettings,
quality: qualitySettings,
infill: infillSettings
};
export {
sliceGeometry,
sliceMesh,
Interface,
defaultSettings
};
export { Slicer, Settings, printerSettings, userSettings };

View File

@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'proptypes';
import injectSheet from 'react-jss';
import ExpandIcon from 'material-ui-icons/ExpandMore';
const styles = {
button: {
cursor: 'pointer'
},
body: {
overflow: 'hidden'
},
closed: {
maxHeight: '0px'
},
title: {
userSelect: 'none',
display: 'flex',
alignItems: 'flex-end'
}
};
class Accordion extends React.Component {
static propTypes = {
elements: PropTypes.arrayOf(PropTypes.shape({ body: PropTypes.node, title: PropTypes.string })),
classes: PropTypes.objectOf(PropTypes.string)
};
static defaultProps: {
elements: []
};
state = {
openAccordion: null
};
changeAccordion = (name) => {
const { openAccordion } = this.state;
if (openAccordion === name) {
this.setState({ openAccordion: null });
} else {
this.setState({ openAccordion: name });
}
};
render() {
const { openAccordion } = this.state;
const { elements, classes } = this.props;
return elements.map(({ body, title }, i) => (
<span key={i}>
<span onClick={() => this.changeAccordion(title)} className={classes.title}>
<ExpandIcon />
<p style={{
fontWeight: openAccordion === title ? 'bold' : 'normal'
}} className={classes.button}>{title}</p>
</span>
<div className={`${classes.body} ${openAccordion === title ? '' : classes.closed}`}>
{body}
</div>
</span>
));
}
}
export default injectSheet(styles)(Accordion);

View File

@ -1,104 +0,0 @@
import React from 'react';
import PropTypes from 'proptypes';
import _ from 'lodash';
import MaterialUISelectField from 'material-ui/SelectField';
import MaterialUICheckbox from 'material-ui/Checkbox';
import TextFieldIcon from 'material-ui-textfield-icon';
import RefreshIcon from 'material-ui-icons/Refresh';
import muiThemeable from 'material-ui/styles/muiThemeable';
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 = ({ name, muiTheme, ...props }, context) => (
<MaterialUISelectField
{...props}
disabled={context.disabled}
value={_.get(context, name)}
onChange={(event, index, value) => context.onChange(name, value)}
/>
);
_SelectField.contextTypes = contextTypes;
_SelectField.propTypes = propTypes;
export const SelectField = muiThemeable()(_SelectField);
const _TextField = ({ name, muiTheme: { palette }, ...props }, context) => (
<TextFieldIcon
{...props}
icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }}
onClick={() => context.onChange(name, null)}
/>}
floatingLabelStyle={{
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
value={_.get(context, name)}
onChange={(event, value) => context.onChange(name, value)}
/>
);
_TextField.contextTypes = contextTypes;
_TextField.propTypes = propTypes;
export const TextField = muiThemeable()(_TextField);
const _NumberField = ({ name, min, max, muiTheme: { palette }, ...props }, context) => (
<TextFieldIcon
{...props}
type="number"
icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }}
onClick={() => 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
{...props}
style={{ display: 'block' }}
iconStyle={{
fill: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
checked={_.get(context, name)}
onCheck={(event, value) => context.onChange(name, value)}
/>
{context.advancedFields.includes(name) && <RefreshIcon
onClick={() => context.onChange(name, null)}
/>}
</span>
);
_Checkbox.contextTypes = contextTypes;
_Checkbox.propTypes = propTypes;
export const Checkbox = muiThemeable()(_Checkbox);

View File

@ -1,568 +0,0 @@
import React from 'react';
import PropTypes from 'proptypes';
import _ from 'lodash';
import { Tabs, Tab } from 'material-ui/Tabs';
import MenuItem from 'material-ui/MenuItem';
import injectSheet from 'react-jss';
import { SelectField, TextField, NumberField, Checkbox } from './FormComponents.js';
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 Accordion from './Accordion.js';
const styles = {
textFieldRow: {
display: 'flex',
alignItems: 'center'
},
container: {
width: '100%',
flexGrow: 1,
overflowY: 'auto',
'& p': {
// fontWeight: 'bold',
margin: '30px 0 0 0'
},
'& h3': {
fontWeight: 'bold',
marginTop: '20px',
marginBottom: '20px'
}
},
error: {
color: red500
}
};
const updateLocalStorage = (localStorage) => {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(localStorage));
};
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;
};
class Settings extends React.Component {
static propTypes = {
selectedPrinter: PropTypes.string,
classes: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func,
disabled: PropTypes.bool.isRequired
};
static defaultProps: {
disabled: false
};
static childContextTypes = {
settings: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
addPrinter: PropTypes.object.isRequired,
managePrinter: PropTypes.object.isRequired,
activePrinter: PropTypes.string,
advancedFields: PropTypes.array.isRequired
};
state = {
localStorage: getLocalStorage(),
addPrinter: {
open: false,
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.values(localStorage.printers)
.find(({ 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();
}
}
changeSettings = (fieldName, value) => {
const { onChange } = this.props;
const { localStorage } = this.state;
let state = _.cloneDeep(this.state);
switch (fieldName) {
case 'managePrinter.printer':
case 'managePrinter.name':
case 'managePrinter.ip':
state = _.set(state, fieldName, value);
state = update(state, { managePrinter: { error: { $set: null } } });
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.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;
default:
break;
}
this.setState(state);
if (localStorage.active) {
if (onChange) onChange(this.constructSettings(state.localStorage));
updateLocalStorage(state.localStorage);
}
return state;
}
getChildContext() {
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 }, 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;
this.setState({
managePrinter: {
open,
name: printers[active].name,
ip: printers[active].ip,
printer: printers[active].settings.printer,
error: null
}
});
}
render() {
const { addPrinter, managePrinter, localStorage } = this.state;
const { classes } = this.props;
return (
<div className={classes.container}>
<div className={classes.textFieldRow}>
<SelectField name="activePrinter" floatingLabelText="Printer" fullWidth>
{Object.entries(localStorage.printers).map(([id, { name }]) => (
<MenuItem key={id} value={id} primaryText={name} />
))}
<Divider />
<MenuItem onClick={this.openAddPrinterDialog} value="add_printer" primaryText="Add Printer" />
</SelectField>
{localStorage.active && <SettingsIcon
onClick={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} />
))}
</SelectField>
<h3>Print Setup</h3>
<Tabs>
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Basic">
<div>
<SelectField name="settings.quality" floatingLabelText="Quality" fullWidth>
{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} />
))}
</SelectField>
</div>
</Tab>
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Advanced">
<div>
<Accordion elements={[{
title: 'Layer',
body: (<NumberField name="settings.layerHeight" min={0.05} max={3} fullWidth floatingLabelText="Height" />)
}, {
title: 'Thickness',
body: (<span>
<NumberField name="settings.thickness.top" min={0} fullWidth floatingLabelText="top" />
<NumberField name="settings.thickness.bottom" min={0} fullWidth floatingLabelText="bottom" />
<NumberField name="settings.thickness.shell" min={0} fullWidth floatingLabelText="shell" />
</span>)
}, {
title: 'Material',
body: (<span>
<NumberField name="settings.filamentThickness" min={0.1} max={10} fullWidth floatingLabelText="Thickness" />
<NumberField name="settings.temperature" min={100} max={400} fullWidth floatingLabelText="Temperature" />
</span>)
}, {
title: 'Bed',
body: (<span>
<NumberField name="settings.bedTemperature" min={30} max={150} fullWidth floatingLabelText="Temperature" />
<Checkbox name="settings.heatedBed" label="Heated" />
</span>)
}, {
title: 'Brim',
body: (<span>
<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" />
</span>)
}, {
title: 'Support',
body: (<span>
<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" />
</span>)
}, {
title: 'First layer',
body: (<span>
<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" />
</span>)
}, {
title: 'Inner shell',
body: (<span>
<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" />
</span>)
}, {
title: 'Outer shell',
body: (<span>
<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" />
</span>)
}, {
title: 'Inner infill',
body: (<span>
<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" />
</span>)
}, {
title: 'Outer infill',
body: (<span>
<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" />
</span>)
}, {
title: 'Travel',
body: (<span>
<NumberField name="settings.travel.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<Checkbox name="settings.combing" label="Combing" />
</span>)
}, {
title: 'Retraction',
body: (<span>
<Checkbox name="settings.retraction.enabled" label="Enabled" />
<NumberField name="settings.retraction.amount" min={0} max={10} fullWidth floatingLabelText="Amount" />
<NumberField name="settings.retraction.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.retraction.minDistance" min={0} fullWidth floatingLabelText="Min distance" />
</span>)
}, {
title: 'Printer dimensions',
body: (<span>
<div className={classes.textFieldRow}>
<NumberField name="settings.dimensions.x" min={1} fullWidth floatingLabelText="X" />
<NumberField name="settings.dimensions.y" min={1} fullWidth floatingLabelText="Y" />
<NumberField name="settings.dimensions.z" min={1} fullWidth floatingLabelText="Z" />
</div>
</span>)
}, {
title: 'Nozzle',
body: (<span>
<NumberField name="settings.nozzleDiameter" min={0.1} max={5} fullWidth floatingLabelText="Diameter" />
</span>)
}]} />
</div>
</Tab>
</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>
);
}
}
function printDialog(props, state, title, form, submitText, data, closeDialog, removeActivePrinter, save) {
const { classes } = props;
return (
<Dialog
title={title}
open={data.open}
onRequestClose={closeDialog ? closeDialog : null}
contentStyle={{ maxWidth: '400px' }}
autoScrollBodyContent
actions={[
closeDialog && <FlatButton
label="Close"
onClick={closeDialog}
/>,
removeActivePrinter && <FlatButton
label="Remove Printer"
onClick={removeActivePrinter}
/>,
<RaisedButton
label={submitText}
primary
onClick={save}
/>
]}
>
<SelectField name={`${form}.printer`} floatingLabelText="Printer" fullWidth>
{Object.entries(printerSettings).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
))}
</SelectField>
{data.error && <p className={classes.error}>{data.error}</p>}
</Dialog>
);
}
printDialog.propTypes = {
classes: PropTypes.objectOf(PropTypes.string)
};
export default injectSheet(styles)(Settings);

View File

@ -1,377 +0,0 @@
import * as THREE from 'three';
import React from 'react';
import PropTypes from 'proptypes';
import { centerGeometry, placeOnGround, createScene, slice, TabTemplate } from './utils.js';
import injectSheet from 'react-jss';
import RaisedButton from 'material-ui/RaisedButton';
import LinearProgress from 'material-ui/LinearProgress';
import { grey50, grey300, grey800, red500 } from 'material-ui/styles/colors';
import Popover from 'material-ui/Popover/Popover';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import { Tabs, Tab } from 'material-ui/Tabs';
import Settings from './Settings.js';
import ReactResizeDetector from 'react-resize-detector';
import muiThemeable from 'material-ui/styles/muiThemeable';
import logo from '../../img/logo.png';
const MAX_FULLSCREEN_WIDTH = 720;
const styles = {
container: {
position: 'relative',
display: 'flex',
height: '100%',
backgroundColor: grey50,
color: grey800,
overflow: 'hidden',
fontFamily: 'roboto, sans-serif'
},
controlBar: {
position: 'absolute',
bottom: '10px',
left: '10px'
},
d3View: {
flexGrow: 1,
flexBasis: 0
},
canvas: {
position: 'absolute'
},
settingsBar: {
display: 'flex',
flexDirection: 'column',
maxWidth: '320px',
boxSizing: 'border-box',
padding: '10px 20px',
backgroundColor: 'white',
overflowY: 'auto',
borderLeft: `1px solid ${grey300}`
},
sliceActions: {
flexShrink: 0
},
sliceInfo: {
margin: '10px 0',
'& p': {
marginBottom: '5px',
fontSize: '11px'
}
},
sliceButtons: {
justifyContent: 'flex-end',
display: 'flex'
},
button: {
margin: '5px 0 5px 5px'
},
controlButton: {
marginRight: '5px'
},
buttonContainer: {
width: '100%',
padding: '10px'
},
error: {
color: red500
},
title: {
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 {
static propTypes = {
selectedPrinter: PropTypes.string,
mesh: PropTypes.shape({ isMesh: PropTypes.oneOf([true]) }),
classes: PropTypes.objectOf(PropTypes.string),
pixelRatio: PropTypes.number.isRequired,
onCancel: PropTypes.func,
onSliceSucces: PropTypes.func.isRequired,
muiTheme: PropTypes.object.isRequired
};
static defaultProps = {
pixelRatio: 1
};
constructor(props) {
super(props);
this.canvasElement = React.createRef();
const scene = createScene(this.props);
this.state = {
scene,
settings: null,
showFullScreen: window.innerWidth > MAX_FULLSCREEN_WIDTH,
isSlicing: false,
error: null,
mesh: null,
objectDimensions: '0x0x0mm',
popover: { open: false, element: null }
};
}
componentDidMount() {
const { scene } = this.state;
scene.updateCanvas(this.canvasElement.current);
const { mesh } = this.props;
if (mesh) {
this.updateMesh(mesh, scene);
}
}
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() {
const { scene: { editorControls, mesh: { material }, renderer } } = this.state;
editorControls.dispose();
material.dispose();
renderer.dispose();
}
resetMesh = () => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
if (mesh) {
mesh.position.set(0, 0, 0);
mesh.scale.set(1, 1, 1);
mesh.rotation.set(0, 0, 0);
mesh.updateMatrix();
placeOnGround(mesh);
this.calculateDimensions();
render();
}
};
scaleUp = () => this.scaleMesh(0.9);
scaleDown = () => this.scaleMesh(1.0 / 0.9);
scaleMesh = (factor) => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
if (mesh) {
mesh.scale.multiplyScalar(factor);
mesh.updateMatrix();
placeOnGround(mesh);
this.calculateDimensions();
render();
}
};
rotateX = () => this.rotate(new THREE.Vector3(0, 0, 1), Math.PI / 2.0);
rotateY = () => this.rotate(new THREE.Vector3(1, 0, 0), Math.PI / 2.0);
rotateZ = () => this.rotate(new THREE.Vector3(0, 1, 0), Math.PI / 2.0);
rotate = (axis, angle) => {
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
if (mesh) {
mesh.rotateOnWorldAxis(axis, angle);
placeOnGround(mesh);
this.calculateDimensions();
render();
}
};
slice = async () => {
const { isSlicing, settings, mesh, scene: { mesh: { matrix } } } = this.state;
const { onSliceSucces } = this.props;
if (isSlicing) return;
if (!settings) {
this.setState({ error: 'please select a printer first' });
return;
}
if (!mesh) {
this.setState({ error: 'there is no file to slice' });
return;
}
this.closePopover();
this.setState({ isSlicing: true, progress: { action: '', percentage: 0, step: 0 }, error: null });
const exportMesh = new THREE.Mesh(mesh.geometry, mesh.material);
exportMesh.applyMatrix(matrix);
try {
const updateProgres = progress => this.setState({ progress: { ...this.state.progress, ...progress } });
const sliceResults = await slice(exportMesh, settings, updateProgres);
onSliceSucces(sliceResults);
} catch (error) {
this.setState({ error: error.message });
throw error;
} finally {
this.setState({ isSlicing: false });
}
};
openPopover = (event) => {
event.preventDefault();
this.setState({
popover: {
element: event.currentTarget,
open: true
}
});
};
closePopover = () => {
this.setState({
popover: {
element: null,
open: false
}
});
};
componentDidUpdate() {
const { scene: { updateCanvas } } = this.state;
if (updateCanvas && this.canvasElement.current) updateCanvas(this.canvasElement.current);
}
onResize3dView = (width, height) => {
window.requestAnimationFrame(() => {
const { scene: { setSize } } = this.state;
const { pixelRatio } = this.props;
if (setSize) setSize(width, height, pixelRatio);
});
};
onResizeContainer = (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` });
};
render() {
const { classes, onCancel, selectedPrinter } = this.props;
const { isSlicing, settings, progress, showFullScreen, error, objectDimensions } = this.state;
const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) };
const settingsPanel = (
<div className={classes.settingsBar} style={style}>
<Settings
selectedPrinter={selectedPrinter}
disabled={isSlicing}
onChange={this.onChangeSettings}
/>
<div className={classes.sliceActions}>
<div className={classes.sliceInfo}>
{error && <p className={classes.error}>{error}</p>}
{isSlicing && <p>{progress.action}</p>}
{isSlicing && <LinearProgress mode="determinate" value={progress.percentage * 100.0} />}
</div>
<div className={classes.sliceButtons}>
{onCancel && <RaisedButton
label="Close"
className={`${classes.button}`}
onClick={onCancel}
/>}
<RaisedButton
label="Download GCODE"
ref="button"
primary
className={`${classes.button}`}
disabled={isSlicing}
onClick={() => this.slice()}
/>
</div>
</div>
</div>
);
const d3Panel = (
<div className={classes.d3View}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
<canvas className={classes.canvas} ref={this.canvasElement} />
<div className={classes.controlBar}>
<div className={classes.detail}>
<p>Dimensions: {objectDimensions}</p>
</div>
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.resetMesh} label="reset" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.scaleUp} label="scale down" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.scaleDown} label="scale up" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateX} label="rotate x" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateY} label="rotate y" />
<RaisedButton disabled={isSlicing} className={classes.controlButton} onClick={this.rotateZ} label="rotate z" />
</div>
</div>
);
if (showFullScreen) {
return (
<div className={classes.container}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
<img src={logo} className={classes.logo} />
{d3Panel}
{settingsPanel}
</div>
);
} else {
return (
<div className={classes.container}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
<Tabs
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
tabItemContainerStyle={{ flexShrink: 0 }}
contentContainerStyle={{ flexGrow: 1, display: 'flex' }}
tabTemplateStyle={{ display: 'flex' }}
tabTemplate={TabTemplate}
>
<Tab label="Settings">
{settingsPanel}
</Tab>
<Tab label="Edit Model">
{d3Panel}
</Tab>
</Tabs>
</div>
);
}
}
}
export default muiThemeable()(injectSheet(styles)(Interface));

View File

@ -1,143 +0,0 @@
import * as THREE from 'three';
import 'three/examples/js/controls/EditorControls';
import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml';
import { sliceGeometry } from '../slicer.js';
import React from 'react';
import PropTypes from 'prop-types';
export function placeOnGround(mesh) {
const boundingBox = new THREE.Box3().setFromObject(mesh);
mesh.position.y -= boundingBox.min.y;
mesh.updateMatrix();
}
export function centerGeometry(mesh) {
// center geometry
mesh.geometry.computeBoundingBox();
const center = mesh.geometry.boundingBox.getCenter();
mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
}
export function createScene({ muiTheme }) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 1, 10000);
camera.position.set(0, 400, 300);
camera.lookAt(new THREE.Vector3(0, 0, 0));
const directionalLightA = new THREE.DirectionalLight(0xa2a2a2);
directionalLightA.position.set(1, 1, 1);
scene.add(directionalLightA);
const directionalLightB = new THREE.DirectionalLight(0xa2a2a2);
directionalLightB.position.set(-1, 1, -1);
scene.add(directionalLightB);
const light = new THREE.AmbientLight(0x656565);
scene.add(light);
const material = new THREE.MeshPhongMaterial({ color: muiTheme.palette.primary2Color, side: THREE.DoubleSide, specular: 0xc5c5c5, shininess: 5, flatShading: false });
const mesh = new THREE.Mesh(new THREE.Geometry(), material);
scene.add(mesh);
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);
let renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
let editorControls = new THREE.EditorControls(camera, renderer.domElement);
box.scale.set(1, 1, 1);
box.updateMatrix();
const render = () => renderer.render(scene, camera);
const setSize = (width, height, pixelRatio = 1) => {
renderer.setSize(width, height);
renderer.setPixelRatio(pixelRatio);
camera.aspect = width / height;
camera.updateProjectionMatrix();
render();
};
const updateCanvas = (canvas) => {
if (!renderer || renderer.domElement !== canvas) {
if (renderer) renderer.dispose();
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0xffffff, 0);
}
if (!editorControls || editorControls.domElement !== canvas) {
if (editorControls) editorControls.dispose();
editorControls = new THREE.EditorControls(camera, canvas);
editorControls.addEventListener('change', render);
}
render();
};
const focus = () => editorControls.focus(mesh);
return { editorControls, scene, mesh, camera, renderer, render, box, setSize, updateCanvas, focus };
}
export function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
export async function slice(mesh, settings, updateProgress) {
let steps = 1;
let currentStep = 0;
const { dimensions } = settings;
const centerX = dimensions.x / 2;
const centerY = dimensions.y / 2;
const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX)
.multiply(new THREE.Matrix4().makeRotationY(-Math.PI / 2.0))
.multiply(mesh.matrix);
const sliceResult = await sliceGeometry({
...settings,
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({
action: progress.action,
percentage: (currentStep + progress.done / progress.total) / steps
});
}).catch(error => {
throw { message: `error during slicing: ${error.message}`, code: 2 };
});
currentStep ++;
return sliceResult;
}
export const TabTemplate = ({ children, selected, style }) => {
const templateStyle = {
width: '100%',
position: 'relative',
textAlign: 'initial',
...style,
...(selected ? {} : {
height: 0,
width: 0,
overflow: 'hidden'
})
};
return (
<div style={templateStyle}>
{children}
</div>
);
};
TabTemplate.propTypes = {
children: PropTypes.node,
selected: PropTypes.bool,
style: PropTypes.object
};

View File

@ -1,77 +0,0 @@
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:
x: 200
y: 200
z: 200
heatedBed: false
nozzleDiameter: 0.4
filamentThickness: 2.85
temperature: 210
bedTemperature: 50
layerHeight: 0.15
combing: false
thickness:
top: 0.45
bottom: 0.45
shell: 0.8
retraction:
enabled: true
amount: 3.0
speed: 50.0
minDistance: 0.0
travel:
speed: 200.0
support:
enabled: false
minArea: 2
distanceY: 0.4
density: 5.0
margin: 2.0
flowRate: 0.8
speed: 40.0
innerShell:
flowRate: 1.0
speed: 50.0
outerShell:
flowRate: 1.0
speed: 40.0
innerInfill:
flowRate: 1.0
speed: 80.0
density: 20.0
outerInfill:
flowRate: 1.0
speed: 50.0
brim:
size: 8.0
flowRate: 1.0
speed: 40.0
firstLayer:
flowRate: 1.2
speed: 40.0

View File

@ -1,21 +0,0 @@
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

View File

@ -1,6 +0,0 @@
pla:
title: PLA
temperature: 210
abs:
title: ABS
temperature: 240

View File

@ -1,367 +0,0 @@
_3Dison_plus:
title: 3Dison plus
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 227
y: 147
z: 150
bigbuilder3d:
title: Big Builder 3D
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
builder3d:
title: Builder 3D
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
bukobot:
title: Bukobot
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
cartesio:
title: Cartesio
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
colido_2_0_plus:
title: ColiDo 2.0 Plus
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 230
y: 150
z: 140
colido_compact:
title: ColiDo Compact
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 130
y: 130
z: 115
colido_diy:
title: ColiDo DIY
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 170
colido_m2020:
title: ColiDo M2020
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
colido_x3045:
title: ColiDo X3045
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 300
y: 300
z: 450
craftbot_plus:
title: CraftBot PLUS
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 250
y: 200
z: 200
cyrus:
title: Cyrus
heatedBed: false
dimensions:
x: 195
y: 195
z: 200
delta_rostockmax:
title: Delta RostockMax
heatedBed: false
deltamaker:
title: Deltamaker
heatedBed: false
doodle_dream:
title: Doodle Dream
heatedBed: false
filamentThickness: 1.75
dimensions:
x: 120
y: 120
z: 80
eventorbot:
title: EventorBot
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 203
y: 250
z: 150
felix:
title: Felix
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 237
y: 244
z: 235
gigabot:
title: Gigabot
heatedBed: false
filamentThickness: 2.85
kossel:
title: Kossel
heatedBed: false
filamentThickness: 2.85
leapfrog_creatr:
title: LeapFrog Creatr
heatedBed: false
filamentThickness: 2.85
lulzbot_aO_101:
title: LulzBot AO-101
heatedBed: false
filamentThickness: 2.85
lulzbot_taz_4:
title: LulzBot TAZ 4
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 298
y: 275
z: 250
makerbot_generic:
title: Generic Makerbot Printer
heatedBed: false
filamentThickness: 2.85
makerbot_replicator2:
title: MakerBot Replicator2
heatedBed: false
filamentThickness: 2.85
makerbot_replicator2x:
title: MakerBot Replicator2x
heatedBed: true
filamentThickness: 2.85
makerbot_thingomatic:
title: MakerBot Thing-o-matic
heatedBed: false
filamentThickness: 2.85
makergear_m2:
title: MakerGear M2
heatedBed: false
filamentThickness: 2.85
makergear_prusa:
title: MakerGear Prusa
heatedBed: false
filamentThickness: 2.85
makibox:
title: Makibox
heatedBed: false
filamentThickness: 2.85
mamba3d:
title: Mamba3D
heatedBed: false
filamentThickness: 2.85
marlin_generic:
title: Generic Marlin Printer
heatedBed: false
filamentThickness: 2.85
minifactory:
title: miniFactory
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 150
y: 150
z: 155
orca_0_3:
title: Orca 0.3
heatedBed: false
filamentThickness: 2.85
ord_bot_hadron:
title: ORD Bot Hadron
heatedBed: false
filamentThickness: 2.85
printrbot:
title: Printrbot
heatedBed: false
filamentThickness: 2.85
printxel_3d:
title: Printxel 3D
heatedBed: false
filamentThickness: 2.85
prusa_i3:
title: Prusa I3
heatedBed: false
filamentThickness: 2.85
prusa_iteration_2:
title: Prusa Iteration 2
heatedBed: false
filamentThickness: 2.85
rapman:
title: RapMan
heatedBed: false
filamentThickness: 2.85
renkforce_rf100:
title: Renkforce RF100
heatedBed: false
filamentThickness: 1.75
dimensions:
x: 100
y: 100
z: 100
reprappro_huxley:
title: RepRapPro Huxley
heatedBed: false
filamentThickness: 2.85
reprappro_mendel:
title: RepRapPro Mendel
heatedBed: false
filamentThickness: 2.85
rigidbot:
title: Rigidbot
heatedBed: false
filamentThickness: 2.85
robo_3d_printer:
title: RoBo 3D Printer
heatedBed: false
filamentThickness: 2.85
shapercube:
title: ShaperCube
heatedBed: false
filamentThickness: 2.85
tantillus:
title: Tantillus
heatedBed: false
filamentThickness: 2.85
ultimaker:
title: Ultimaker Original
heatedBed: false
filamentThickness: 2.85
ultimaker2:
title: Ultimaker 2
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 205
ultimaker2_plus:
title: Ultimaker 2+
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 205
ultimaker2_plus_extended:
title: Ultimaker 2+ Extended
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 305
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
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 120
y: 120
z: 112
ultimaker_original_plus:
title: Ultimaker Original Plus
heatedBed: true
filamentThickness: 2.85
vision_3d_printer:
title: Vision 3D Printer
heatedBed: false
filamentThickness: 2.85
wanhao_duplicator4:
title: Wanhao Duplicator 4
heatedBed: true
filamentThickness: 1.75
dimensions:
x: 210
y: 140
z: 140
wanhao_duplicator_i3_plus:
title: Wanhao Duplicator i3 Plus
heatedBed: false
filamentThickness: 1.75
wanhao_duplicator_i3_mini:
duplicator_i3_mini:
startCode: |-
M104 S{temperature}
G28
M109 S{temperature}
G90
M82
G1 Z10.0 F6000
G92 E0
G1 F200 E3
G92 E0
endCode: |-
M104 S0
G92 E1
G1 E-1 F300
G28 X0 Y0
M84
M82
M104 S0
title: Wanhao Duplicator i3 Mini
heatedBed: false
filamentThickness: 1.75
dimensions:
x: 120
y: 135
z: 120

View File

@ -0,0 +1,38 @@
{
"ultimaker": {
"dimensionsX": 200,
"dimensionsY": 200,
"dimensionsZ": 200,
"endCode": "M107 ;fan off\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;disable axes / steppers\nG90 ;absolute positioning\nM104 S{preheatTemp}\n{if heatedBed}M140 S{preheatBedTemp}\nM117 Done ;display message (20 characters to clear whole screen)\n",
"filamentThickness": 2.85,
"heatedBed": false,
"heatupEnabled": true,
"nozzleDiameter": 0.4,
"startCode": ";Generated with Doodle3D (default)\nM109 S{printingTemp} ;set target temperature \n{if heatedBed}M190 S{printingBedTemp} ;set target bed temperature\nG21 ;metric values\nG91 ;relative positioning\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E10 ;extrude 10mm of feed stock\nG92 E0 ;zero the extruded length again\nG92 E0 ;zero the extruded length again\nG1 F9000\nG90 ;absolute positioning\nM117 Printing Doodle... ;display message (20 characters to clear whole screen)\n",
"type": "ultimaker"
},
"ultimaker2": {
"dimensionsX": 223,
"dimensionsY": 223,
"dimensionsZ": 205,
"endCode": "M107 ;fan off\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+5.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 ;home the printer\nM84 ;disable axes / steppers\nG90 ;absolute positioning\nM104 S{preheatTemp}\n{if heatedBed}M140 S{preheatBedTemp}\nM117 Done ;display message (20 characters to clear whole screen)\n",
"filamentThickness": 2.85,
"heatedBed": true,
"heatupEnabled": true,
"nozzleDiameter": 0.4,
"startCode": ";Generated with Doodle3D (ultimaker2)\nM109 S{printingTemp} ;set target temperature \n{if heatedBed}M190 S{printingBedTemp} ;set target bed temperature\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nG28 ; home to endstops\nG1 Z15 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E10 ;extrude 10mm of feed stock\nG92 E0 ;zero the extruded length again\nG0 X0 Y0 F{travelSpeed} ;home position is not X0 Y0\nM117 Printing Doodle... ;display message (20 characters to clear whole screen)\n",
"type": "ultimaker2"
},
"ultimaker2go": {
"dimensionsX": 120,
"dimensionsY": 120,
"dimensionsZ": 115,
"endCode": "M107 ;fan off\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 ;home the printer\nM84 ;disable axes / steppers\nG90 ;absolute positioning\nM104 S{preheatTemp}\n{if heatedBed}M140 S{preheatBedTemp}\nM117 Done ;display message (20 characters to clear whole screen)\n",
"filamentThickness": 2.85,
"heatedBed": false,
"heatupEnabled": true,
"nozzleDiameter": 0.4,
"startCode": ";Generated with Doodle3D (ultimaker2go)\nM109 S{printingTemp} ;set target temperature \n{if heatedBed}M190 S{printingBedTemp} ;set target bed temperature\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nG28 ; home to endstops\nG1 Z15 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E10 ;extrude 10mm of feed stock\nG92 E0 ;zero the extruded length again\nG0 X0 Y0 F{travelSpeed} ;home position is not X0 Y0\nM117 Printing Doodle... ;display message (20 characters to clear whole screen)\n",
"type": "ultimaker2g0"
}
}

View File

@ -1,54 +0,0 @@
low:
title: "Low"
thickness:
top: 0.30
bottom: 0.30
shell: 0.4
layerHeight: .2
innerShell:
speed: 80.0
outerShell:
speed: 70.0
outerInfill:
speed: 80.0
firstLayer:
speed: 70.0
innerInfill:
speed: 80.0
density: 10.0
medium:
title: "Medium"
layerHeight: .15
thickness:
top: 0.45
bottom: 0.45
shell: 0.8
innerShell:
speed: 50.0
outerShell:
speed: 40.0
outerInfill:
speed: 50.0
firstLayer:
speed: 40.0
innerInfill:
speed: 80.0
density: 10.0
high:
title: "High"
thickness:
top: 0.60
bottom: 0.60
shell: 1.2
layerHeight: .1
innerShell:
speed: 40.0
outerShell:
speed: 30.0
outerInfill:
speed: 40.0
firstLayer:
speed: 30.0
innerInfill:
speed: 70.0
density: 20.0

View File

@ -0,0 +1,144 @@
{
"low": {
"heatupBedTemperature": 70,
"heatupTemperature": 20,
"temperature": 210.0,
"layerHeight": 0.15,
"bottomThickness": 0.4,
"topThickness": 0.8,
"shellThickness": 0.4,
"brimOffset": 4.0,
"fillGridSize": 5.0,
"infillOverlap": 0.0,
"travelSpeed": 200.0,
"retractionAmount": 3.0,
"retractionEnabled": true,
"retractionSpeed": 50.0,
"retractionMinDistance": 0.0,
"supportAcceptanceMargin": 1.5,
"supportDistanceY": 0.4,
"supportEnabled": false,
"supportGridSize": 6.0,
"supportMargin": 2.0,
"supportPlateSize": 4.0,
"outerLine": {
"flowRate": 1.0,
"speed": 40.0
},
"innerLine": {
"flowRate": 1.0,
"speed": 50.0
},
"fill": {
"flowRate": 1.0,
"speed": 50.0
},
"brim": {
"flowRate": 1.0,
"speed": 40.0
},
"support": {
"flowRate": 0.8,
"speed": 40.0
},
"bottom": {
"flowRate": 1.2,
"speed": 40.0
}
},
"medium": {
"heatupBedTemperature": 70,
"heatupTemperature": 20,
"temperature": 210.0,
"layerHeight": 0.1,
"bottomThickness": 0.4,
"topThickness": 0.8,
"shellThickness": 0.4,
"brimOffset": 4.0,
"fillGridSize": 5.0,
"infillOverlap": 0.0,
"travelSpeed": 200.0,
"retractionAmount": 3.0,
"retractionEnabled": true,
"retractionSpeed": 50.0,
"retractionMinDistance": 0.0,
"supportAcceptanceMargin": 1.5,
"supportDistanceY": 0.4,
"supportEnabled": false,
"supportGridSize": 6.0,
"supportMargin": 2.0,
"supportPlateSize": 4.0,
"outerLine": {
"flowRate": 1.0,
"speed": 40.0
},
"innerLine": {
"flowRate": 1.0,
"speed": 50.0
},
"fill": {
"flowRate": 1.0,
"speed": 50.0
},
"brim": {
"flowRate": 1.0,
"speed": 40.0
},
"support": {
"flowRate": 0.8,
"speed": 40.0
},
"bottom": {
"flowRate": 1.2,
"speed": 40.0
}
},
"high": {
"heatupBedTemperature": 70,
"heatupTemperature": 20,
"temperature": 210.0,
"layerHeight": 0.06,
"bottomThickness": 0.4,
"topThickness": 0.8,
"shellThickness": 0.4,
"brimOffset": 4.0,
"fillGridSize": 5.0,
"infillOverlap": 0.0,
"travelSpeed": 200.0,
"retractionAmount": 3.0,
"retractionEnabled": true,
"retractionSpeed": 50.0,
"retractionMinDistance": 0.0,
"supportAcceptanceMargin": 1.5,
"supportDistanceY": 0.4,
"supportEnabled": false,
"supportGridSize": 6.0,
"supportMargin": 2.0,
"supportPlateSize": 4.0,
"outerLine": {
"flowRate": 1.0,
"speed": 40.0
},
"innerLine": {
"flowRate": 1.0,
"speed": 50.0
},
"fill": {
"flowRate": 1.0,
"speed": 50.0
},
"brim": {
"flowRate": 1.0,
"speed": 40.0
},
"support": {
"flowRate": 0.8,
"speed": 40.0
},
"bottom": {
"flowRate": 1.2,
"speed": 40.0
}
}
}

View File

@ -1,36 +1,21 @@
import Shape from '@doodle3d/clipper-js';
import * as THREE from 'three.js';
import { PRECISION } from '../constants.js';
const OFFSET_OPTIONS = {
jointType: 'jtRound',
const offsetOptions = {
jointType: 'jtSquare',
endType: 'etClosedPolygon',
miterLimit: 2.0,
roundPrecision: 0.25,
endType: 'etClosedPolygon'
roundPrecision: 0.25
};
export default function addBrim(slices, settings) {
let {
brim: { size: brimSize },
nozzleDiameter
} = settings;
console.log('add brim');
nozzleDiameter /= PRECISION;
brimSize /= PRECISION;
const nozzleRadius = nozzleDiameter / 2;
let { brimOffset } = settings.config;
brimOffset /= PRECISION;
const [firstLayer] = slices;
const brim = firstLayer.parts.reduce((_brim, { shape }) => (
_brim.join(shape.offset(nozzleRadius, {
...OFFSET_OPTIONS,
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);
}
const fistLayer = slices[0];
fistLayer.brim = fistLayer
.getOutline()
.offset(brimOffset, offsetOptions);
}

View File

@ -1,13 +1,11 @@
import { PRECISION } from '../constants.js';
import { divide } from './helpers/vector2.js';
import { PRECISION } from '../constants.js'
export default function applyPrecision(layers) {
for (let layer = 0; layer < layers.length; layer ++) {
const { fillShapes, lineShapesOpen, lineShapesClosed } = layers[layer];
export default function applyPrecision(shapes) {
for (let i = 0; i < shapes.length; i ++) {
const { closedShapes, openShapes } = shapes[i];
scaleUpShape(fillShapes);
scaleUpShape(lineShapesOpen);
scaleUpShape(lineShapesClosed);
scaleUpShape(closedShapes);
// scaleUpShape(openShapes);
}
}
@ -16,7 +14,9 @@ function scaleUpShape(shape) {
const path = shape[i];
for (let i = 0; i < path.length; i ++) {
path[i] = divide(path[i], PRECISION);
const point = path[i];
point.copy(point.divideScalar(PRECISION));
}
}
}

View File

@ -1,28 +1,33 @@
import { Z_OFFSET } from '../constants.js';
import * as THREE from 'three.js';
export default function calculateLayersIntersections(lines, settings) {
const {
dimensions: { z: dimensionsZ },
layerHeight
} = settings;
console.log('calculating layer intersections');
const numLayers = Math.floor((dimensionsZ - Z_OFFSET) / layerHeight);
const { layerHeight, dimensionsZ } = settings.config;
const layerPoints = Array.from(Array(numLayers)).map(() => ({}));
const layerFaceIndexes = Array.from(Array(numLayers)).map(() => []);
const numLayers = Math.floor(dimensionsZ / layerHeight);
const layerIntersectionIndexes = [];
const layerIntersectionPoints = [];
for (let layer = 0; layer < numLayers; layer ++) {
layerIntersectionIndexes[layer] = [];
layerIntersectionPoints[layer] = [];
}
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
const { line, faces } = lines[lineIndex];
const line = lines[lineIndex].line;
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) - Z_OFFSET) / layerHeight);
const min = Math.ceil(Math.min(line.start.y, line.end.y) / layerHeight);
const max = Math.floor(Math.max(line.start.y, line.end.y) / layerHeight);
for (let layerIndex = min; layerIndex <= max; layerIndex ++) {
if (layerIndex >= 0 && layerIndex < numLayers) {
const y = layerIndex * layerHeight + Z_OFFSET;
let x;
let z;
layerIntersectionIndexes[layerIndex].push(lineIndex);
const y = layerIndex * layerHeight;
let x, z;
if (line.start.y === line.end.y) {
x = line.start.x;
z = line.start.z;
@ -33,14 +38,10 @@ export default function calculateLayersIntersections(lines, settings) {
z = line.end.z * alpha + line.start.z * alpha1;
}
layerPoints[layerIndex][lineIndex] = { x: z, y: x };
for (const faceIndex of faces) {
const layerFaceIndex = layerFaceIndexes[layerIndex];
if (!layerFaceIndex.includes(faceIndex)) layerFaceIndex.push(faceIndex);
}
layerIntersectionPoints[layerIndex][lineIndex] = new THREE.Vector2(z, x);
}
}
}
return { layerPoints, layerFaceIndexes };
return { layerIntersectionIndexes, layerIntersectionPoints };
}

View File

@ -1,70 +1,49 @@
import * as vector2 from './helpers/vector2.js';
import * as vector3 from './helpers/vector3.js';
import * as THREE from 'three.js';
export default function createLines(geometry) {
const faces = [];
const lines = [];
const lineLookup = {};
function addLine(geometry, lineLookup, lines, a, b) {
const index = lines.length;
lineLookup[`${a}_${b}`] = index;
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);
lines.push({
line: new THREE.Line3(geometry.vertices[a], geometry.vertices[b]),
connects: [],
normals: []
});
// skip faces that point up or down
if (normal.y > 0.999 || normal.y < -0.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;
if (typeof lineLookup[`${b}_${a}`] !== 'undefined') {
index = lineLookup[`${b}_${a}`];
} else {
const start = getVertex(vertices, a);
const end = getVertex(vertices, b);
const line = { start, end };
const faces = [];
index = lines.length;
lineLookup[`${a}_${b}`] = index;
lines.push({ line, faces });
}
lines[index].faces.push(faceIndex);
return index;
}
function calculateNormal(vertices, a, b, c) {
a = getVertex(vertices, a);
b = getVertex(vertices, b);
c = getVertex(vertices, c);
export default function createLines(geometry, settings) {
console.log('constructing unique lines from geometry');
const cb = vector3.subtract(c, b);
const ab = vector3.subtract(a, b);
const normal = vector3.normalize(vector3.cross(cb, ab));
const lines = [];
const lineLookup = {};
return normal;
}
function getVertex(vertices, i) {
const i3 = i * 3;
return {
x: vertices[i3],
y: vertices[i3 + 1],
z: vertices[i3 + 2]
};
for (let i = 0; i < geometry.faces.length; i ++) {
const face = geometry.faces[i];
if (face.normal.y !== 1 && face.normal.y !== -1) {
const normal = new THREE.Vector2(face.normal.z, face.normal.x).normalize();
const lookupA = lineLookup[`${face.b}_${face.a}`];
const lookupB = lineLookup[`${face.c}_${face.b}`];
const lookupC = lineLookup[`${face.a}_${face.c}`];
// only add unique lines
// returns index of said line
const indexA = lookupA !== undefined ? lookupA : addLine(geometry, lineLookup, lines, face.a, face.b);
const indexB = lookupB !== undefined ? lookupB : addLine(geometry, lineLookup, lines, face.b, face.c);
const indexC = lookupC !== undefined ? lookupC : addLine(geometry, lineLookup, lines, face.c, face.a);
// set connecting lines (based on face)
lines[indexA].connects.push(indexB, indexC);
lines[indexB].connects.push(indexC, indexA);
lines[indexC].connects.push(indexA, indexB);
lines[indexA].normals.push(normal);
lines[indexB].normals.push(normal);
lines[indexC].normals.push(normal);
}
}
return lines;
}

View File

@ -1,70 +1,80 @@
import { PRECISION } from '../constants.js';
import { PRECISION } from '../constants.js'
import getFillTemplate from './getFillTemplate.js';
import Shape from 'Doodle3D/clipper-js';
export default function generateInfills(slices, settings) {
console.log('generating infills');
let {
layerHeight,
innerInfill: { density },
thickness: {
top: topThickness,
bottom: bottomThickness
},
nozzleDiameter
} = settings;
fillGridSize,
bottomThickness,
topThickness,
nozzleDiameter,
infillOverlap
} = settings.config;
density /= 100;
fillGridSize /= PRECISION;
nozzleDiameter /= PRECISION;
infillOverlap /= PRECISION;
const bidirectionalInfill = density < 0.8;
const infillGridSize = nozzleDiameter * (bidirectionalInfill ? 2 : 1) / density;
const bottomSkinCount = Math.ceil(bottomThickness / layerHeight);
const topSkinCount = Math.ceil(topThickness / layerHeight);
const bottomSkinCount = Math.ceil(bottomThickness/layerHeight);
const topSkinCount = Math.ceil(topThickness/layerHeight);
const nozzleRadius = nozzleDiameter / 2;
const outerFillTemplateSize = nozzleDiameter;
const hightemplateSize = Math.sqrt(2 * Math.pow(nozzleDiameter, 2));
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
let surroundingLayer;
if (layer - bottomSkinCount >= 0 && layer + topSkinCount < slices.length) {
const downSkin = slices[layer - bottomSkinCount].outline;
const upSkin = slices[layer + topSkinCount].outline;
const downSkin = slices[layer - bottomSkinCount].getOutline();
const upSkin = slices[layer + topSkinCount].getOutline();
surroundingLayer = upSkin.intersect(downSkin);
}
for (let i = 0; i < slice.parts.length; i ++) {
const even = (layer % 2 === 0);
const part = slice.parts[i];
if (!part.closed) continue;
const innerShell = part.shell[part.shell.length - 1];
if (innerShell.paths.length === 0) continue;
const fillArea = innerShell.offset(-nozzleRadius);
let innerFillArea;
let outerFillArea;
if (surroundingLayer) {
outerFillArea = fillArea.difference(surroundingLayer).intersect(fillArea);
innerFillArea = fillArea.difference(outerFillArea);
} else {
outerFillArea = fillArea;
if (!part.shape.closed) {
continue;
}
if (innerFillArea && innerFillArea.paths.length > 0) {
const bounds = innerFillArea.shapeBounds();
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, bidirectionalInfill || even, bidirectionalInfill || !even);
const outerLine = part.outerLine;
part.innerFill.join(innerFillTemplate.intersect(innerFillArea));
}
if (outerLine.paths.length > 0) {
const inset = (part.innerLines.length > 0) ? part.innerLines[part.innerLines.length - 1] : outerLine;
if (outerFillArea.paths.length > 0) {
const bounds = outerFillArea.shapeBounds();
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
const fillArea = inset.offset(-nozzleRadius);
let lowFillArea;
let highFillArea;
if (surroundingLayer) {
highFillArea = fillArea.difference(surroundingLayer);
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));
if (infillOverlap > 0) {
highFillArea = highFillArea.offset(infillOverlap);
}
highFillArea = highFillArea.intersect(fillArea);
lowFillArea = fillArea.difference(highFillArea);
} else {
highFillArea = fillArea;
}
if (lowFillArea && lowFillArea.paths.length > 0) {
const bounds = lowFillArea.shapeBounds();
const lowFillTemplate = getFillTemplate(bounds, fillGridSize, true, true);
part.fill.join(lowFillTemplate.intersect(lowFillArea));
}
if (highFillArea.paths.length > 0) {
const bounds = highFillArea.shapeBounds();
const even = (layer % 2 === 0);
const highFillTemplate = getFillTemplate(bounds, hightemplateSize, even, !even);
part.fill.join(highFillTemplate.intersect(highFillArea));
}
}
}
}

View File

@ -1,6 +1,6 @@
import { PRECISION } from '../constants.js';
import { PRECISION } from '../constants.js'
const OFFSET_OPTIONS = {
const offsetOptions = {
jointType: 'jtSquare',
endType: 'etClosedPolygon',
miterLimit: 2.0,
@ -8,17 +8,14 @@ const OFFSET_OPTIONS = {
};
export default function generateInnerLines(slices, settings) {
// need to scale up everything because of clipper rounding errors
let {
nozzleDiameter,
thickness: { shell: shellThickness }
} = settings;
console.log('generating outer lines and inner lines');
// need to scale up everything because of clipper rounding errors
let { layerHeight, nozzleDiameter, shellThickness } = settings.config;
nozzleDiameter /= PRECISION;
shellThickness /= PRECISION;
const nozzleRadius = nozzleDiameter / 2;
const numShells = Math.round(shellThickness / nozzleDiameter);
const shells = Math.round(shellThickness / nozzleDiameter);
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
@ -26,28 +23,26 @@ export default function generateInnerLines(slices, settings) {
for (let i = 0; i < slice.parts.length; i ++) {
const part = slice.parts[i];
if (!part.closed) continue;
if (!part.shape.closed) continue;
const outerLine = part.shape.offset(-nozzleRadius, OFFSET_OPTIONS);
const outerLine = part.shape.offset(-nozzleRadius, offsetOptions);
if (outerLine.paths.length === 0) continue;
if (outerLine.paths.length > 0) {
part.outerLine.join(outerLine);
part.shell.push(outerLine);
for (let shell = 1; shell < shells; shell += 1) {
const offset = shell * nozzleDiameter;
// start with 1 because outerLine is the 1st (0) shell
for (let inset = 1; inset < numShells; inset += 1) {
const offset = inset * nozzleDiameter;
const innerLine = outerLine.offset(-offset, offsetOptions);
const shell = outerLine.offset(-offset, OFFSET_OPTIONS);
if (shell.paths.length === 0) {
break;
} else {
part.shell.push(shell);
if (innerLine.paths.length > 0) {
part.innerLines.push(innerLine);
}
else {
break;
}
}
}
}
slice.parts = slice.parts.filter(part => !part.closed || part.shell.length !== 0);
}
}

View File

@ -1,15 +0,0 @@
import Shape from '@doodle3d/clipper-js';
export default function calculateOutlines(slices) {
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
slice.outline = slice.parts.reduce((shape, part) => {
if (part.closed) {
const [outerLine] = part.shell;
shape.join(outerLine);
}
return shape;
}, new Shape([], true));
}
}

View File

@ -1,41 +1,76 @@
import getFillTemplate from './getFillTemplate.js';
import Shape from '@doodle3d/clipper-js';
import Shape from 'Doodle3D/clipper-js';
import { PRECISION } from '../constants.js';
const PRECISION_SQUARED = Math.pow(PRECISION, 2);
export default function generateSupport(slices, settings) {
if (!settings.support.enabled) return;
console.log('generating support');
if (!settings.config.supportEnabled) return;
let {
layerHeight,
support: { density, margin, minArea, distanceY },
supportGridSize,
supportAcceptanceMargin,
supportPlateSize: plateSize,
supportDistanceY,
nozzleDiameter
} = settings;
} = settings.config;
density /= 100;
margin /= PRECISION;
supportGridSize /= PRECISION;
supportMargin /= PRECISION;
plateSize /= PRECISION;
nozzleDiameter /= PRECISION;
var supportDistanceLayers = Math.max(Math.ceil(supportDistanceY / layerHeight), 1);
const infillGridSize = nozzleDiameter * 2 / density;
const supportDistanceLayers = Math.max(Math.ceil(distanceY / layerHeight), 1);
var supportAreas = new Shape([], true);
let supportArea = new Shape([], true);
for (var layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) {
var currentSlice = slices[layer];
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 (supportAreas.length > 0) {
const neededSupportArea = upSkin.outline.difference(currentLayer.outline.offset(margin));
if (layer >= supportDistanceLayers) {
var sliceSkin = slices[layer - supportDistanceLayers].getOutline();
sliceSkin = sliceSkin;
if (neededSupportArea.totalArea() * PRECISION_SQUARED > minArea) supportArea = supportArea.union(neededSupportArea);
if (downSkin) supportArea = supportArea.difference(downSkin.outline.offset(margin));
var supportAreasSlimmed = supportAreas.difference(sliceSkin.offset(supportMargin));
if (supportAreasSlimmed.area() < 100.0) {
supportAreas = supportAreas.difference(sliceSkin);
}
else {
supportAreas = supportAreasSlimmed;
}
}
const bounds = supportArea.shapeBounds();
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, true, true);
var supportTemplate = getFillTemplate(supportAreas.bounds(), supportGridSize, true, true);
var supportFill = supportTemplate.intersect(supportAreas);
if (supportFill.length === 0) {
currentSlice.support = supportAreas.clone();
}
else {
currentSlice.support = supportFill;
}
}
slices[layer].support = supportArea.clone().join(supportArea.intersect(innerFillTemplate));
slices[layer].supportOutline = supportArea;
var supportSkin = slices[layer + supportDistanceLayers - 1].getOutline();
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(supportAcceptanceMargin);
}
var overlap = supportSkin.offset(supportAcceptanceMargin).intersect(outerLine);
var overhang = outerLine.difference(overlap);
if (overlap.length === 0 || overhang.length > 0) {
supportAreas = supportAreas.join(overhang);
}
}
}
}

View File

@ -1,10 +1,8 @@
import Shape from '@doodle3d/clipper-js';
import Shape from 'Doodle3D/clipper-js';
export default function getFillTemplate(bounds, gridSize, even, uneven) {
export default function getFillTemplate(bounds, size, even, uneven) {
const paths = [];
const size = Math.sqrt(2 * Math.pow(gridSize, 2));
const left = Math.floor(bounds.left / size) * size;
const right = Math.ceil(bounds.right / size) * size;
const top = Math.floor(bounds.top / size) * size;

View File

@ -1,149 +0,0 @@
import { distanceTo } from './vector2.js';
import { VERSION } from '../../constants.js';
export const MOVE = 'G';
export const M_COMMAND = 'M';
export const FAN_SPEED = 'S';
export const SPEED = 'F';
export const EXTRUDER = 'E';
export const POSITION_X = 'X';
export const POSITION_Y = 'Y';
export const POSITION_Z = 'Z';
export default class GCode {
constructor(settings) {
this._nozzleToFilamentRatio = 1;
this._gcode = [
`; ${JSON.stringify(settings)}`,
`; Generated with Doodle3D Slicer V${VERSION}`
];
this._currentValues = {};
this._nozzlePosition = { x: 0, y: 0 };
this._extruder = 0.0;
this._duration = 0.0;
this._isRetracted = false;
this._isFanOn = false;
}
_addGCode(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) {
this._isFanOn = true;
const gcode = { [M_COMMAND]: 106 };
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
this._addGCode(gcode);
return this;
}
turnFanOff() {
this._isFanOn = false;
this._addGCode({ [M_COMMAND]: 107 });
return this;
}
moveTo(x, y, z, { speed }) {
const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._duration += lineLength / speed;
this._addGCode({
[MOVE]: 0,
[POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z,
[SPEED]: speed * 60
});
this._nozzlePosition = newNozzlePosition;
return this;
}
lineTo(x, y, z, { speed, flowRate }) {
const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
this._duration += lineLength / speed;
this._addGCode({
[MOVE]: 1,
[POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z,
[SPEED]: speed * 60,
[EXTRUDER]: this._extruder
});
this._nozzlePosition = newNozzlePosition;
return this;
}
unRetract({ enabled, speed, minDistance, amount }) {
if (this._isRetracted && enabled) {
this._isRetracted = false;
if (this._extruder > minDistance) {
this._duration += amount / speed;
this._addGCode({
[MOVE]: 0,
[EXTRUDER]: this._extruder,
[SPEED]: speed * 60
});
}
}
return this;
}
retract({ enabled, speed, minDistance, amount }) {
if (!this._isRetracted && enabled) {
this._isRetracted = true;
if (this._extruder > minDistance) {
this._duration += amount / speed;
this._addGCode({
[MOVE]: 0,
[EXTRUDER]: this._extruder - amount,
[SPEED]: speed * 60
});
}
}
return this;
}
addGCode(gcode, { temperature, bedTemperature, heatedBed }) {
gcode = gcode
.replace(/{temperature}/g, temperature)
.replace(/{if heatedBed}.*?\n/g, str => heatedBed ? str.replace(/{if heatedBed}/g, '') : '')
.replace(/{bedTemperature}/g, bedTemperature);
this._addGCode(gcode);
}
getGCode() {
return {
gcode: this._gcode,
duration: this._duration,
filament: this._extruder
};
}
}

View File

@ -1,18 +0,0 @@
import Shape from '@doodle3d/clipper-js';
export default class Slice {
constructor() {
this.parts = [];
}
add(shape, closed) {
const part = { shape, closed };
if (closed) {
part.shell = [];
part.innerFill = new Shape([], false);
part.outerFill = new Shape([], false);
}
this.parts.push(part);
}
}

View File

@ -1,26 +0,0 @@
export function hslToRgb(h, s, l) {
let r;
let g;
let b;
if (s === 0) {
r = g = b = l;
} 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;
}

View File

@ -1,197 +0,0 @@
import { angle, subtract, distanceTo } from './vector2.js';
const graphs = new WeakMap();
export default function comb(polygons, start, end) {
if (!graphs.has(polygons)) graphs.set(polygons, createGraph(polygons));
let { edges, graph, points } = graphs.get(polygons);
points = [...points, start, end];
graph = [...graph];
const startNode = createNode(graph, points, edges, start);
const endNode = createNode(graph, points, edges, end);
let result;
if (graph[startNode].some(node => node.to === endNode)) {
result = [start, end];
} else {
const path = shortestPath(graph, startNode, endNode);
if (path) {
result = path.map(index => points[index]);
} else {
result = [start, end];
}
}
return result;
}
function createGraph(polygons) {
const points = [];
const edges = [];
const nextPoints = new WeakMap();
const previousPoints = new WeakMap();
for (let i = 0; i < polygons.length; i ++) {
const polygon = polygons[i];
for (let j = 0; j < polygon.length; j ++) {
const point = polygon[j];
const nextPoint = polygon[(j + 1) % polygon.length];
const previousPoint = polygon[(j - 1 + polygon.length) % polygon.length];
points.push(point);
edges.push([point, nextPoint]);
nextPoints.set(point, nextPoint);
previousPoints.set(point, previousPoint);
}
}
const graph = points.map(() => ([]));
for (let i = 0; i < points.length; i ++) {
const a = points[i];
for (let j = i + 1; j < points.length; j ++) {
const b = points[j];
const nextPoint = nextPoints.get(a);
const previousPoint = previousPoints.get(a);
if (!lineIsVisible(previousPoint, nextPoint, edges, a, b)) continue;
const distance = distanceTo(a, b);
const connectNodeA = graph[i];
connectNodeA.push({ to: j, distance });
const connectNodeB = graph[j];
connectNodeB.push({ to: i, distance });
}
}
return { graph, edges, points };
}
function createNode(graph, points, edges, point) {
const node = [];
const to = graph.length;
graph.push(node);
let previousPoint;
let nextPoint;
for (let j = 0; j < edges.length; j ++) {
const edge = edges[j];
if (pointOnLine(edge, point)) [previousPoint, nextPoint] = edge;
}
for (let i = 0; i < graph.length; i ++) {
const b = points[i];
if (!lineIsVisible(previousPoint, nextPoint, edges, point, b)) continue;
const distance = distanceTo(point, b);
node.push({ to: i, distance });
graph[i] = [...graph[i], { to, distance }];
}
return to;
}
function lineIsVisible(previousPoint, nextPoint, edges, a, b) {
if (b === nextPoint || b === previousPoint) return true;
if (previousPoint && nextPoint) {
const angleLine = angle(subtract(b, a));
const anglePrevious = angle(subtract(previousPoint, a));
const angleNext = angle(subtract(nextPoint, a));
if (betweenAngles(angleLine, anglePrevious, angleNext)) return false;
}
if (lineCrossesEdges(edges, a, b)) return false;
return true;
}
function lineCrossesEdges(edges, a, b) {
for (let i = 0; i < edges.length; i ++) {
const [c, d] = edges[i];
if (lineSegmentsCross(a, b, c, d)) return true;
}
return false;
}
function lineSegmentsCross(a, b, c, d) {
const denominator = ((b.x - a.x) * (d.y - c.y)) - ((b.y - a.y) * (d.x - c.x));
if (denominator === 0.0) return false;
const numerator1 = ((a.y - c.y) * (d.x - c.x)) - ((a.x - c.x) * (d.y - c.y));
const numerator2 = ((a.y - c.y) * (b.x - a.x)) - ((a.x - c.x) * (b.y - a.y));
if (numerator1 === 0.0 || numerator2 === 0.0) return false;
const r = numerator1 / denominator;
const s = numerator2 / denominator;
return (r > 0.0 && r < 1.0) && (s >= 0.0 && s <= 1.0);
}
const TAU = Math.PI * 2.0;
function normalizeAngle(a) {
a %= TAU;
return a > 0.0 ? a : a + TAU;
}
function betweenAngles(n, a, b) {
n = normalizeAngle(n);
a = normalizeAngle(a);
b = normalizeAngle(b);
return a < b ? a <= n && n <= b : a <= n || n <= b;
}
// dijkstra's algorithm
function shortestPath(graph, start, end) {
const distances = graph.map(() => Infinity);
distances[start] = 0;
const traverse = [];
const queue = [];
for (let i = 0; i < distances.length; i ++) {
queue.push(i);
}
while (queue.length > 0) {
let queueIndex;
let minDistance = Infinity;
for (let index = 0; index < queue.length; index ++) {
const nodeIndex = queue[index];
const distance = distances[nodeIndex];
if (distances[nodeIndex] < minDistance) {
queueIndex = index;
minDistance = distance;
}
}
const [nodeIndex] = queue.splice(queueIndex, 1);
const node = graph[nodeIndex];
for (let i = 0; i < node.length; i ++) {
const child = node[i];
const distance = distances[nodeIndex] + child.distance;
if (distance < distances[child.to]) {
distances[child.to] = distance;
traverse[child.to] = nodeIndex;
}
}
}
if (!traverse.hasOwnProperty(end)) return null;
const path = [end];
let nodeIndex = end;
do {
nodeIndex = traverse[nodeIndex];
path.push(nodeIndex);
} while (nodeIndex !== start);
return path.reverse();
}
function pointOnLine([a, b], point) {
return (a.x - point.x) * (a.y - point.y) === (b.x - point.x) * (b.y - point.y);
}

View File

@ -1,34 +0,0 @@
export const subtract = (a, b) => ({
x: a.x - b.x,
y: a.y - b.y
});
export const add = (a, b) => ({
x: a.x + b.x,
y: a.y + b.y
});
export const scale = (v, factor) => ({
x: v.x * factor,
y: v.y * factor
});
export const divide = (v, factor) => ({
x: v.x / factor,
y: v.y / factor
});
export const normal = (v) => ({
x: -v.y,
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 length = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
export const distanceTo = (a, b) => length(subtract(a, b));
export const angle = (v) => Math.atan2(v.y, v.x);
export const normalize = (v) => {
const l = length(v);
return {
x: v.x / l,
y: v.y / l
};
};

View File

@ -1,38 +0,0 @@
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
};
};

View File

@ -1,163 +1,120 @@
import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js';
import * as THREE from 'three.js';
import Shape from 'Doodle3D/clipper-js';
export default function intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings) {
console.log('generating slices');
export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes) {
const layers = [];
for (let layer = 0; layer < layerPoints.length; layer ++) {
const fillShapes = [];
const lineShapesOpen = [];
const lineShapesClosed = [];
for (let layer = 1; layer < layerIntersectionIndexes.length; layer ++) {
const intersectionIndexes = layerIntersectionIndexes[layer];
const intersectionPoints = layerIntersectionPoints[layer];
const points = layerPoints[layer];
const faceIndexes = layerFaceIndexes[layer];
if (intersectionIndexes.length === 0) continue;
if (faceIndexes.length === 0) continue;
const closedShapes = [];
const openShapes = [];
for (let i = 0; i < intersectionIndexes.length; i ++) {
let index = intersectionIndexes[i];
const shapes = {};
if (intersectionPoints[index] === undefined) continue;
const startConnects = {};
const endConnects = {};
const shape = [];
for (let i = 0; i < faceIndexes.length; i ++) {
const faceIndex = faceIndexes[i];
const { lineIndexes, flatNormal, objectIndex } = faces[faceIndex];
const firstPoints = [index];
let isFirstPoint = true;
let closed = false;
const a = lineIndexes[0];
const b = lineIndexes[1];
const c = lineIndexes[2];
while (index !== -1) {
const intersection = intersectionPoints[index];
// uppercase X and Y because clipper vector
shape.push(intersection);
let pointA;
let pointB;
if (points[a] && points[b]) {
pointA = a;
pointB = b;
} else if (points[b] && points[c]) {
pointA = b;
pointB = c;
} else if (points[c] && points[a]) {
pointA = c;
pointB = a;
} else {
// should never happen
continue;
}
delete intersectionPoints[index];
const segmentNormal = normalize(normal(subtract(points[pointA], points[pointB])));
if (dot(segmentNormal, flatNormal) < 0) {
const temp = pointB;
pointB = pointA;
pointA = temp;
}
const connects = lines[index].connects;
const faceNormals = lines[index].normals;
if (endConnects[pointA]) {
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;
shapes[objectIndex].splice(shapes[objectIndex].indexOf(startConnects[pointB]), 1);
for (let i = 0; i < connects.length; i ++) {
index = connects[i];
if (firstPoints.indexOf(index) !== -1 && shape.length > 2) {
closed = true;
index = -1;
break;
}
} 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;
shapes[objectIndex].splice(shapes[objectIndex].indexOf(endConnects[pointA]), 1);
} else {
lineSegment.unshift(pointA);
startConnects[pointA] = lineSegment;
}
} else {
const lineSegment = [pointA, pointB];
startConnects[pointA] = lineSegment;
endConnects[pointB] = lineSegment;
if (!shapes[objectIndex]) shapes[objectIndex] = [];
shapes[objectIndex].push(lineSegment);
// Check if index has an intersection or is already used
if (intersectionPoints[index] !== undefined) {
const faceNormal = faceNormals[Math.floor(i / 2)];
const a = new THREE.Vector2(intersection.x, intersection.y);
const b = new THREE.Vector2(intersectionPoints[index].x, intersectionPoints[index].y);
// can't calculate normal between points if distance is smaller as 0.0001
if ((faceNormal.x === 0 && faceNormal.y === 0) || a.distanceTo(b) < 0.0001) {
if (isFirstPoint) {
firstPoints.push(index);
}
delete intersectionPoints[index];
connects.push(...lines[index].connects);
faceNormals.push(...lines[index].normals);
index = -1;
} else {
// make sure the path goes the right direction
// THREE.Vector2.normal is not yet implimented
// const normal = a.sub(b).normal().normalize();
const normal = a.sub(b);
normal.set(-normal.y, normal.x).normalize();
if (normal.dot(faceNormal) > 0) {
break;
} else {
index = -1;
}
}
} else {
index = -1;
}
}
isFirstPoint = false;
}
if (!closed) {
index = firstPoints[0];
while (index !== -1) {
if (firstPoints.indexOf(index) === -1) {
const intersection = intersectionPoints[index];
shape.unshift(intersection);
delete intersectionPoints[index];
}
const connects = lines[index].connects;
for (let i = 0; i < connects.length; i ++) {
index = connects[i];
if (intersectionPoints[index] !== undefined) {
break;
} else {
index = -1;
}
}
}
}
if (closed) {
closedShapes.push(shape);
} else {
openShapes.push(shape);
}
}
for (const objectIndex in shapes) {
const shape = shapes[objectIndex]
.map(lineSegment => lineSegment.map(pointIndex => points[pointIndex]))
.filter(lineSegment => lineSegment.some(point => !almostEquals(lineSegment[0], point)));
const openShape = openObjectIndexes[objectIndex];
const connectPoints = [];
for (let pathIndex = 0; pathIndex < shape.length; pathIndex ++) {
const path = shape[pathIndex];
if (almostEquals(path[0], path[path.length - 1])) {
if (openShape) {
lineShapesClosed.push(path);
} else {
fillShapes.push(path);
}
continue;
}
let shapeStartPoint = path[0];
const connectNext = connectPoints.find(({ point }) => almostEquals(point, shapeStartPoint));
if (connectNext) {
connectNext.next = pathIndex;
} else {
connectPoints.push({ point: shapeStartPoint, next: pathIndex, previous: -1 });
}
let shapeEndPoint = path[path.length - 1];
const connectPrevious = connectPoints.find(({ point }) => almostEquals(point, shapeEndPoint));
if (connectPrevious) {
connectPrevious.previous = pathIndex;
} else {
connectPoints.push({ point: shapeEndPoint, next: -1, previous: pathIndex });
}
}
connectPoints.sort((a, b) => b.previous - a.previous);
while (connectPoints.length !== 0) {
let { next, previous } = connectPoints.pop();
const line = [];
if (previous !== -1) line.push(...shape[previous]);
while (true) {
const pointIndex = connectPoints.findIndex(point => point.previous === next);
if (pointIndex === -1) break;
const point = connectPoints[pointIndex];
line.push(...shape[point.previous]);
connectPoints.splice(pointIndex, 1);
if (point.next === -1) break;
if (point.next === previous) break;
next = point.next;
}
if (openShape) {
if (almostEquals(line[0], line[line.length - 1])) {
lineShapesClosed.push(line);
} else {
lineShapesOpen.push(line);
}
} else {
fillShapes.push(line);
}
}
}
layers.push({ fillShapes, lineShapesOpen, lineShapesClosed });
layers.push({ closedShapes, openShapes });
}
return layers;

View File

@ -1,27 +1,20 @@
import { distanceTo } from './helpers/vector2.js';
import Shape from '@doodle3d/clipper-js';
import * as THREE from 'three.js';
import Shape from 'Doodle3D/clipper-js';
export default function optimizePaths(slices) {
let start = { x: 0, y: 0 };
export default function optimizePaths(slices, settings) {
console.log('optimize paths');
const start = new THREE.Vector2(0, 0);
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
if (slice.brim !== undefined && slice.brim.paths.length > 0) {
slice.brim = optimizeShape(slice.brim, start);
start = slice.brim.lastPoint(true);
start.copy(slice.brim.lastPoint(true));
}
const parts = [];
const boundingBoxes = new WeakMap();
for (let i = 0; i < slice.parts.length; i ++) {
const part = slice.parts[i];
const shape = part.closed ? part.shell[0] : part.shape;
const bounds = shape.shapeBounds();
boundingBoxes.set(part, bounds);
}
while (slice.parts.length > 0) {
let closestDistance = Infinity;
@ -29,14 +22,20 @@ export default function optimizePaths(slices) {
for (let i = 0; i < slice.parts.length; i ++) {
const part = slice.parts[i];
const bounds = boundingBoxes.get(part);
const topDistance = bounds.top - start.y;
const bottomDistance = start.y - bounds.bottom;
const leftDistance = bounds.left - start.x;
const rightDistance = start.x - bounds.right;
let bounds;
if (part.shape.closed) {
bounds = part.outerLine.shapeBounds();
} else {
bounds = part.shape.shapeBounds();
}
const distance = Math.max(topDistance, bottomDistance, leftDistance, rightDistance);
const top = bounds.top - start.y;
const bottom = start.y - bounds.bottom;
const left = bounds.left - start.x;
const right = start.x - bounds.right;
const distance = Math.max(top, bottom, left, right);
if (distance < closestDistance) {
closestDistance = distance;
@ -44,64 +43,67 @@ export default function optimizePaths(slices) {
}
}
const [part] = slice.parts.splice(closestPart, 1);
const part = slice.parts.splice(closestPart, 1)[0];
parts.push(part);
if (part.closed) {
for (let i = 0; i < part.shell.length; i ++) {
const shell = part.shell[i];
if (shell.paths.length === 0) continue;
part.shell[i] = optimizeShape(shell, start);
start = part.shell[i].lastPoint(true);
if (part.shape.closed) {
if (part.outerLine.paths.length > 0) {
part.outerLine = optimizeShape(part.outerLine, start);
start.copy(part.outerLine.lastPoint(true));
}
if (part.outerFill.paths.length > 0) {
part.outerFill = optimizeShape(part.outerFill, start);
start = part.outerFill.lastPoint(true);
for (let i = 0; i < part.innerLines.length; i ++) {
const innerLine = part.innerLines[i];
if (innerLine.paths.length > 0) {
part.innerLines[i] = optimizeShape(innerLine, start);
start.copy(part.innerLines[i].lastPoint(true));
}
}
if (part.innerFill.paths.length > 0) {
part.innerFill = optimizeShape(part.innerFill, start);
start = part.innerFill.lastPoint(true);
if (part.fill.paths.length > 0) {
part.fill = optimizeShape(part.fill, start);
start.copy(part.fill.lastPoint(true));
}
} else {
part.shape = optimizeShape(part.shape, start);
start = part.shape.lastPoint(true);
start.copy(part.shape.lastPoint(true));
}
}
slice.parts = parts;
if (typeof slice.support !== 'undefined' && slice.support.paths.length > 0) {
if (slice.support !== undefined && slice.support.length > 0) {
slice.support = optimizeShape(slice.support, start);
start = slice.support.lastPoint(true);
start.copy(slice.support.lastPoint(true));
}
}
}
function optimizeShape(shape, start) {
const inputPaths = shape.mapToLower().filter(path => path.length > 0);
start = start.clone();
const inputPaths = shape.mapToLower();
const optimizedPaths = [];
const donePaths = [];
while (optimizedPaths.length !== inputPaths.length) {
let minLength = Infinity;
let minLength = false;
let reverse;
let minPath;
let offset;
let pathIndex;
for (let i = 0; i < inputPaths.length; i ++) {
if (donePaths.includes(i)) continue;
if (donePaths.indexOf(i) !== -1) continue;
const path = inputPaths[i];
if (shape.closed) {
for (let j = 0; j < path.length; j += 1) {
const length = distanceTo(path[j], start);
if (length < minLength) {
const point = new THREE.Vector2().copy(path[j]);
const length = point.sub(start).length();
if (minLength === false || length < minLength) {
minPath = path;
minLength = length;
offset = j;
@ -109,15 +111,17 @@ function optimizeShape(shape, start) {
}
}
} else {
const lengthToStart = distanceTo(path[0], start);
if (lengthToStart < minLength) {
const startPoint = new THREE.Vector2().copy(path[0]);
const lengthToStart = startPoint.sub(start).length();
if (minLength === false || lengthToStart < minLength) {
minPath = path;
minLength = lengthToStart;
reverse = false;
pathIndex = i;
}
const lengthToEnd = distanceTo(path[path.length - 1], start);
const endPoint = new THREE.Vector2().copy(path[path.length - 1]);
const lengthToEnd = endPoint.sub(start).length();
if (lengthToEnd < minLength) {
minPath = path;
minLength = lengthToEnd;
@ -127,15 +131,20 @@ function optimizeShape(shape, start) {
}
}
let point;
if (shape.closed) {
minPath = minPath.concat(minPath.splice(0, offset));
start = minPath[0];
point = minPath[0];
} else {
if (reverse) minPath.reverse();
start = minPath[minPath.length - 1];
if (reverse) {
minPath.reverse();
}
point = minPath[minPath.length - 1];
}
donePaths.push(pathIndex);
start.copy(point);
optimizedPaths.push(minPath);
}

View File

@ -1,30 +1,34 @@
import * as THREE from 'three.js';
import { PRECISION } from '../constants.js';
const inversePrecision = 1 / PRECISION;
export default function removePrecision(slices) {
console.log('remove precision');
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
for (let i = 0; i < slice.parts.length; i ++) {
const part = slice.parts[i];
if (part.closed) {
for (let i = 0; i < part.shell.length; i ++) {
const innerLine = part.shell[i];
if (part.shape.closed) {
part.outerLine.scaleDown(inversePrecision);
for (let i = 0; i < part.innerLines.length; i ++) {
const innerLine = part.innerLines[i];
innerLine.scaleDown(inversePrecision);
}
part.innerFill.scaleDown(inversePrecision);
part.outerFill.scaleDown(inversePrecision);
} else {
part.shape.scaleDown(inversePrecision);
part.fill.scaleDown(inversePrecision);
}
// else {
// part.shape.scaleDown(inversePrecision);
// }
}
if (typeof slice.support !== 'undefined') {
if (slice.support !== undefined) {
slice.support.scaleDown(inversePrecision);
}
if (typeof slice.brim !== 'undefined') {
if (slice.brim !== undefined) {
slice.brim.scaleDown(inversePrecision);
}
}

View File

@ -1,43 +1,42 @@
import Shape from '@doodle3d/clipper-js';
import Slice from './helpers/Slice.js';
import Shape from 'Doodle3D/clipper-js';
import Slice from '../Slice.js';
import { PRECISION, MIN_AREA } from '../constants.js';
import { CLEAN_DELTA, PRECISION } from '../constants.js';
export default function shapesToSlices(shapes) {
const cleanDelta = CLEAN_DELTA / PRECISION;
export default function shapesToSlices(shapes, settings) {
const sliceLayers = [];
for (let layer = 0; layer < shapes.length; layer ++) {
let { fillShapes, lineShapesOpen, lineShapesClosed } = shapes[layer];
let { closedShapes, openShapes } = shapes[layer];
fillShapes = new Shape(fillShapes, true, true, true, true)
closedShapes = new Shape(closedShapes, true, true, true, true)
.fixOrientation()
.simplify('pftNonZero')
.clean(1)
.thresholdArea(MIN_AREA / Math.pow(PRECISION, 2))
.separateShapes();
.clean(cleanDelta)
.seperateShapes();
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
.clean(1);
lineShapesOpen = new Shape(lineShapesOpen, false, true, true, true);
// .clean(1);
openShapes = new Shape(openShapes, false, true, true, true);
// .clean(cleanDelta);
// TODO
// Enable cleaning when https://sourceforge.net/p/jsclipper/tickets/24/ is fixed
// Cleaning is actually wanted here but there is a bug in the clean function
// https://sourceforge.net/p/jsclipper/tickets/16/
const slice = new Slice();
for (let i = 0; i < fillShapes.length; i ++) {
const fillShape = fillShapes[i];
if (fillShape.paths.length === 0) continue;
for (let i = 0; i < closedShapes.length; i ++) {
const closedShape = closedShapes[i];
slice.add(closedShape);
slice.add(fillShape, true);
if (lineShapesClosed.paths.length > 0) lineShapesClosed = lineShapesClosed.difference(fillShape);
if (lineShapesOpen.paths.length > 0) lineShapesOpen = lineShapesOpen.difference(fillShape);
// if (openShapes.path.length > 0) {
// openShapes = openShapes.difference(closedShape);
// }
}
if (lineShapesClosed.paths.length > 0) slice.add(lineShapesClosed, false);
if (lineShapesOpen.paths.length > 0) slice.add(lineShapesOpen, false);
if (openShapes.paths.length > 0) {
slice.add(openShapes);
}
sliceLayers.push(slice);
}

View File

@ -2,7 +2,6 @@ import calculateLayersIntersections from './calculateLayersIntersections.js';
import createLines from './createLines.js';
import generateInfills from './generateInfills.js';
import generateInnerLines from './generateInnerLines.js';
import generateOutlines from './generateOutlines.js';
import generateSupport from './generateSupport.js';
import intersectionsToShapes from './intersectionsToShapes.js';
import addBrim from './addBrim.js';
@ -10,113 +9,31 @@ import optimizePaths from './optimizePaths.js';
import shapesToSlices from './shapesToSlices.js';
import slicesToGCode from './slicesToGCode.js';
import applyPrecision from './applyPrecision.js';
import { hslToRgb } from './helpers/color.js';
import removePrecision from './removePrecision.js';
export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
const total = 11;
let done = -1;
const updateProgress = action => {
done ++;
if (onProgress) onProgress({ progress: { done, total, action } });
};
export default function(geometry, settings) {
// get unique lines from geometry;
const lines = createLines(geometry, settings);
updateProgress('Constructing unique lines from geometry');
const { lines, faces } = createLines(geometry, settings);
const {
layerIntersectionIndexes,
layerIntersectionPoints
} = calculateLayersIntersections(lines, settings);
updateProgress('Calculating layer intersections');
const { layerPoints, layerFaceIndexes } = calculateLayersIntersections(lines, settings);
updateProgress('Constructing shapes from intersections');
const shapes = intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings);
const shapes = intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings);
applyPrecision(shapes);
updateProgress('Constructing slices from shapes');
const slices = shapesToSlices(shapes, settings);
updateProgress('Generating inner lines');
generateInnerLines(slices, settings);
updateProgress('Generating out lines');
generateOutlines(slices, settings);
updateProgress('Generating infills');
generateInfills(slices, settings);
updateProgress('Generating support');
generateSupport(slices, settings);
updateProgress('Adding brim');
addBrim(slices, settings);
updateProgress('Optimizing paths');
optimizePaths(slices, settings);
removePrecision(slices);
updateProgress('Constructing gcode');
const gcode = slicesToGCode(slices, settings);
updateProgress('Finished');
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
gcode.gcode = new Blob([gcodeToString(gcode.gcode)], { type: 'text/plain' });
return gcode;
}
const PRECISION = 1000;
function toFixedTrimmed(value) {
return (Math.round(value * PRECISION) / PRECISION).toString();
}
function gcodeToString(gcode) {
const currentValues = {};
return gcode.reduce((string, command) => {
if (typeof command === 'string') {
string += command;
} else {
let first = true;
for (const action in command) {
const value = toFixedTrimmed(command[action]);
const currentValue = currentValues[action];
if (first) {
string += `${action}${value}`;
first = false;
} else if (currentValue !== value) {
string += ` ${action}${value}`;
currentValues[action] = value;
}
}
}
string += '\n';
return string;
}, '');
}
const MAX_SPEED = 100 * 60;
function createGcodeGeometry(gcode) {
const positions = [];
const colors = [];
let lastPoint = [0, 0, 0];
for (let i = 0; i < gcode.length; i ++) {
const command = gcode[i];
if (typeof command === 'string') continue;
const { G, F, X, Y, Z } = command;
if (X || Y || Z) {
if (G === 1) {
positions.push(lastPoint.Y, lastPoint.Z, lastPoint.X);
positions.push(Y, Z, X);
const color = (G === 0) ? [0, 1, 0] : hslToRgb(F / MAX_SPEED, 0.5, 0.5);
colors.push(...color, ...color);
}
lastPoint = { X, Y, Z };
}
}
return {
positions: new Float32Array(positions),
colors: new Float32Array(colors)
};
}

View File

@ -1,88 +1,49 @@
import GCode from './helpers/GCode.js';
import comb from './helpers/comb.js';
import { Z_OFFSET } from '../constants.js';
const PROFILE_TYPES = ['support', 'innerShell', 'outerShell', 'innerInfill', 'outerInfill', 'brim'];
import GCode from '../GCode.js';
export default function slicesToGCode(slices, settings) {
const {
layerHeight,
filamentThickness,
nozzleDiameter,
retraction,
travel,
combing
} = settings;
console.log('slices to gcode');
const gcode = new GCode(settings);
gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness);
if (settings.startCode) gcode.addGCode(settings.startCode, settings);
const defaultProfile = {
travelProfile: travel,
retractionProfile: retraction
};
let isFirstLayer = true;
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
const z = layer * layerHeight + Z_OFFSET;
if (layer === 1) {
gcode.updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness);
gcode.turnFanOn();
isFirstLayer = false;
gcode.bottom = false;
}
const profiles = PROFILE_TYPES.reduce((_profiles, profileType) => {
_profiles[profileType] = {
...defaultProfile,
lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType]
};
return _profiles;
}, {});
if (typeof slice.brim !== 'undefined') {
pathToGCode(null, false, gcode, slice.brim, true, true, z, profiles.brim);
if (slice.brim !== undefined) {
pathToGCode(gcode, slice.brim, true, true, layer, 'brim');
}
for (let i = 0; i < slice.parts.length; i ++) {
const part = slice.parts[i];
if (part.closed) {
const outline = part.shell[0].mapToLower();
if (part.shape.closed) {
pathToGCode(gcode, part.outerLine, false, true, layer, 'outerLine');
for (let i = 0; i < part.shell.length; i ++) {
const shell = part.shell[i];
const isOuterShell = i === 0;
const unRetract = isOuterShell;
const profile = isOuterShell ? profiles.outerShell : profiles.innerShell;
pathToGCode(outline, combing, gcode, shell, false, unRetract, z, profile);
for (let i = 0; i < part.innerLines.length; i ++) {
const innerLine = part.innerLines[i];
pathToGCode(gcode, innerLine, false, false, layer, 'innerLine');
}
pathToGCode(outline, combing, gcode, part.outerFill, false, false, z, profiles.outerInfill);
pathToGCode(outline, combing, gcode, part.innerFill, true, false, z, profiles.innerInfill);
pathToGCode(gcode, part.fill, true, false, layer, 'fill');
} else {
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
const retract = !(slice.parts.length === 1 && slice.support === undefined);
pathToGCode(gcode, part.shape, retract, retract, layer, 'outerLine');
}
}
if (typeof slice.support !== 'undefined') {
const supportOutline = slice.supportOutline.mapToLower();
pathToGCode(supportOutline, combing, gcode, slice.support, true, true, z, profiles.support);
if (slice.support !== undefined) {
pathToGCode(gcode, slice.support, true, true, layer, 'support');
}
}
if (settings.endCode) gcode.addGCode(settings.endCode, settings);
return gcode.getGCode();
}
function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, profiles) {
const { lineProfile, travelProfile, retractionProfile } = profiles;
function pathToGCode(gcode, shape, retract, unRetract, layer, type) {
const { closed } = shape;
const paths = shape.mapToLower();
@ -94,26 +55,20 @@ function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, prof
const point = line[i % line.length];
if (i === 0) {
if (combing) {
const combPath = comb(outline, gcode._nozzlePosition, point);
for (let i = 0; i < combPath.length; i ++) {
const combPoint = combPath[i];
gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);
}
} else {
gcode.moveTo(point.x, point.y, z, travelProfile);
}
// TODO
// moveTo should impliment combing
gcode.moveTo(point.x, point.y, layer);
if (unRetract) {
gcode.unRetract(retractionProfile);
gcode.unRetract();
}
} else {
gcode.lineTo(point.x, point.y, z, lineProfile);
gcode.lineTo(point.x, point.y, layer, type);
}
}
}
if (retract) {
gcode.retract(retractionProfile);
gcode.retract();
}
}

View File

@ -1,127 +0,0 @@
import * as THREE from 'three';
import slice from './sliceActions/slice.js';
import SlicerWorker from './slicer.worker.js';
export function sliceMesh(settings, mesh, sync = false, constructLinePreview = false, onProgress) {
if (!mesh || !mesh.isMesh) {
throw new Error('Provided mesh is not intance of THREE.Mesh');
}
mesh.updateMatrix();
const { geometry, matrix, material } = mesh;
return sliceGeometry(settings, geometry, material, matrix, sync, constructLinePreview, onProgress);
}
export function sliceGeometry(settings, geometry, materials, matrix, sync = false, constructLinePreview = false, onProgress) {
if (!geometry) {
throw new Error('Missing required geometry argument');
} else if (geometry.isBufferGeometry) {
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
} else if (geometry.isGeometry) {
geometry = geometry.clone();
} else {
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
}
if (matrix && matrix.isMatrix4) geometry.applyMatrix(matrix);
const vertices = geometry.vertices.reduce((array, { x, y, z }, i) => {
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 }) => {
switch (side) {
case THREE.FrontSide:
return false;
case THREE.DoubleSide:
return true;
default:
return false;
}
}) : [false];
if (sync) {
return sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
} else {
return sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
}
}
function sliceSync(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) {
return new Promise((resolve, reject) => {
// create the slicer worker
const slicerWorker = new SlicerWorker();
slicerWorker.addEventListener('error', event => {
slicerWorker.terminate();
reject(event);
});
// listen to messages send from worker
slicerWorker.addEventListener('message', (event) => {
const { message, data } = event.data;
switch (message) {
case 'SLICE': {
slicerWorker.terminate();
const { gcode } = data;
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
resolve(gcode);
break;
}
case 'PROGRESS': {
if (typeof onProgress !== 'undefined') onProgress(data);
break;
}
default:
break;
}
});
const { vertices, faces, objectIndexes } = geometry;
const buffers = [vertices.buffer, faces.buffer, objectIndexes.buffer];
slicerWorker.postMessage({
message: 'SLICE',
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;
}

View File

@ -1,34 +0,0 @@
import 'core-js'; // polyfills
import slice from './sliceActions/slice.js';
const onProgress = progress => {
self.postMessage({
message: 'PROGRESS',
data: progress
});
};
self.addEventListener('message', (event) => {
const { message, data } = event.data;
switch (message) {
case 'SLICE': {
const { settings, geometry, constructLinePreview, openObjectIndexes } = data;
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
const buffers = [];
if (gcode.linePreview) {
buffers.push(gcode.linePreview.positions.buffer);
buffers.push(gcode.linePreview.colors.buffer);
}
self.postMessage({
message: 'SLICE',
data: { gcode }
}, buffers);
break;
}
default:
break;
}
}, false);

25
src/slicerWorker.js Normal file
View File

@ -0,0 +1,25 @@
import Settings from './Settings.js';
import slice from './sliceActions/slice.js';
import * as THREE from 'three.js';
const loader = new THREE.JSONLoader();
self.addEventListener('message', (event) => {
const { message, data } = event.data;
switch (message) {
case 'SLICE': {
const { geometry: JSONGeometry, config } = data;
const { geometry } = new loader.parse(JSONGeometry.data);
const settings = new Settings(config);
const gcode = slice(geometry, settings);
self.postMessage({
message: 'SLICE',
data: { gcode }
});
break;
}
}
}, false);

View File

@ -1,92 +0,0 @@
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production';
const analyzeBundle = process.env.ANALYZE_BUNDLE;
const babelLoader = {
loader: 'babel-loader',
options: {
presets: [
require('babel-preset-env'),
require('babel-preset-stage-0'),
require('babel-preset-react')
],
plugins: [
require('babel-plugin-transform-class-properties'),
require('babel-plugin-transform-object-rest-spread'),
require('babel-plugin-transform-runtime'),
require('babel-plugin-transform-es2015-classes')
],
babelrc: false
}
};
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: babelLoader
}, { // make THREE global available to three.js examples
test: /three\/examples\/.+\.js/,
use: 'imports-loader?THREE=three'
}, {
test: /\.yml$/,
use: 'yml-loader'
}, {
test: /\.worker\.js$/,
use: [{
loader: 'worker-loader',
options: {
inline: false,
name: '[name].js'
}
}, babelLoader]
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: { name: '[path][name].[ext]' }
},
...(!devMode ? [{
loader: 'image-webpack-loader',
options: {
mozjpeg: { progressive: true, quality: 65 },
optipng: { enabled: false },
pngquant: { quality: '65-90', speed: 4 }
}
}] : [])]
}, {
test: /\.stl$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.glsl$/,
use: ['raw-loader']
}
]
},
plugins: analyzeBundle ? [new BundleAnalyzerPlugin()] : [
new HTMLWebpackPlugin({
favicon: 'favicon.ico',
title: 'Doodle3D Slicer',
template: require('html-webpack-template'),
inject: false,
hash: !devMode,
appMountId: 'app'
}),
],
devtool: devMode ? 'source-map' : false,
devServer: {
contentBase: 'dist'
}
};