mirror of
https://github.com/Doodle3D/Doodle3D-Slicer.git
synced 2025-03-14 08:31:43 +01:00
Compare commits
324 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
41be8dcfd1 | ||
|
a54a716d6d | ||
|
c472739baa | ||
|
8e01a5c2e2 | ||
|
06efd35119 | ||
|
3b440b346a | ||
|
8015cd780d | ||
|
01f40eb5f8 | ||
|
49b4a2be59 | ||
|
7df13f6db0 | ||
|
766cadcd44 | ||
|
cbc439b4dc | ||
|
7da0b2fa17 | ||
|
5cb91e6c5a | ||
|
92593cef14 | ||
|
ebbc985d67 | ||
|
246f1627c5 | ||
|
6971c3c4b5 | ||
|
fc3dc7355c | ||
|
3944202a83 | ||
|
fd6e5cebbd | ||
|
ead6be081f | ||
|
d63754dbd0 | ||
|
2044929808 | ||
|
7dceeda291 | ||
|
86eed64255 | ||
|
caf36a5505 | ||
|
1493ae3536 | ||
|
5900bbcc50 | ||
|
2c953496f7 | ||
|
c642375295 | ||
|
942addf8d7 | ||
|
b7269da172 | ||
|
50ff72a037 | ||
|
60fb966ccb | ||
|
0f70855989 | ||
|
e966bc89b2 | ||
|
6c8b8e9d44 | ||
|
79e4acd3d1 | ||
|
59681e8023 | ||
|
f109147e62 | ||
|
0119c91001 | ||
|
8467da3894 | ||
|
808d585f2a | ||
|
0804dd5282 | ||
|
0957d53b41 | ||
|
0be1ee6d51 | ||
|
5a40c7c647 | ||
|
141c38d878 | ||
|
97cb6e062d | ||
|
264ea9ff00 | ||
|
374fc4a32e | ||
|
396502948b | ||
|
a0b9cd1306 | ||
|
75c3e9d632 | ||
|
06ce0b0d6b | ||
|
74ed9e58e4 | ||
|
6be1291923 | ||
|
701d736cc0 | ||
|
61722a6985 | ||
|
ef7ceb7849 | ||
|
5302857b5e | ||
|
5a51ddf0f6 | ||
|
14c33c0225 | ||
|
5a6468baef | ||
|
763c6e08e8 | ||
|
5b2c3bb80a | ||
|
f65ab470bb | ||
|
991586dc4c | ||
|
5e88cd8c17 | ||
|
38ab39f7de | ||
|
df6e084503 | ||
|
c149667409 | ||
|
d341cc28c9 | ||
|
e1c4e2c1d4 | ||
|
bc67cab75f | ||
|
229929e4e8 | ||
|
a3f5c398da | ||
|
cd8687d148 | ||
|
e056b37677 | ||
|
9a8f96a844 | ||
|
b29198edd4 | ||
|
8bb97527f4 | ||
|
16e6e11e2f | ||
|
c4d8d1136a | ||
|
34a2b6cafc | ||
|
f032d8c267 | ||
|
1107353290 | ||
|
3d308e2533 | ||
|
793f8100fb | ||
|
120b49dfb7 | ||
|
1434006f95 | ||
|
8313982342 | ||
|
e087fadd80 | ||
|
2b4941eefb | ||
|
c835ea12b2 | ||
|
caf5e655da | ||
|
ca886afa25 | ||
|
dbc01167e5 | ||
|
45b0f541b1 | ||
|
4352293e95 | ||
|
4b17325c3f | ||
|
03f95b7570 | ||
|
5944153de7 | ||
|
774289895e | ||
|
4bf14d3a1d | ||
|
34147a918e | ||
|
78764383e1 | ||
|
a08223f243 | ||
|
89d4a659db | ||
|
0b6a4a1588 | ||
|
bc06f703ac | ||
|
6a63077b55 | ||
|
c927c7bec1 | ||
|
0ef85cc918 | ||
|
4fc34d0272 | ||
|
aad4c1564c | ||
|
8f475195b8 | ||
|
64d28affe6 | ||
|
491067b070 | ||
|
3b99a3c494 | ||
|
4cea3b3086 | ||
|
dd007b8bbf | ||
|
4d93ee2e9d | ||
|
8fde65a78a | ||
|
fded534a10 | ||
|
9cc64f44a5 | ||
|
07f8255a37 | ||
|
f809c28d26 | ||
|
9eb01f2f21 | ||
|
377088fa34 | ||
|
47e44dfed6 | ||
|
602b5b35fe | ||
|
13cc1238f1 | ||
|
4a3a5d832c | ||
|
a422956146 | ||
|
556d2f3eab | ||
|
a88db15c96 | ||
|
407d3355c0 | ||
|
4482bf1f73 | ||
|
0c7f08735f | ||
|
cd2b0322d8 | ||
|
635e01fd01 | ||
|
67b4084e55 | ||
|
21bd6c4714 | ||
|
cdb12f4d78 | ||
|
a3c028a71c | ||
|
61b3d61f8a | ||
|
abf426c5f2 | ||
|
fb06bfb135 | ||
|
e289fd848a | ||
|
8c3d444eed | ||
|
0067c7a9e1 | ||
|
109cd0898c | ||
|
e091f425a1 | ||
|
e09ed8012c | ||
|
56929c6af7 | ||
|
de2acfe6be | ||
|
6628b9cf13 | ||
|
5cef2777fb | ||
|
70aa39d89f | ||
|
1af76e6ef1 | ||
|
88291ba549 | ||
|
5b934f0e71 | ||
|
7906bfe43d | ||
|
5d49ee0c74 | ||
|
67981872aa | ||
|
e384c74f8e | ||
|
d5ea670967 | ||
|
4dc5e4849e | ||
|
f28722aec5 | ||
|
485f741077 | ||
|
2f04aa9c50 | ||
|
ca3718e492 | ||
|
0bb646a5ac | ||
|
60c70cdbd5 | ||
|
198ca783f7 | ||
|
961337138b | ||
|
a892d6ff89 | ||
|
d4743ef867 | ||
|
46408e0668 | ||
|
bbd8bc529d | ||
|
0f7da85453 | ||
|
082329b810 | ||
|
b562d3c2e2 | ||
|
a568a79ede | ||
|
742783e4db | ||
|
ac85bbc6d5 | ||
|
53e961b9bb | ||
|
11222aaa82 | ||
|
1f4ca15442 | ||
|
222a27d5b5 | ||
|
9f1958563d | ||
|
a8b3d68845 | ||
|
2f4adbbb47 | ||
|
2fdc5ca16b | ||
|
291e11fecf | ||
|
89e67882a0 | ||
|
137f95fdba | ||
|
9b78f4e2c8 | ||
|
879667fa05 | ||
|
a48768e268 | ||
|
5fbd7f50ec | ||
|
e06281667a | ||
|
d190625f14 | ||
|
9764e0a374 | ||
|
e1d833d4f3 | ||
|
f20f5b95b8 | ||
|
7b59ba1108 | ||
|
9d47e8dc23 | ||
|
eee2682f70 | ||
|
1ebbe7fc6a | ||
|
9a37e8a928 | ||
|
da3ab2b0e6 | ||
|
700b27e6e0 | ||
|
55eadc73de | ||
|
03b9570014 | ||
|
457f110dd2 | ||
|
0d64b62f12 | ||
|
212075e306 | ||
|
ecc37273ca | ||
|
10fb3714c7 | ||
|
43af4e05ab | ||
|
2aee317d42 | ||
|
6f735b5de4 | ||
|
246522ee5f | ||
|
c1cbe4f280 | ||
|
d9edfe9bde | ||
|
54811b27e9 | ||
|
745c675f67 | ||
|
9fc1ba1cb8 | ||
|
68df877e1d | ||
|
346256ff47 | ||
|
a7b2852c6e | ||
|
175689bd64 | ||
|
c91ee0a5e9 | ||
|
5e959fa634 | ||
|
264ed096a4 | ||
|
b830cc611b | ||
|
6e55ca7a79 | ||
|
a4d8e255cc | ||
|
7b57d5c7b0 | ||
|
b85781620e | ||
|
db0d82c396 | ||
|
6c02343da3 | ||
|
6b84572931 | ||
|
5f1e628952 | ||
|
a1b4a9c454 | ||
|
bcf0bb254d | ||
|
d2c70f3b2f | ||
|
95ba0cfeb1 | ||
|
84e28bc598 | ||
|
31073e7122 | ||
|
3dfce6a610 | ||
|
a79dd30abc | ||
|
9d14e40c21 | ||
|
65d44db405 | ||
|
bc9f0e673e | ||
|
ed8ccd3f68 | ||
|
8c546e31b3 | ||
|
8e5e000a12 | ||
|
994a1caa98 | ||
|
b47e98c005 | ||
|
2aa72566db | ||
|
2d0f628743 | ||
|
2c2bbeda53 | ||
|
5bc7d09e8d | ||
|
e33c967934 | ||
|
5f1a7e3e74 | ||
|
72c7c91b27 | ||
|
aef67db205 | ||
|
b9b1f59af2 | ||
|
22298c9cb6 | ||
|
1f7b48662a | ||
|
3221278853 | ||
|
aa339fda3a | ||
|
6e43994305 | ||
|
f4eaac16ff | ||
|
9f7242e0e4 | ||
|
9e28b3b317 | ||
|
4e291b2370 | ||
|
d60b8d1ddb | ||
|
71c271bb59 | ||
|
1eedb88f0b | ||
|
597ec406de | ||
|
23087af9fc | ||
|
8e985669ed | ||
|
81d842cc8c | ||
|
fcfe7f7bc6 | ||
|
10055824aa | ||
|
83d96d88ec | ||
|
45e0f02936 | ||
|
cd737da6d9 | ||
|
53d2023047 | ||
|
6bdcb6cb23 | ||
|
057fd4e094 | ||
|
6768810fd6 | ||
|
dcd3dc1614 | ||
|
6cd899f32b | ||
|
c1117a8ce5 | ||
|
88e056aece | ||
|
dc6c1d7575 | ||
|
9c233b1ab6 | ||
|
c65a5a3df4 | ||
|
3a09b93f46 | ||
|
16158a3e3c | ||
|
245e1b705a | ||
|
b6f94f6edb | ||
|
a5547ac070 | ||
|
2c2b547ea2 | ||
|
9f49bb7b8c | ||
|
40d505d754 | ||
|
ccc676ebd1 | ||
|
3b4df148ed | ||
|
849f3f893a | ||
|
4654858c3e | ||
|
624a178bb2 | ||
|
2b783e2889 | ||
|
bacca4099c | ||
|
c30bd0440e | ||
|
cd0406f0a9 | ||
|
ae24974e31 | ||
|
3f3408747c | ||
|
dcfd1bf318 |
21
.babelrc
21
.babelrc
@ -1,19 +1,26 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
// transpile to common node & browser compatible js, keeping modules
|
|
||||||
"module": {
|
"module": {
|
||||||
"presets": [
|
"presets": [
|
||||||
["latest", {
|
["env", {
|
||||||
|
"targets": {
|
||||||
|
"node": "6",
|
||||||
|
"browsers": ["last 2 versions", "safari >= 7", "not ie < 11"]
|
||||||
|
},
|
||||||
"modules": false
|
"modules": false
|
||||||
}]
|
}],
|
||||||
|
"stage-0",
|
||||||
|
"react"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// transpile to common node & browser compatible js, using commonjs
|
|
||||||
"main": {
|
"main": {
|
||||||
"presets": ["latest"]
|
"presets": ["env", "stage-0", "react"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"babel-plugin-transform-object-rest-spread"
|
"transform-class-properties",
|
||||||
|
"transform-object-rest-spread",
|
||||||
|
"transform-runtime",
|
||||||
|
"transform-es2015-classes"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
33
.eslintrc
Normal file
33
.eslintrc
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"extends": "eslint-config-airbnb",
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true,
|
||||||
|
"modules": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"comma-dangle": [1, "never"],
|
||||||
|
"no-else-return": 0,
|
||||||
|
"no-use-before-define": [2, "nofunc"],
|
||||||
|
"no-param-reassign": 0,
|
||||||
|
"no-var": 1,
|
||||||
|
"no-labels": 0,
|
||||||
|
"guard-for-in": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"no-unused-vars": 1,
|
||||||
|
"key-spacing": [1, {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
|
||||||
|
"no-loop-func": 1,
|
||||||
|
"react/sort-comp": [0],
|
||||||
|
"max-len": [1, 110, 4],
|
||||||
|
"camelcase": 1,
|
||||||
|
"new-cap": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"THREE": false
|
||||||
|
}
|
||||||
|
}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,10 +1,8 @@
|
|||||||
|
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
|
||||||
jspm_package
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
lib
|
lib
|
||||||
module
|
module
|
||||||
dist
|
dist
|
||||||
|
@ -1,4 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
jspm_packages
|
jspm_packages
|
||||||
example
|
|
||||||
simpleExample
|
|
136
DOCS.md
Normal file
136
DOCS.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Doodle3D Slicer
|
||||||
|
This document explains how the slice process works.
|
||||||
|
|
||||||
|
In this slicer Z is the "up" vector.
|
||||||
|
|
||||||
|
Requisites
|
||||||
|
- 2D Vector math
|
||||||
|
- 3D Vector math
|
||||||
|
- 2D Boolean operations (union, difference)
|
||||||
|
- 2D Path offsetting
|
||||||
|
|
||||||
|
### Step 0: Preparation
|
||||||
|
The first step is to prepare the data for slicing. Most of the model data is mapped into `typed arrays`. This way they can be send to the worker very efficiently (due to the transferable nature of typed arrays).
|
||||||
|
```
|
||||||
|
Vertices: Float32Array
|
||||||
|
Faces: Uint32Array
|
||||||
|
ObjectIndexes: UInt8Array
|
||||||
|
OpenObjectIndexes: [...Int]
|
||||||
|
Settings:
|
||||||
|
startCode: String
|
||||||
|
endcode: String
|
||||||
|
dimensions:
|
||||||
|
x: Number
|
||||||
|
y: Number
|
||||||
|
z: Number
|
||||||
|
heatedBed: Bool
|
||||||
|
nozzleDiameter: Number
|
||||||
|
filamentThickness: Number
|
||||||
|
temperature: Number
|
||||||
|
bedTemperature: Number
|
||||||
|
layerHeight: Number
|
||||||
|
combing: Bool
|
||||||
|
thickness:
|
||||||
|
top: Number
|
||||||
|
bottom: Number
|
||||||
|
shell: Number
|
||||||
|
retraction:
|
||||||
|
enabled: Bool
|
||||||
|
amount: Number
|
||||||
|
speed: Number
|
||||||
|
minDistance: Number
|
||||||
|
travel:
|
||||||
|
speed: Number
|
||||||
|
support:
|
||||||
|
enabled: Bool
|
||||||
|
minArea: Number
|
||||||
|
distanceY: Number
|
||||||
|
density: Number
|
||||||
|
margin: Number
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
innerShell:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
outerShell:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
innerInfill:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
density: Number
|
||||||
|
outerInfill:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
brim:
|
||||||
|
size: Number
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
firstLayer:
|
||||||
|
flowRate: Number
|
||||||
|
speed: Number
|
||||||
|
```
|
||||||
|
- Vertices: List of points in 3d
|
||||||
|
- Faces: Indexes refering to points in the vertices list that make a triangular surface
|
||||||
|
- ObjectIndexes: Describes of what object each face is part of (important for the generating of 2d shapes)
|
||||||
|
- OpenObjectIndexes: Determines weather a object is open or closed (important for the generating of 2d shapes)
|
||||||
|
- Settings: object containing all the settings for slicing. We go in depth in this object when it's needed
|
||||||
|
|
||||||
|
### Step 1: Creating lines
|
||||||
|
In this we take the 3d model and look at each surface to extract all individual lines. Note some lines are part of multiple surfaces. In addition we also add some additional data to each line, like the surfaces it is part of we'll also store the 2d normal.
|
||||||
|
|
||||||
|
```
|
||||||
|
function calculateNormal(vertices, a, b, c) {
|
||||||
|
a = getVertex(vertices, a);
|
||||||
|
b = getVertex(vertices, b);
|
||||||
|
c = getVertex(vertices, c);
|
||||||
|
|
||||||
|
const cb = vector3.subtract(c, b);
|
||||||
|
const ab = vector3.subtract(a, b);
|
||||||
|
const normal = vector3.normalize(vector3.cross(cb, ab));
|
||||||
|
|
||||||
|
return normal;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to extract all unique lines from the model we'll loop through each face of the model.
|
||||||
|
|
||||||
|
### Step 2: Calculate Layers Intersections
|
||||||
|
This is a fairly straight forward step. We take the lines and calculate on what layers that line will be intersecting. Additinally we calculate the coordinates where the line intersects each layer.
|
||||||
|
|
||||||
|
### Step 3: Intersections To Shapes
|
||||||
|
### Step 4: Shapes To Slices
|
||||||
|
### Step 5: Generate Inner Lines
|
||||||
|
### Step 6: Generate Outlines
|
||||||
|
### Step 7: Generate Infills
|
||||||
|
### Step 8: Generate Support
|
||||||
|
### Step 9: AddBrim
|
||||||
|
|
||||||
|
```
|
||||||
|
let {
|
||||||
|
brim: { size: brimSize },
|
||||||
|
nozzleDiameter
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
nozzleDiameter /= PRECISION;
|
||||||
|
brimSize /= PRECISION;
|
||||||
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
|
|
||||||
|
const [firstLayer] = slices;
|
||||||
|
|
||||||
|
const brim = firstLayer.parts.reduce((brim, { shape }) => (
|
||||||
|
brim.join(shape.offset(nozzleRadius, {
|
||||||
|
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
||||||
|
}))
|
||||||
|
), new Shape([], true)).simplify('pftNonZero');
|
||||||
|
|
||||||
|
firstLayer.brim = new Shape([], true);
|
||||||
|
|
||||||
|
for (let offset = 0; offset < brimSize; offset += nozzleDiameter) {
|
||||||
|
const brimPart = brim.offset(offset, OFFSET_OPTIONS);
|
||||||
|
firstLayer.brim = firstLayer.brim.join(brimPart);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 10: Optimize Paths
|
||||||
|
### Step 11: Slices To GCode
|
@ -9,7 +9,7 @@ import * as THREE from 'three';
|
|||||||
import { defaultSettings, sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
|
import { defaultSettings, sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings.base,
|
...defaultSettings.default,
|
||||||
...defaultSettings.material.pla,
|
...defaultSettings.material.pla,
|
||||||
...defaultSettings.printer.ultimaker2go,
|
...defaultSettings.printer.ultimaker2go,
|
||||||
...defaultSettings.quality.high
|
...defaultSettings.quality.high
|
||||||
@ -27,7 +27,7 @@ const gcode = await sliceGeometry(settings, geometry);
|
|||||||
import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer';
|
import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer';
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings.base,
|
...defaultSettings.default,
|
||||||
...defaultSettings.material.pla,
|
...defaultSettings.material.pla,
|
||||||
...defaultSettings.printer.ultimaker2go,
|
...defaultSettings.printer.ultimaker2go,
|
||||||
...defaultSettings.quality.high
|
...defaultSettings.quality.high
|
||||||
|
97
comb.js
Normal file
97
comb.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import comb from './src/sliceActions/helpers/comb.js';
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
canvas.width = 800;
|
||||||
|
canvas.height = 800;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.lineJoin = 'bevel';
|
||||||
|
|
||||||
|
function circle(radius = 10, x = 0, y = 0, clockWise = true, segments = 40) {
|
||||||
|
const shape = [];
|
||||||
|
|
||||||
|
for (let rad = 0; rad < Math.PI * 2; rad += Math.PI * 2 / segments) {
|
||||||
|
if (clockWise) {
|
||||||
|
shape.push({ x: Math.cos(rad) * radius + x, y: Math.sin(rad) * radius + y });
|
||||||
|
} else {
|
||||||
|
shape.push({ x: Math.cos(rad) * radius + x, y: -Math.sin(rad) * radius + y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
const START = { x: 200, y: 400 };
|
||||||
|
const END = { x: 400, y: 300 };
|
||||||
|
|
||||||
|
const POLYGON = [[
|
||||||
|
{ x: 10, y: 10 },
|
||||||
|
{ x: 600, y: 10 },
|
||||||
|
{ x: 500, y: 200 },
|
||||||
|
{ x: 600, y: 600 },
|
||||||
|
{ x: 10, y: 600 }
|
||||||
|
], [
|
||||||
|
{ x: 160, y: 120 },
|
||||||
|
{ x: 120, y: 400 },
|
||||||
|
{ x: 400, y: 400 }
|
||||||
|
]];
|
||||||
|
// const POLYGON = [
|
||||||
|
// circle(300, 305, 305, true, 4),
|
||||||
|
// circle(40, 305, 105, false, 4),
|
||||||
|
// circle(40, 305, 205, false, 4),
|
||||||
|
// circle(40, 305, 305, false, 4),
|
||||||
|
// circle(40, 305, 405, false, 4),
|
||||||
|
// circle(40, 305, 505, false, 4)
|
||||||
|
// ];
|
||||||
|
|
||||||
|
canvas.onmousedown = (event) => {
|
||||||
|
START.x = event.offsetX;
|
||||||
|
START.y = event.offsetY;
|
||||||
|
compute();
|
||||||
|
};
|
||||||
|
canvas.onmousemove = (event) => {
|
||||||
|
END.x = event.offsetX;
|
||||||
|
END.y = event.offsetY;
|
||||||
|
compute();
|
||||||
|
};
|
||||||
|
compute();
|
||||||
|
|
||||||
|
function compute() {
|
||||||
|
const path = comb(POLYGON, START, END);
|
||||||
|
|
||||||
|
// draw
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
for (const shape of POLYGON) {
|
||||||
|
let first = true;
|
||||||
|
for (const { x, y } of shape) {
|
||||||
|
if (first) {
|
||||||
|
context.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
context.lineTo(x, y);
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.closePath();
|
||||||
|
context.fillStyle = 'lightgray';
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
for (const { x, y } of path) {
|
||||||
|
context.lineTo(x, y);
|
||||||
|
}
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(START.x, START.y, 3, 0, Math.PI * 2);
|
||||||
|
context.fillStyle = 'blue';
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(END.x, END.y, 3, 0, Math.PI * 2);
|
||||||
|
context.fillStyle = 'red';
|
||||||
|
context.fill();
|
||||||
|
}
|
BIN
data/bunny.stl
Normal file
BIN
data/bunny.stl
Normal file
Binary file not shown.
@ -1,128 +0,0 @@
|
|||||||
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'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,21 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container, svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg, #controls {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=range] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1,19 +0,0 @@
|
|||||||
<!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('./save.js');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,27 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
import { defaultSettings, sliceGeometry } from 'src/index.js';
|
|
||||||
import fileSaver from 'file-saver';
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
...defaultSettings.base,
|
|
||||||
...defaultSettings.material.pla,
|
|
||||||
...defaultSettings.printer.ultimaker2go,
|
|
||||||
...defaultSettings.quality.high
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsonLoader = new THREE.JSONLoader();
|
|
||||||
jsonLoader.load('models/airplane.json', async geometry => {
|
|
||||||
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
|
|
||||||
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.0, 50)));
|
|
||||||
geometry.computeFaceNormals();
|
|
||||||
|
|
||||||
const onProgress = ({ progress: { done, total, action } }) => {
|
|
||||||
const percentage = `${(done / total * 100).toFixed()}%`
|
|
||||||
document.write(`<p>${action}, ${percentage}</p>`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const gcode = await sliceGeometry(settings, geometry, null, false, onProgress);
|
|
||||||
|
|
||||||
const file = new File([gcode], 'gcode.gcode', { type: 'text/plain' });
|
|
||||||
fileSaver.saveAs(file);
|
|
||||||
});
|
|
55582
example/stl/Airplane.stl
55582
example/stl/Airplane.stl
File diff suppressed because it is too large
Load Diff
94222
example/stl/Rocket.stl
94222
example/stl/Rocket.stl
File diff suppressed because it is too large
Load Diff
Binary file not shown.
36906
example/stl/traktor.stl
36906
example/stl/traktor.stl
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,35 +0,0 @@
|
|||||||
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}
|
|
||||||
/>,
|
|
||||||
document.getElementById('container')
|
|
||||||
);
|
|
||||||
});
|
|
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
img/logo.png
Normal file
BIN
img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
47
index.js
Normal file
47
index.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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'));
|
||||||
|
});
|
1
models/Doodle.d3sketch
Normal file
1
models/Doodle.d3sketch
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
1
models/Doodle_2.d3sketch
Normal file
1
models/Doodle_2.d3sketch
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
22604
package-lock.json
generated
22604
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@ -1,12 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@doodle3d/doodle3d-slicer",
|
"name": "@doodle3d/doodle3d-slicer",
|
||||||
"version": "0.0.13",
|
"version": "0.0.18",
|
||||||
"description": "JavaScript gcode slicer, Intended to use with the Doodle3D WiFi-Box # Usage",
|
"description": "JavaScript gcode slicer for Doodle3D Transform",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"module": "module/index.js",
|
"module": "module/index.js",
|
||||||
"esnext": "src/index.js",
|
"esnext": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "webpack-dev-server -w",
|
||||||
|
"dist": "NODE_ENV=production webpack -p",
|
||||||
|
"lint": "eslint src",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
|
"upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print",
|
||||||
|
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p",
|
||||||
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
|
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
|
||||||
"build:main": "BABEL_ENV=main babel src -s -d lib",
|
"build:main": "BABEL_ENV=main babel src -s -d lib",
|
||||||
"build:module": "BABEL_ENV=module babel src -s -d module",
|
"build:module": "BABEL_ENV=module babel src -s -d module",
|
||||||
@ -14,21 +19,61 @@
|
|||||||
"build:module:settings": "cp -r src/settings module"
|
"build:module:settings": "cp -r src/settings module"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@doodle3d/clipper-js": "^1.0.7",
|
"@doodle3d/clipper-js": "^1.0.10",
|
||||||
"three": "^0.83.0"
|
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.24.1",
|
"file-saver": "^1.3.3",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.23.0",
|
"babel-cli": "6.24.1",
|
||||||
"babel-preset-latest": "^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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git"
|
"url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "Casper @Doodle3D",
|
||||||
"license": "UNLICENSED",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": false,
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues"
|
"url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues"
|
||||||
},
|
},
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
import { defaultSettings, sliceGeometry } from 'doodle3d-slicer';
|
|
||||||
import fileURL from '!url-loader!./models/combingtest.json';
|
|
||||||
import fileSaver from 'file-saver';
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
...defaultSettings.base,
|
|
||||||
...defaultSettings.material.pla,
|
|
||||||
...defaultSettings.printer.ultimaker2go,
|
|
||||||
...defaultSettings.quality.high
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsonLoader = new THREE.JSONLoader();
|
|
||||||
jsonLoader.load(fileURL, geometry => {
|
|
||||||
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
|
|
||||||
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.0, 50)));
|
|
||||||
|
|
||||||
const onProgress = ({ progress: { done, total, action } }) => {
|
|
||||||
const percentage = `${(done / total * 100).toFixed()}%`
|
|
||||||
document.write(`<p>${action}, ${percentage}</p>`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { filament, duration, gcode } = sliceGeometry(settings, geometry, null, true, onProgress);
|
|
||||||
// console.log('filament: ', filament);
|
|
||||||
// console.log('duration: ', duration);
|
|
||||||
// document.body.innerHTML = gcode.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
|
||||||
const file = new File([gcode], 'gcode.gcode', { type: 'text/plain' });
|
|
||||||
fileSaver.saveAs(file);
|
|
||||||
});
|
|
5429
simpleExample/package-lock.json
generated
5429
simpleExample/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "doodle3d-simple-example",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack -p",
|
|
||||||
"start": "webpack-dev-server -w"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"babel-polyfill": "^6.23.0",
|
|
||||||
"file-saver": "^1.3.3",
|
|
||||||
"three": "^0.83.0",
|
|
||||||
"url-loader": "^0.5.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-core": "^6.25.0",
|
|
||||||
"babel-loader": "^7.1.1",
|
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.23.0",
|
|
||||||
"babel-preset-latest": "^6.24.1",
|
|
||||||
"html-webpack-plugin": "^2.29.0",
|
|
||||||
"webpack": "^3.3.0",
|
|
||||||
"webpack-dev-server": "^2.5.1",
|
|
||||||
"worker-loader": "^0.8.1",
|
|
||||||
"yml-loader": "^2.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
|
|
||||||
const babelLoader = {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: [
|
|
||||||
['latest', {
|
|
||||||
'modules': false,
|
|
||||||
'loose': true
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
plugins: [require('babel-plugin-transform-object-rest-spread')],
|
|
||||||
babelrc: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './index.js',
|
|
||||||
output: {
|
|
||||||
filename: 'bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'dist')
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'doodle3d-slicer': path.resolve(__dirname, '../src/index.js'),
|
|
||||||
'clipper-lib': '@doodle3d/clipper-lib',
|
|
||||||
'clipper-js': '@doodle3d/clipper-js'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: babelLoader
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.yml$/,
|
|
||||||
use: 'yml-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.worker\.js$/,
|
|
||||||
use: ['worker-loader', babelLoader]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HTMLWebpackPlugin({
|
|
||||||
title: 'Doodle3D Slicer - Simple example'
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
devtool: "source-map",
|
|
||||||
devServer: {
|
|
||||||
contentBase: 'dist'
|
|
||||||
}
|
|
||||||
};
|
|
@ -1 +1,5 @@
|
|||||||
export const PRECISION = 0.01;
|
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;
|
||||||
|
10
src/index.js
10
src/index.js
@ -1,18 +1,22 @@
|
|||||||
import { sliceGeometry, sliceMesh } from './slicer.js';
|
import { sliceGeometry, sliceMesh } from './slicer.js';
|
||||||
import baseSettings from './settings/default.yml';
|
import Interface from './interface/index.js';
|
||||||
|
import _defaultSettings from './settings/default.yml';
|
||||||
import printerSettings from './settings/printer.yml';
|
import printerSettings from './settings/printer.yml';
|
||||||
import materialSettings from './settings/material.yml';
|
import materialSettings from './settings/material.yml';
|
||||||
import qualitySettings from './settings/quality.yml';
|
import qualitySettings from './settings/quality.yml';
|
||||||
|
import infillSettings from './settings/infill.yml';
|
||||||
|
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
base: baseSettings,
|
default: _defaultSettings,
|
||||||
printer: printerSettings,
|
printer: printerSettings,
|
||||||
material: materialSettings,
|
material: materialSettings,
|
||||||
quality: qualitySettings
|
quality: qualitySettings,
|
||||||
|
infill: infillSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sliceGeometry,
|
sliceGeometry,
|
||||||
sliceMesh,
|
sliceMesh,
|
||||||
|
Interface,
|
||||||
defaultSettings
|
defaultSettings
|
||||||
};
|
};
|
||||||
|
65
src/interface/Accordion.js
Normal file
65
src/interface/Accordion.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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);
|
104
src/interface/FormComponents.js
Normal file
104
src/interface/FormComponents.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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);
|
568
src/interface/Settings.js
Normal file
568
src/interface/Settings.js
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
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);
|
377
src/interface/index.js
Normal file
377
src/interface/index.js
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
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));
|
143
src/interface/utils.js
Normal file
143
src/interface/utils.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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
|
||||||
|
};
|
@ -1,3 +1,30 @@
|
|||||||
|
startCode: |-
|
||||||
|
M109 S{temperature} ;set target temperature
|
||||||
|
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
|
||||||
|
G21 ;metric values
|
||||||
|
M107 ;start with the fan off
|
||||||
|
G28 X0 Y0 ;move X/Y to min endstops
|
||||||
|
G28 Z0 ;move Z to min endstops
|
||||||
|
G1 Z15 F9000 ;move the platform down 15mm
|
||||||
|
G92 E0 ;zero the extruded length
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 F200 E10 ;extrude 10mm of feed stock
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G1 F9000
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M117 Printing Doodle...
|
||||||
|
endCode: |-
|
||||||
|
M107 ;fan off
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
|
||||||
|
G1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
|
||||||
|
G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
|
||||||
|
M84 ;disable axes / steppers
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M104 S0
|
||||||
|
{if heatedBed}M140 S0
|
||||||
|
M117 Done
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 200
|
x: 200
|
||||||
y: 200
|
y: 200
|
||||||
@ -6,12 +33,12 @@ heatedBed: false
|
|||||||
nozzleDiameter: 0.4
|
nozzleDiameter: 0.4
|
||||||
filamentThickness: 2.85
|
filamentThickness: 2.85
|
||||||
temperature: 210
|
temperature: 210
|
||||||
bedTemperature: 70
|
bedTemperature: 50
|
||||||
layerHeight: 0.15
|
layerHeight: 0.15
|
||||||
combing: true
|
combing: false
|
||||||
thickness:
|
thickness:
|
||||||
top: 1.2
|
top: 0.45
|
||||||
bottom: 1.2
|
bottom: 0.45
|
||||||
shell: 0.8
|
shell: 0.8
|
||||||
retraction:
|
retraction:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -22,11 +49,10 @@ travel:
|
|||||||
speed: 200.0
|
speed: 200.0
|
||||||
support:
|
support:
|
||||||
enabled: false
|
enabled: false
|
||||||
acceptanceMargin: 1.5
|
minArea: 2
|
||||||
distanceY: 0.4
|
distanceY: 0.4
|
||||||
gridSize: 6.0
|
density: 5.0
|
||||||
margin: 2.0
|
margin: 2.0
|
||||||
plateSize: 4.0
|
|
||||||
flowRate: 0.8
|
flowRate: 0.8
|
||||||
speed: 40.0
|
speed: 40.0
|
||||||
innerShell:
|
innerShell:
|
||||||
@ -38,12 +64,12 @@ outerShell:
|
|||||||
innerInfill:
|
innerInfill:
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 80.0
|
speed: 80.0
|
||||||
gridSize: 5.0
|
density: 20.0
|
||||||
outerInfill:
|
outerInfill:
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 50.0
|
speed: 50.0
|
||||||
brim:
|
brim:
|
||||||
offset: 4.0
|
size: 8.0
|
||||||
flowRate: 1.0
|
flowRate: 1.0
|
||||||
speed: 40.0
|
speed: 40.0
|
||||||
firstLayer:
|
firstLayer:
|
||||||
|
21
src/settings/infill.yml
Normal file
21
src/settings/infill.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
0pct:
|
||||||
|
title: Hollow (0%)
|
||||||
|
innerInfill:
|
||||||
|
density: 0.0
|
||||||
|
10pct:
|
||||||
|
title: Light (10%)
|
||||||
|
innerInfill:
|
||||||
|
density: 10.0
|
||||||
|
20pct:
|
||||||
|
title: Normal (20%)
|
||||||
|
innerInfill:
|
||||||
|
density: 20.0
|
||||||
|
50pct:
|
||||||
|
title: Dense (50%)
|
||||||
|
innerInfill:
|
||||||
|
density: 50.0
|
||||||
|
100pct:
|
||||||
|
title: Solid (100%)
|
||||||
|
innerInfill:
|
||||||
|
density: 100.0
|
||||||
|
|
@ -1,40 +1,79 @@
|
|||||||
_3Dison_plus:
|
_3Dison_plus:
|
||||||
title: 3Dison plus
|
title: 3Dison plus
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 227
|
x: 227
|
||||||
y: 147
|
y: 147
|
||||||
z: 150
|
z: 150
|
||||||
bigbuilder3d:
|
bigbuilder3d:
|
||||||
title: Big Builder 3D
|
title: Big Builder 3D
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
builder3d:
|
builder3d:
|
||||||
title: Builder 3D
|
title: Builder 3D
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
bukobot:
|
bukobot:
|
||||||
title: Bukobot
|
title: Bukobot
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
cartesio:
|
cartesio:
|
||||||
title: Cartesio
|
title: Cartesio
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
colido_2_0_plus:
|
colido_2_0_plus:
|
||||||
title: ColiDo 2.0 Plus
|
title: ColiDo 2.0 Plus
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 230
|
x: 230
|
||||||
y: 150
|
y: 150
|
||||||
z: 140
|
z: 140
|
||||||
colido_compact:
|
colido_compact:
|
||||||
title: ColiDo Compact
|
title: ColiDo Compact
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 130
|
x: 130
|
||||||
y: 130
|
y: 130
|
||||||
z: 115
|
z: 115
|
||||||
colido_diy:
|
colido_diy:
|
||||||
title: ColiDo DIY
|
title: ColiDo DIY
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
z: 170
|
z: 170
|
||||||
colido_m2020:
|
colido_m2020:
|
||||||
title: ColiDo M2020
|
title: ColiDo M2020
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 200
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
colido_x3045:
|
colido_x3045:
|
||||||
title: ColiDo X3045
|
title: ColiDo X3045
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 300
|
x: 300
|
||||||
y: 300
|
y: 300
|
||||||
@ -42,23 +81,27 @@ colido_x3045:
|
|||||||
craftbot_plus:
|
craftbot_plus:
|
||||||
title: CraftBot PLUS
|
title: CraftBot PLUS
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
filamentThickness: 1.75
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 250
|
x: 250
|
||||||
|
y: 200
|
||||||
|
z: 200
|
||||||
cyrus:
|
cyrus:
|
||||||
title: Cyrus
|
title: Cyrus
|
||||||
|
heatedBed: false
|
||||||
|
dimensions:
|
||||||
|
x: 195
|
||||||
|
y: 195
|
||||||
|
z: 200
|
||||||
delta_rostockmax:
|
delta_rostockmax:
|
||||||
title: Delta RostockMax
|
title: Delta RostockMax
|
||||||
dimensions:
|
heatedBed: false
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
deltamaker:
|
deltamaker:
|
||||||
title: Deltamaker
|
title: Deltamaker
|
||||||
dimensions:
|
heatedBed: false
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
doodle_dream:
|
doodle_dream:
|
||||||
title: Doodle Dream
|
title: Doodle Dream
|
||||||
|
heatedBed: false
|
||||||
filamentThickness: 1.75
|
filamentThickness: 1.75
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 120
|
x: 120
|
||||||
@ -66,68 +109,119 @@ doodle_dream:
|
|||||||
z: 80
|
z: 80
|
||||||
eventorbot:
|
eventorbot:
|
||||||
title: EventorBot
|
title: EventorBot
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 203
|
||||||
|
y: 250
|
||||||
|
z: 150
|
||||||
felix:
|
felix:
|
||||||
title: Felix
|
title: Felix
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
|
dimensions:
|
||||||
|
x: 237
|
||||||
|
y: 244
|
||||||
|
z: 235
|
||||||
gigabot:
|
gigabot:
|
||||||
title: Gigabot
|
title: Gigabot
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
kossel:
|
kossel:
|
||||||
title: Kossel
|
title: Kossel
|
||||||
dimensions:
|
heatedBed: false
|
||||||
x: 0
|
filamentThickness: 2.85
|
||||||
y: 0
|
|
||||||
leapfrog_creatr:
|
leapfrog_creatr:
|
||||||
title: LeapFrog Creatr
|
title: LeapFrog Creatr
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
lulzbot_aO_101:
|
lulzbot_aO_101:
|
||||||
title: LulzBot AO-101
|
title: LulzBot AO-101
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
lulzbot_taz_4:
|
lulzbot_taz_4:
|
||||||
title: LulzBot TAZ 4
|
title: LulzBot TAZ 4
|
||||||
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 298
|
x: 298
|
||||||
y: 275
|
y: 275
|
||||||
z: 250
|
z: 250
|
||||||
heatedBed: true
|
|
||||||
makerbot_generic:
|
makerbot_generic:
|
||||||
title: Generic Makerbot Printer
|
title: Generic Makerbot Printer
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
makerbot_replicator2:
|
makerbot_replicator2:
|
||||||
title: MakerBot Replicator2
|
title: MakerBot Replicator2
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
makerbot_replicator2x:
|
makerbot_replicator2x:
|
||||||
title: MakerBot Replicator2x
|
title: MakerBot Replicator2x
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
makerbot_thingomatic:
|
makerbot_thingomatic:
|
||||||
title: MakerBot Thing-o-matic
|
title: MakerBot Thing-o-matic
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
makergear_m2:
|
makergear_m2:
|
||||||
title: MakerGear M2
|
title: MakerGear M2
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
makergear_prusa:
|
makergear_prusa:
|
||||||
title: MakerGear Prusa
|
title: MakerGear Prusa
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
makibox:
|
makibox:
|
||||||
title: Makibox
|
title: Makibox
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
mamba3d:
|
mamba3d:
|
||||||
title: Mamba3D
|
title: Mamba3D
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
marlin_generic:
|
marlin_generic:
|
||||||
title: Generic Marlin Printer
|
title: Generic Marlin Printer
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
minifactory:
|
minifactory:
|
||||||
title: miniFactory
|
title: miniFactory
|
||||||
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 150
|
x: 150
|
||||||
y: 150
|
y: 150
|
||||||
z: 155
|
z: 155
|
||||||
heatedBed: true
|
|
||||||
orca_0_3:
|
orca_0_3:
|
||||||
title: Orca 0.3
|
title: Orca 0.3
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
ord_bot_hadron:
|
ord_bot_hadron:
|
||||||
title: ORD Bot Hadron
|
title: ORD Bot Hadron
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
printrbot:
|
printrbot:
|
||||||
title: Printrbot
|
title: Printrbot
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
printxel_3d:
|
printxel_3d:
|
||||||
title: Printxel 3D
|
title: Printxel 3D
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
prusa_i3:
|
prusa_i3:
|
||||||
title: Prusa I3
|
title: Prusa I3
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
prusa_iteration_2:
|
prusa_iteration_2:
|
||||||
title: Prusa Iteration 2
|
title: Prusa Iteration 2
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
rapman:
|
rapman:
|
||||||
title: RapMan
|
title: RapMan
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
renkforce_rf100:
|
renkforce_rf100:
|
||||||
title: Renkforce RF100
|
title: Renkforce RF100
|
||||||
|
heatedBed: false
|
||||||
filamentThickness: 1.75
|
filamentThickness: 1.75
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 100
|
x: 100
|
||||||
@ -135,27 +229,91 @@ renkforce_rf100:
|
|||||||
z: 100
|
z: 100
|
||||||
reprappro_huxley:
|
reprappro_huxley:
|
||||||
title: RepRapPro Huxley
|
title: RepRapPro Huxley
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
reprappro_mendel:
|
reprappro_mendel:
|
||||||
title: RepRapPro Mendel
|
title: RepRapPro Mendel
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
rigidbot:
|
rigidbot:
|
||||||
title: Rigidbot
|
title: Rigidbot
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
robo_3d_printer:
|
robo_3d_printer:
|
||||||
title: RoBo 3D Printer
|
title: RoBo 3D Printer
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
shapercube:
|
shapercube:
|
||||||
title: ShaperCube
|
title: ShaperCube
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
tantillus:
|
tantillus:
|
||||||
title: Tantillus
|
title: Tantillus
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
ultimaker:
|
ultimaker:
|
||||||
title: Ultimaker Original
|
title: Ultimaker Original
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
ultimaker2:
|
ultimaker2:
|
||||||
title: Ultimaker 2
|
title: Ultimaker 2
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 223
|
x: 223
|
||||||
y: 223
|
y: 223
|
||||||
z: 205
|
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:
|
ultimaker2go:
|
||||||
|
startCode: |-
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X8 Y28 SDoodle3D heat up...
|
||||||
|
M109 S{temperature} ;set target temperature
|
||||||
|
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
|
||||||
|
G21 ;metric values
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M107 ;start with the fan off
|
||||||
|
G28 ; home to endstops
|
||||||
|
G1 Z15 F9000 ;move the platform down 15mm
|
||||||
|
G92 E0 ;zero the extruded length
|
||||||
|
G1 F200 E10 ;extrude 10mm of feed stock
|
||||||
|
G92 E0 ;zero the extruded length again
|
||||||
|
G1 F9000
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X8 Y28 SDoodle3D printing...
|
||||||
|
endCode: |-
|
||||||
|
M10000
|
||||||
|
M10000
|
||||||
|
M10001 X20 Y28 SDoodle3D done!
|
||||||
|
M107 ;fan off
|
||||||
|
G91 ;relative positioning
|
||||||
|
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
|
||||||
|
G1 Z+5.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
|
||||||
|
G28 ;home the printer
|
||||||
|
M84 ;disable axes / steppers
|
||||||
|
G90 ;absolute positioning
|
||||||
|
M104 S0
|
||||||
|
{if heatedBed}M140 S0
|
||||||
title: Ultimaker 2 Go
|
title: Ultimaker 2 Go
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 120
|
x: 120
|
||||||
y: 120
|
y: 120
|
||||||
@ -163,13 +321,47 @@ ultimaker2go:
|
|||||||
ultimaker_original_plus:
|
ultimaker_original_plus:
|
||||||
title: Ultimaker Original Plus
|
title: Ultimaker Original Plus
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 2.85
|
||||||
vision_3d_printer:
|
vision_3d_printer:
|
||||||
title: Vision 3D Printer
|
title: Vision 3D Printer
|
||||||
|
heatedBed: false
|
||||||
|
filamentThickness: 2.85
|
||||||
wanhao_duplicator4:
|
wanhao_duplicator4:
|
||||||
title: Wanhao Duplicator 4
|
title: Wanhao Duplicator 4
|
||||||
filamentThickness: 1.75
|
|
||||||
heatedBed: true
|
heatedBed: true
|
||||||
|
filamentThickness: 1.75
|
||||||
dimensions:
|
dimensions:
|
||||||
x: 210
|
x: 210
|
||||||
y: 140
|
y: 140
|
||||||
z: 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
|
@ -1,11 +1,54 @@
|
|||||||
low:
|
low:
|
||||||
title: "Low"
|
title: "Low"
|
||||||
|
thickness:
|
||||||
|
top: 0.30
|
||||||
|
bottom: 0.30
|
||||||
|
shell: 0.4
|
||||||
layerHeight: .2
|
layerHeight: .2
|
||||||
fill:
|
innerShell:
|
||||||
gridSize: 15.0
|
speed: 80.0
|
||||||
|
outerShell:
|
||||||
|
speed: 70.0
|
||||||
|
outerInfill:
|
||||||
|
speed: 80.0
|
||||||
|
firstLayer:
|
||||||
|
speed: 70.0
|
||||||
|
innerInfill:
|
||||||
|
speed: 80.0
|
||||||
|
density: 10.0
|
||||||
medium:
|
medium:
|
||||||
title: "Medium"
|
title: "Medium"
|
||||||
layerHeight: .15
|
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:
|
high:
|
||||||
title: "High"
|
title: "High"
|
||||||
|
thickness:
|
||||||
|
top: 0.60
|
||||||
|
bottom: 0.60
|
||||||
|
shell: 1.2
|
||||||
layerHeight: .1
|
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
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION } from '../constants.js';
|
||||||
|
|
||||||
const offsetOptions = {
|
const OFFSET_OPTIONS = {
|
||||||
jointType: 'jtRound',
|
jointType: 'jtRound',
|
||||||
miterLimit: 2.0,
|
miterLimit: 2.0,
|
||||||
roundPrecision: 0.25
|
roundPrecision: 0.25,
|
||||||
|
endType: 'etClosedPolygon'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function addBrim(slices, settings) {
|
export default function addBrim(slices, settings) {
|
||||||
let {
|
let {
|
||||||
brim: { offset: brimOffset }
|
brim: { size: brimSize },
|
||||||
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
brimOffset /= PRECISION;
|
|
||||||
|
nozzleDiameter /= PRECISION;
|
||||||
|
brimSize /= PRECISION;
|
||||||
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
|
|
||||||
const [firstLayer] = slices;
|
const [firstLayer] = slices;
|
||||||
|
|
||||||
firstLayer.brim = firstLayer.parts.reduce((brim, { shape }) => (
|
const brim = firstLayer.parts.reduce((_brim, { shape }) => (
|
||||||
brim.join(shape.offset(brimOffset, {
|
_brim.join(shape.offset(nozzleRadius, {
|
||||||
...offsetOptions,
|
...OFFSET_OPTIONS,
|
||||||
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
|
||||||
}))
|
}))
|
||||||
), new Shape([], true)).simplify('pftNonZero');
|
), new Shape([], true)).simplify('pftNonZero');
|
||||||
|
|
||||||
|
firstLayer.brim = new Shape([], true);
|
||||||
|
|
||||||
|
for (let offset = 0; offset < brimSize; offset += nozzleDiameter) {
|
||||||
|
const brimPart = brim.offset(offset, OFFSET_OPTIONS);
|
||||||
|
firstLayer.brim = firstLayer.brim.join(brimPart);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { PRECISION } from '../constants.js'
|
import { PRECISION } from '../constants.js';
|
||||||
|
import { divide } from './helpers/vector2.js';
|
||||||
|
|
||||||
export default function applyPrecision(shapes) {
|
export default function applyPrecision(layers) {
|
||||||
for (let i = 0; i < shapes.length; i ++) {
|
for (let layer = 0; layer < layers.length; layer ++) {
|
||||||
const { fillShapes, lineShapesOpen, lineShapesClosed } = shapes[i];
|
const { fillShapes, lineShapesOpen, lineShapesClosed } = layers[layer];
|
||||||
|
|
||||||
scaleUpShape(fillShapes);
|
scaleUpShape(fillShapes);
|
||||||
scaleUpShape(lineShapesOpen);
|
scaleUpShape(lineShapesOpen);
|
||||||
@ -15,9 +16,7 @@ function scaleUpShape(shape) {
|
|||||||
const path = shape[i];
|
const path = shape[i];
|
||||||
|
|
||||||
for (let i = 0; i < path.length; i ++) {
|
for (let i = 0; i < path.length; i ++) {
|
||||||
const point = path[i];
|
path[i] = divide(path[i], PRECISION);
|
||||||
|
|
||||||
point.copy(point.divideScalar(PRECISION));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,28 @@
|
|||||||
import * as THREE from 'three';
|
import { Z_OFFSET } from '../constants.js';
|
||||||
|
|
||||||
export default function calculateLayersIntersections(lines, settings) {
|
export default function calculateLayersIntersections(lines, settings) {
|
||||||
const {
|
const {
|
||||||
layerHeight,
|
dimensions: { z: dimensionsZ },
|
||||||
dimensions: { z: dimensionsZ }
|
layerHeight
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const numLayers = Math.floor(dimensionsZ / layerHeight);
|
const numLayers = Math.floor((dimensionsZ - Z_OFFSET) / layerHeight);
|
||||||
|
|
||||||
const layerIntersectionIndexes = Array.from(Array(numLayers)).map(() => []);
|
const layerPoints = Array.from(Array(numLayers)).map(() => ({}));
|
||||||
const layerIntersectionPoints = Array.from(Array(numLayers)).map(() => []);
|
const layerFaceIndexes = Array.from(Array(numLayers)).map(() => []);
|
||||||
|
|
||||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
|
||||||
const { line, isFlat } = lines[lineIndex];
|
const { line, faces } = lines[lineIndex];
|
||||||
|
|
||||||
if (isFlat) continue;
|
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 ++) {
|
for (let layerIndex = min; layerIndex <= max; layerIndex ++) {
|
||||||
if (layerIndex >= 0 && layerIndex < numLayers) {
|
if (layerIndex >= 0 && layerIndex < numLayers) {
|
||||||
|
const y = layerIndex * layerHeight + Z_OFFSET;
|
||||||
|
|
||||||
layerIntersectionIndexes[layerIndex].push(lineIndex);
|
let x;
|
||||||
|
let z;
|
||||||
const y = layerIndex * layerHeight;
|
|
||||||
|
|
||||||
let x, z;
|
|
||||||
if (line.start.y === line.end.y) {
|
if (line.start.y === line.end.y) {
|
||||||
x = line.start.x;
|
x = line.start.x;
|
||||||
z = line.start.z;
|
z = line.start.z;
|
||||||
@ -37,10 +33,14 @@ export default function calculateLayersIntersections(lines, settings) {
|
|||||||
z = line.end.z * alpha + line.start.z * alpha1;
|
z = line.end.z * alpha + line.start.z * alpha1;
|
||||||
}
|
}
|
||||||
|
|
||||||
layerIntersectionPoints[layerIndex][lineIndex] = new THREE.Vector2(z, x);
|
layerPoints[layerIndex][lineIndex] = { x: z, y: x };
|
||||||
|
for (const faceIndex of faces) {
|
||||||
|
const layerFaceIndex = layerFaceIndexes[layerIndex];
|
||||||
|
if (!layerFaceIndex.includes(faceIndex)) layerFaceIndex.push(faceIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { layerIntersectionIndexes, layerIntersectionPoints };
|
return { layerPoints, layerFaceIndexes };
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,70 @@
|
|||||||
import * as THREE from 'three';
|
import * as vector2 from './helpers/vector2.js';
|
||||||
|
import * as vector3 from './helpers/vector3.js';
|
||||||
|
|
||||||
function addLine(geometry, lineLookup, lines, a, b, isFlat) {
|
export default function createLines(geometry) {
|
||||||
const index = lines.length;
|
const faces = [];
|
||||||
lineLookup[`${a}_${b}`] = index;
|
|
||||||
|
|
||||||
lines.push({
|
|
||||||
line: new THREE.Line3(geometry.vertices[a], geometry.vertices[b]),
|
|
||||||
connects: [],
|
|
||||||
normals: [],
|
|
||||||
isFlat
|
|
||||||
});
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createLines(geometry, settings) {
|
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const lineLookup = {};
|
const lineLookup = {};
|
||||||
|
|
||||||
for (let i = 0; i < geometry.faces.length; i ++) {
|
for (let i = 0; i < geometry.objectIndexes.length; i ++) {
|
||||||
const face = geometry.faces[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);
|
||||||
|
|
||||||
const lookupA = lineLookup[`${face.b}_${face.a}`];
|
// skip faces that point up or down
|
||||||
const lookupB = lineLookup[`${face.c}_${face.b}`];
|
if (normal.y > 0.999 || normal.y < -0.999) {
|
||||||
const lookupC = lineLookup[`${face.a}_${face.c}`];
|
faces.push(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const isFlat = face.normal.y > 0.999 || face.normal.y < -0.999;
|
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);
|
||||||
|
|
||||||
// only add unique lines
|
const flatNormal = vector2.normalize({ x: normal.z, y: normal.x });
|
||||||
// returns index of said line
|
const lineIndexes = [indexA, indexB, indexC];
|
||||||
const lineIndexA = typeof lookupA !== 'undefined' ? lookupA : addLine(geometry, lineLookup, lines, face.a, face.b, isFlat);
|
|
||||||
const lineIndexB = typeof lookupB !== 'undefined' ? lookupB : addLine(geometry, lineLookup, lines, face.b, face.c, isFlat);
|
|
||||||
const lineIndexC = typeof lookupC !== 'undefined' ? lookupC : addLine(geometry, lineLookup, lines, face.c, face.a, isFlat);
|
|
||||||
|
|
||||||
// set connecting lines (based on face)
|
faces.push({ lineIndexes, flatNormal, objectIndex });
|
||||||
lines[lineIndexA].connects.push(lineIndexB, lineIndexC);
|
|
||||||
lines[lineIndexB].connects.push(lineIndexC, lineIndexA);
|
|
||||||
lines[lineIndexC].connects.push(lineIndexA, lineIndexB);
|
|
||||||
|
|
||||||
const normal = new THREE.Vector2(face.normal.z, face.normal.x).normalize();
|
|
||||||
lines[lineIndexA].normals.push(normal);
|
|
||||||
lines[lineIndexB].normals.push(normal);
|
|
||||||
lines[lineIndexC].normals.push(normal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
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);
|
||||||
|
|
||||||
|
const cb = vector3.subtract(c, b);
|
||||||
|
const ab = vector3.subtract(a, b);
|
||||||
|
const normal = vector3.normalize(vector3.cross(cb, ab));
|
||||||
|
|
||||||
|
return normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVertex(vertices, i) {
|
||||||
|
const i3 = i * 3;
|
||||||
|
return {
|
||||||
|
x: vertices[i3],
|
||||||
|
y: vertices[i3 + 1],
|
||||||
|
z: vertices[i3 + 2]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
export default function detectOpenClosed(lines) {
|
|
||||||
const pools = getPools(lines);
|
|
||||||
const openLines = lines.map(line => line.connects.length === 2);
|
|
||||||
|
|
||||||
for (let i = 0; i < pools.length; i ++) {
|
|
||||||
const pool = pools[i];
|
|
||||||
|
|
||||||
const isOpenGeometry = pool.some(lineIndex => openLines[lineIndex]);
|
|
||||||
|
|
||||||
for (let j = 0; j < pool.length; j ++) {
|
|
||||||
const lineIndex = pool[j];
|
|
||||||
const line = lines[lineIndex];
|
|
||||||
line.openGeometry = isOpenGeometry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPool(pools, lines, lineIndex) {
|
|
||||||
const { connects } = lines[lineIndex];
|
|
||||||
for (let i = 0; i < pools.length; i ++) {
|
|
||||||
const pool = pools[i];
|
|
||||||
|
|
||||||
if (pool.find(lineIndex => connects.includes(lineIndex))) {
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no pool found
|
|
||||||
// create new pool
|
|
||||||
const pool = [];
|
|
||||||
pools.push(pool);
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPools(lines) {
|
|
||||||
const pools = [];
|
|
||||||
|
|
||||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex ++) {
|
|
||||||
const pool = findPool(pools, lines, lineIndex);
|
|
||||||
pool.push(lineIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < pools.length; i ++) {
|
|
||||||
const poolA = pools[i];
|
|
||||||
|
|
||||||
for (let j = i + 1; j < pools.length; j ++) {
|
|
||||||
const poolB = pools[j];
|
|
||||||
|
|
||||||
for (let k = 0; k < poolA.length; k ++) {
|
|
||||||
const { connects } = lines[poolA[k]];
|
|
||||||
|
|
||||||
if (poolB.find(lineIndex => connects.includes(lineIndex))) {
|
|
||||||
poolA.splice(poolA.length, 0, ...poolB);
|
|
||||||
poolB.splice(0, poolB.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pools.filter(pool => pool.length > 0);
|
|
||||||
}
|
|
@ -1,11 +1,10 @@
|
|||||||
import { PRECISION } from '../constants.js'
|
import { PRECISION } from '../constants.js';
|
||||||
import getFillTemplate from './getFillTemplate.js';
|
import getFillTemplate from './getFillTemplate.js';
|
||||||
import Shape from 'clipper-js';
|
|
||||||
|
|
||||||
export default function generateInfills(slices, settings) {
|
export default function generateInfills(slices, settings) {
|
||||||
let {
|
let {
|
||||||
layerHeight,
|
layerHeight,
|
||||||
innerInfill: { gridSize: infillGridSize },
|
innerInfill: { density },
|
||||||
thickness: {
|
thickness: {
|
||||||
top: topThickness,
|
top: topThickness,
|
||||||
bottom: bottomThickness
|
bottom: bottomThickness
|
||||||
@ -13,13 +12,16 @@ export default function generateInfills(slices, settings) {
|
|||||||
nozzleDiameter
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
infillGridSize /= PRECISION;
|
density /= 100;
|
||||||
nozzleDiameter /= PRECISION;
|
nozzleDiameter /= PRECISION;
|
||||||
|
|
||||||
const bottomSkinCount = Math.ceil(bottomThickness/layerHeight);
|
const bidirectionalInfill = density < 0.8;
|
||||||
const topSkinCount = Math.ceil(topThickness/layerHeight);
|
const infillGridSize = nozzleDiameter * (bidirectionalInfill ? 2 : 1) / density;
|
||||||
|
|
||||||
|
const bottomSkinCount = Math.ceil(bottomThickness / layerHeight);
|
||||||
|
const topSkinCount = Math.ceil(topThickness / layerHeight);
|
||||||
const nozzleRadius = nozzleDiameter / 2;
|
const nozzleRadius = nozzleDiameter / 2;
|
||||||
const outerFillTemplateSize = Math.sqrt(2 * Math.pow(nozzleDiameter, 2));
|
const outerFillTemplateSize = nozzleDiameter;
|
||||||
|
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
@ -32,6 +34,7 @@ export default function generateInfills(slices, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < slice.parts.length; i ++) {
|
for (let i = 0; i < slice.parts.length; i ++) {
|
||||||
|
const even = (layer % 2 === 0);
|
||||||
const part = slice.parts[i];
|
const part = slice.parts[i];
|
||||||
|
|
||||||
if (!part.closed) continue;
|
if (!part.closed) continue;
|
||||||
@ -52,14 +55,13 @@ export default function generateInfills(slices, settings) {
|
|||||||
|
|
||||||
if (innerFillArea && innerFillArea.paths.length > 0) {
|
if (innerFillArea && innerFillArea.paths.length > 0) {
|
||||||
const bounds = innerFillArea.shapeBounds();
|
const bounds = innerFillArea.shapeBounds();
|
||||||
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, true, true);
|
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, bidirectionalInfill || even, bidirectionalInfill || !even);
|
||||||
|
|
||||||
part.innerFill.join(innerFillTemplate.intersect(innerFillArea));
|
part.innerFill.join(innerFillTemplate.intersect(innerFillArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outerFillArea.paths.length > 0) {
|
if (outerFillArea.paths.length > 0) {
|
||||||
const bounds = outerFillArea.shapeBounds();
|
const bounds = outerFillArea.shapeBounds();
|
||||||
const even = (layer % 2 === 0);
|
|
||||||
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
|
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
|
||||||
|
|
||||||
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));
|
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PRECISION } from '../constants.js'
|
import { PRECISION } from '../constants.js';
|
||||||
|
|
||||||
const offsetOptions = {
|
const OFFSET_OPTIONS = {
|
||||||
jointType: 'jtSquare',
|
jointType: 'jtSquare',
|
||||||
endType: 'etClosedPolygon',
|
endType: 'etClosedPolygon',
|
||||||
miterLimit: 2.0,
|
miterLimit: 2.0,
|
||||||
@ -10,7 +10,6 @@ const offsetOptions = {
|
|||||||
export default function generateInnerLines(slices, settings) {
|
export default function generateInnerLines(slices, settings) {
|
||||||
// need to scale up everything because of clipper rounding errors
|
// need to scale up everything because of clipper rounding errors
|
||||||
let {
|
let {
|
||||||
layerHeight,
|
|
||||||
nozzleDiameter,
|
nozzleDiameter,
|
||||||
thickness: { shell: shellThickness }
|
thickness: { shell: shellThickness }
|
||||||
} = settings;
|
} = settings;
|
||||||
@ -29,7 +28,7 @@ export default function generateInnerLines(slices, settings) {
|
|||||||
|
|
||||||
if (!part.closed) continue;
|
if (!part.closed) continue;
|
||||||
|
|
||||||
const outerLine = part.shape.offset(-nozzleRadius, offsetOptions);
|
const outerLine = part.shape.offset(-nozzleRadius, OFFSET_OPTIONS);
|
||||||
|
|
||||||
if (outerLine.paths.length === 0) continue;
|
if (outerLine.paths.length === 0) continue;
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ export default function generateInnerLines(slices, settings) {
|
|||||||
for (let inset = 1; inset < numShells; inset += 1) {
|
for (let inset = 1; inset < numShells; inset += 1) {
|
||||||
const offset = inset * nozzleDiameter;
|
const offset = inset * nozzleDiameter;
|
||||||
|
|
||||||
const shell = outerLine.offset(-offset, offsetOptions);
|
const shell = outerLine.offset(-offset, OFFSET_OPTIONS);
|
||||||
|
|
||||||
if (shell.paths.length === 0) {
|
if (shell.paths.length === 0) {
|
||||||
break;
|
break;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
|
|
||||||
export default function calculateOutlines(slices, settings) {
|
export default function calculateOutlines(slices) {
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
|
|
||||||
|
@ -1,76 +1,41 @@
|
|||||||
import getFillTemplate from './getFillTemplate.js';
|
import getFillTemplate from './getFillTemplate.js';
|
||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION } from '../constants.js';
|
||||||
|
|
||||||
|
const PRECISION_SQUARED = Math.pow(PRECISION, 2);
|
||||||
|
|
||||||
export default function generateSupport(slices, settings) {
|
export default function generateSupport(slices, settings) {
|
||||||
if (!settings.support.enabled) return;
|
if (!settings.support.enabled) return;
|
||||||
|
|
||||||
let {
|
let {
|
||||||
layerHeight,
|
layerHeight,
|
||||||
support: {
|
support: { density, margin, minArea, distanceY },
|
||||||
gridSize: supportGridSize,
|
|
||||||
margin: supportMargin,
|
|
||||||
plateSize: plateSize,
|
|
||||||
distanceY: supportDistanceY
|
|
||||||
},
|
|
||||||
nozzleDiameter
|
nozzleDiameter
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
supportGridSize /= PRECISION;
|
density /= 100;
|
||||||
supportMargin /= PRECISION;
|
margin /= PRECISION;
|
||||||
plateSize /= PRECISION;
|
|
||||||
nozzleDiameter /= PRECISION;
|
nozzleDiameter /= PRECISION;
|
||||||
var supportDistanceLayers = Math.max(Math.ceil(supportDistanceY / layerHeight), 1);
|
|
||||||
|
|
||||||
var supportAreas = new Shape([], true);
|
const infillGridSize = nozzleDiameter * 2 / density;
|
||||||
|
const supportDistanceLayers = Math.max(Math.ceil(distanceY / layerHeight), 1);
|
||||||
|
|
||||||
for (var layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) {
|
let supportArea = new Shape([], true);
|
||||||
var currentSlice = slices[layer];
|
|
||||||
|
|
||||||
if (supportAreas.length > 0) {
|
for (let layer = slices.length - 1 - supportDistanceLayers; layer >= 0; layer --) {
|
||||||
|
const currentLayer = slices[layer + supportDistanceLayers - 1];
|
||||||
|
const upSkin = slices[layer + supportDistanceLayers];
|
||||||
|
const downSkin = slices[layer - supportDistanceLayers];
|
||||||
|
|
||||||
if (layer >= supportDistanceLayers) {
|
const neededSupportArea = upSkin.outline.difference(currentLayer.outline.offset(margin));
|
||||||
var sliceSkin = slices[layer - supportDistanceLayers].outline;
|
|
||||||
sliceSkin = sliceSkin;
|
|
||||||
|
|
||||||
var supportAreasSlimmed = supportAreas.difference(sliceSkin.offset(supportMargin));
|
if (neededSupportArea.totalArea() * PRECISION_SQUARED > minArea) supportArea = supportArea.union(neededSupportArea);
|
||||||
if (supportAreasSlimmed.area() < 100.0) {
|
if (downSkin) supportArea = supportArea.difference(downSkin.outline.offset(margin));
|
||||||
supportAreas = supportAreas.difference(sliceSkin);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
supportAreas = supportAreasSlimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportTemplate = getFillTemplate(supportAreas.bounds(), supportGridSize, true, true);
|
const bounds = supportArea.shapeBounds();
|
||||||
var supportFill = supportTemplate.intersect(supportAreas);
|
const innerFillTemplate = getFillTemplate(bounds, infillGridSize, true, true);
|
||||||
if (supportFill.length === 0) {
|
|
||||||
currentSlice.support = supportAreas.clone();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
currentSlice.support = supportFill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportSkin = slices[layer + supportDistanceLayers - 1].outline;
|
slices[layer].support = supportArea.clone().join(supportArea.intersect(innerFillTemplate));
|
||||||
|
slices[layer].supportOutline = supportArea;
|
||||||
var slice = slices[layer + supportDistanceLayers];
|
|
||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
|
||||||
var slicePart = slice.parts[i];
|
|
||||||
|
|
||||||
if (slicePart.intersect.closed) {
|
|
||||||
var outerLine = slicePart.outerLine;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var outerLine = slicePart.intersect.offset(supportMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
var overlap = supportSkin.offset(supportMargin).intersect(outerLine);
|
|
||||||
var overhang = outerLine.difference(overlap);
|
|
||||||
|
|
||||||
if (overlap.length === 0 || overhang.length > 0) {
|
|
||||||
supportAreas = supportAreas.join(overhang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
|
|
||||||
export default function getFillTemplate(bounds, size, even, uneven) {
|
export default function getFillTemplate(bounds, gridSize, even, uneven) {
|
||||||
const paths = [];
|
const paths = [];
|
||||||
|
|
||||||
|
const size = Math.sqrt(2 * Math.pow(gridSize, 2));
|
||||||
|
|
||||||
const left = Math.floor(bounds.left / size) * size;
|
const left = Math.floor(bounds.left / size) * size;
|
||||||
const right = Math.ceil(bounds.right / size) * size;
|
const right = Math.ceil(bounds.right / size) * size;
|
||||||
const top = Math.floor(bounds.top / size) * size;
|
const top = Math.floor(bounds.top / size) * size;
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
import * as THREE from 'three';
|
import { distanceTo } from './vector2.js';
|
||||||
import { PRECISION } from '../../constants.js';
|
import { VERSION } from '../../constants.js';
|
||||||
|
|
||||||
const MOVE = 'G';
|
export const MOVE = 'G';
|
||||||
const M_COMMAND = 'M';
|
export const M_COMMAND = 'M';
|
||||||
const FAN_SPEED = 'S';
|
export const FAN_SPEED = 'S';
|
||||||
const SPEED = 'F';
|
export const SPEED = 'F';
|
||||||
const EXTRUDER = 'E';
|
export const EXTRUDER = 'E';
|
||||||
const POSITION_X = 'X';
|
export const POSITION_X = 'X';
|
||||||
const POSITION_Y = 'Y';
|
export const POSITION_Y = 'Y';
|
||||||
const POSITION_Z = 'Z';
|
export const POSITION_Z = 'Z';
|
||||||
|
|
||||||
export default class {
|
export default class GCode {
|
||||||
constructor(nozzleToFilamentRatio) {
|
constructor(settings) {
|
||||||
this._nozzleToFilamentRatio = nozzleToFilamentRatio;
|
this._nozzleToFilamentRatio = 1;
|
||||||
|
this._gcode = [
|
||||||
this._gcode = '';
|
`; ${JSON.stringify(settings)}`,
|
||||||
|
`; Generated with Doodle3D Slicer V${VERSION}`
|
||||||
|
];
|
||||||
this._currentValues = {};
|
this._currentValues = {};
|
||||||
this._nozzlePosition = new THREE.Vector2(0, 0);
|
this._nozzlePosition = { x: 0, y: 0 };
|
||||||
this._extruder = 0.0;
|
this._extruder = 0.0;
|
||||||
this._duration = 0.0;
|
this._duration = 0.0;
|
||||||
this._isRetracted = false;
|
this._isRetracted = false;
|
||||||
@ -24,30 +26,19 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_addGCode(command) {
|
_addGCode(command) {
|
||||||
let str = '';
|
this._gcode.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
let first = true;
|
updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness) {
|
||||||
for (const action in command) {
|
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
|
||||||
const value = command[action];
|
const lineSurfaceArea = nozzleDiameter * layerHeight;
|
||||||
const currentValue = this._currentValues[action];
|
this._nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
|
||||||
if (first) {
|
|
||||||
str = action + value;
|
|
||||||
|
|
||||||
first = false;
|
|
||||||
} else if (currentValue !== value) {
|
|
||||||
str += ` ${action}${value}`;
|
|
||||||
|
|
||||||
this._currentValues[action] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._gcode += `${str}\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
turnFanOn(fanSpeed) {
|
turnFanOn(fanSpeed) {
|
||||||
this._isFanOn = true;
|
this._isFanOn = true;
|
||||||
|
|
||||||
const gcode = { [M_COMMAND]: 106 }
|
const gcode = { [M_COMMAND]: 106 };
|
||||||
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
|
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
|
||||||
|
|
||||||
this._addGCode(gcode);
|
this._addGCode(gcode);
|
||||||
@ -64,41 +55,41 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveTo(x, y, z, { speed }) {
|
moveTo(x, y, z, { speed }) {
|
||||||
const newNozzlePosition = new THREE.Vector2(x, y).multiplyScalar(PRECISION);
|
const newNozzlePosition = { x, y };
|
||||||
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
|
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
|
||||||
|
|
||||||
this._duration += lineLength / speed;
|
this._duration += lineLength / speed;
|
||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[POSITION_X]: newNozzlePosition.x.toFixed(3),
|
[POSITION_X]: newNozzlePosition.x,
|
||||||
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
|
[POSITION_Y]: newNozzlePosition.y,
|
||||||
[POSITION_Z]: z.toFixed(3),
|
[POSITION_Z]: z,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
|
|
||||||
this._nozzlePosition.copy(newNozzlePosition);
|
this._nozzlePosition = newNozzlePosition;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
lineTo(x, y, z, { speed, flowRate }) {
|
lineTo(x, y, z, { speed, flowRate }) {
|
||||||
const newNozzlePosition = new THREE.Vector2(x, y).multiplyScalar(PRECISION);
|
const newNozzlePosition = { x, y };
|
||||||
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
|
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
|
||||||
|
|
||||||
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
|
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
|
||||||
this._duration += lineLength / speed;
|
this._duration += lineLength / speed;
|
||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 1,
|
[MOVE]: 1,
|
||||||
[POSITION_X]: newNozzlePosition.x.toFixed(3),
|
[POSITION_X]: newNozzlePosition.x,
|
||||||
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
|
[POSITION_Y]: newNozzlePosition.y,
|
||||||
[POSITION_Z]: z.toFixed(3),
|
[POSITION_Z]: z,
|
||||||
[SPEED]: (speed * 60).toFixed(3),
|
[SPEED]: speed * 60,
|
||||||
[EXTRUDER]: this._extruder.toFixed(3)
|
[EXTRUDER]: this._extruder
|
||||||
});
|
});
|
||||||
|
|
||||||
this._nozzlePosition.copy(newNozzlePosition);
|
this._nozzlePosition = newNozzlePosition;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -112,8 +103,8 @@ export default class {
|
|||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[EXTRUDER]: this._extruder.toFixed(3),
|
[EXTRUDER]: this._extruder,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,8 +121,8 @@ export default class {
|
|||||||
|
|
||||||
this._addGCode({
|
this._addGCode({
|
||||||
[MOVE]: 0,
|
[MOVE]: 0,
|
||||||
[EXTRUDER]: (this._extruder - amount).toFixed(3),
|
[EXTRUDER]: this._extruder - amount,
|
||||||
[SPEED]: (speed * 60).toFixed(3)
|
[SPEED]: speed * 60
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,6 +130,15 @@ export default class {
|
|||||||
return this;
|
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() {
|
getGCode() {
|
||||||
return {
|
return {
|
||||||
gcode: this._gcode,
|
gcode: this._gcode,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
|
|
||||||
export default class {
|
export default class Slice {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.parts = [];
|
this.parts = [];
|
||||||
}
|
}
|
||||||
|
@ -1,27 +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 = (a, factor) => ({
|
|
||||||
x: a.x * factor,
|
|
||||||
y: a.y * factor
|
|
||||||
});
|
|
||||||
export const normal = (a) => ({
|
|
||||||
x: -a.y,
|
|
||||||
y: a.x
|
|
||||||
});
|
|
||||||
export const dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
||||||
export const length = (a) => Math.sqrt(a.x * a.x + a.y * a.y);
|
|
||||||
export const distanceTo = (a, b) => length(subtract(a, b));
|
|
||||||
export const normalize = (a) => {
|
|
||||||
const l = length(a);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: a.x / l,
|
|
||||||
y: a.y / l
|
|
||||||
};
|
|
||||||
}
|
|
26
src/sliceActions/helpers/color.js
Normal file
26
src/sliceActions/helpers/color.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -1,131 +1,197 @@
|
|||||||
import Shape from 'clipper-js';
|
import { angle, subtract, distanceTo } from './vector2.js';
|
||||||
import { subtract, add, scale, normalize, dot, length, distanceTo } from './VectorUtils.js';
|
|
||||||
import { PRECISION } from '../../constants.js';
|
|
||||||
|
|
||||||
const TOLERANCE = 5 / PRECISION;
|
const graphs = new WeakMap();
|
||||||
|
export default function comb(polygons, start, end) {
|
||||||
|
if (!graphs.has(polygons)) graphs.set(polygons, createGraph(polygons));
|
||||||
|
let { edges, graph, points } = graphs.get(polygons);
|
||||||
|
|
||||||
export default function comb(outline, start, end) {
|
points = [...points, start, end];
|
||||||
if (distanceTo(start, end) < TOLERANCE) {
|
graph = [...graph];
|
||||||
return [start, end];
|
|
||||||
}
|
|
||||||
|
|
||||||
let combPath = new Shape([[start, end]], false, true, false);
|
const startNode = createNode(graph, points, edges, start);
|
||||||
|
const endNode = createNode(graph, points, edges, end);
|
||||||
|
|
||||||
for (let i = 0; i < outline.paths.length; i ++) {
|
let result;
|
||||||
let outlinePart = new Shape([outline.paths[i]], true, false, false, true);
|
if (graph[startNode].some(node => node.to === endNode)) {
|
||||||
|
result = [start, end];
|
||||||
let snappedCombPaths = outlinePart.orientation(0) ? combPath.intersect(outlinePart) : combPath.difference(outlinePart);
|
|
||||||
|
|
||||||
snappedCombPaths = snappedCombPaths.mapToLower();
|
|
||||||
outlinePart = outlinePart.mapToLower()[0];
|
|
||||||
|
|
||||||
if (distanceTo(start, outlinePart[outlinePart.length - 1]) < distanceTo(start, outlinePart[0])) {
|
|
||||||
outlinePart = outlinePart.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const distanceMap = new WeakMap();
|
|
||||||
|
|
||||||
for (let i = 0; i < snappedCombPaths.length; i ++) {
|
|
||||||
const snappedCombPath = snappedCombPaths[i];
|
|
||||||
|
|
||||||
const distanceStart = distanceTo(start, snappedCombPath[0]);
|
|
||||||
const distanceEnd = distanceTo(start, snappedCombPath[snappedCombPath.length - 1]);
|
|
||||||
|
|
||||||
if (distanceStart < distanceEnd) {
|
|
||||||
distanceMap.set(snappedCombPath, distanceStart);
|
|
||||||
} else {
|
|
||||||
snappedCombPath.reverse();
|
|
||||||
distanceMap.set(snappedCombPath, distanceEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snappedCombPaths.sort((a, b) => distanceMap.get(a) - distanceMap.get(b));
|
|
||||||
|
|
||||||
const firstPath = snappedCombPaths[0];
|
|
||||||
const lastPath = snappedCombPaths[snappedCombPaths.length - 1];
|
|
||||||
|
|
||||||
if (snappedCombPaths.length === 0) {
|
|
||||||
snappedCombPaths.push([start], [end]);
|
|
||||||
} else if (distanceTo(firstPath[0], start) > 1.0) {
|
|
||||||
snappedCombPaths.unshift([start]);
|
|
||||||
} else if (distanceTo(lastPath[lastPath.length - 1], end) > 1.0) {
|
|
||||||
snappedCombPaths.push([end]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snappedCombPaths.length === 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPath = snappedCombPaths[0];
|
|
||||||
const startPoint = startPath[startPath.length - 1];
|
|
||||||
|
|
||||||
const endPath = snappedCombPaths[snappedCombPaths.length - 1];
|
|
||||||
const endPoint = endPath[0];
|
|
||||||
|
|
||||||
const lineIndexStart = findClosestLineOnPath(outlinePart, startPoint);
|
|
||||||
const lineIndexEnd = findClosestLineOnPath(outlinePart, endPoint);
|
|
||||||
|
|
||||||
const path = [];
|
|
||||||
if (lineIndexEnd === lineIndexStart) {
|
|
||||||
continue;
|
|
||||||
} else if (lineIndexEnd > lineIndexStart) {
|
|
||||||
if (lineIndexStart + outlinePart.length - lineIndexEnd < lineIndexEnd - lineIndexStart) {
|
|
||||||
for (let i = lineIndexStart + outlinePart.length; i > lineIndexEnd; i --) {
|
|
||||||
path.push(outlinePart[i % outlinePart.length]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = lineIndexStart; i < lineIndexEnd; i ++) {
|
|
||||||
path.push(outlinePart[i + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (lineIndexEnd + outlinePart.length - lineIndexStart < lineIndexStart - lineIndexEnd) {
|
|
||||||
for (let i = lineIndexStart; i < lineIndexEnd + outlinePart.length; i ++) {
|
|
||||||
path.push(outlinePart[(i + 1) % outlinePart.length]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = lineIndexStart; i > lineIndexEnd; i --) {
|
|
||||||
path.push(outlinePart[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combPath = new Shape([[...startPath, ...path, ...endPath]], false, true, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return combPath.mapToLower()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClosestLineOnPath(path, point) {
|
|
||||||
let distance = Infinity;
|
|
||||||
let lineIndex;
|
|
||||||
|
|
||||||
for (let i = 0; i < path.length; i ++) {
|
|
||||||
const pointA = path[i];
|
|
||||||
const pointB = path[(i + 1) % path.length];
|
|
||||||
|
|
||||||
const tempClosestPoint = findClosestPointOnLine(pointA, pointB, point);
|
|
||||||
const tempDistance = distanceTo(tempClosestPoint, point);
|
|
||||||
|
|
||||||
if (tempDistance < distance) {
|
|
||||||
distance = tempDistance;
|
|
||||||
lineIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lineIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClosestPointOnLine(a, b, c) {
|
|
||||||
const b_ = subtract(b, a);
|
|
||||||
const c_ = subtract(c, a);
|
|
||||||
|
|
||||||
const lambda = dot(normalize(b_), c_) / length(b_);
|
|
||||||
|
|
||||||
if (lambda >= 1) {
|
|
||||||
return b;
|
|
||||||
} else if (lambda > 0) {
|
|
||||||
return add(a, scale(b_, lambda));
|
|
||||||
} else {
|
} else {
|
||||||
return a;
|
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);
|
||||||
}
|
}
|
||||||
|
34
src/sliceActions/helpers/vector2.js
Normal file
34
src/sliceActions/helpers/vector2.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
38
src/sliceActions/helpers/vector3.js
Normal file
38
src/sliceActions/helpers/vector3.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
export const subtract = (a, b) => ({
|
||||||
|
x: a.x - b.x,
|
||||||
|
y: a.y - b.y,
|
||||||
|
z: a.z - b.z
|
||||||
|
});
|
||||||
|
export const add = (a, b) => ({
|
||||||
|
x: a.x + b.x,
|
||||||
|
y: a.y + b.y,
|
||||||
|
z: a.z + b.z
|
||||||
|
});
|
||||||
|
export const scale = (v, factor) => ({
|
||||||
|
x: v.x * factor,
|
||||||
|
y: v.y * factor,
|
||||||
|
z: v.z * factor
|
||||||
|
});
|
||||||
|
export const divide = (v, factor) => ({
|
||||||
|
x: v.x / factor,
|
||||||
|
y: v.y / factor,
|
||||||
|
z: v.z / factor
|
||||||
|
});
|
||||||
|
export const cross = (a, b) => ({
|
||||||
|
x: a.y * b.z - a.z * b.y,
|
||||||
|
y: a.z * b.x - a.x * b.z,
|
||||||
|
z: a.x * b.y - a.y * b.x
|
||||||
|
});
|
||||||
|
export const equals = (a, b) => a.x === b.x && a.y === b.y && a.z === b.z;
|
||||||
|
export const almostEquals = (a, b) => Math.abs(a.x - b.x) < 0.001 && Math.abs(a.y - b.y) < 0.001 && Math.abs(a.z - b.z) < 0.001;
|
||||||
|
export const length = (v) => Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
||||||
|
export const distanceTo = (a, b) => length(subtract(a, b));
|
||||||
|
export const normalize = (v) => {
|
||||||
|
const l = length(v);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: v.x / l,
|
||||||
|
y: v.y / l,
|
||||||
|
z: v.z / l
|
||||||
|
};
|
||||||
|
};
|
@ -1,120 +1,159 @@
|
|||||||
import * as THREE from 'three';
|
import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js';
|
||||||
import Shape from 'clipper-js';
|
|
||||||
|
|
||||||
export default function intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings) {
|
export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes) {
|
||||||
const layers = [];
|
const layers = [];
|
||||||
|
|
||||||
for (let layer = 1; layer < layerIntersectionIndexes.length; layer ++) {
|
for (let layer = 0; layer < layerPoints.length; layer ++) {
|
||||||
const intersectionIndexes = layerIntersectionIndexes[layer];
|
|
||||||
const intersectionPoints = layerIntersectionPoints[layer];
|
|
||||||
|
|
||||||
if (intersectionIndexes.length === 0) continue;
|
|
||||||
|
|
||||||
const fillShapes = [];
|
const fillShapes = [];
|
||||||
const lineShapesOpen = [];
|
const lineShapesOpen = [];
|
||||||
const lineShapesClosed = [];
|
const lineShapesClosed = [];
|
||||||
for (let i = 0; i < intersectionIndexes.length; i ++) {
|
|
||||||
let index = intersectionIndexes[i];
|
|
||||||
|
|
||||||
if (typeof intersectionPoints[index] === 'undefined') continue;
|
const points = layerPoints[layer];
|
||||||
|
const faceIndexes = layerFaceIndexes[layer];
|
||||||
|
|
||||||
const shape = [];
|
if (faceIndexes.length === 0) continue;
|
||||||
|
|
||||||
const firstPoints = [index];
|
const shapes = {};
|
||||||
const { openGeometry } = lines[index];
|
|
||||||
let isFirstPoint = true;
|
|
||||||
let openShape = true;
|
|
||||||
|
|
||||||
while (index !== -1) {
|
const startConnects = {};
|
||||||
const intersection = intersectionPoints[index];
|
const endConnects = {};
|
||||||
// uppercase X and Y because clipper vector
|
|
||||||
shape.push(intersection);
|
|
||||||
|
|
||||||
delete intersectionPoints[index];
|
for (let i = 0; i < faceIndexes.length; i ++) {
|
||||||
|
const faceIndex = faceIndexes[i];
|
||||||
|
const { lineIndexes, flatNormal, objectIndex } = faces[faceIndex];
|
||||||
|
|
||||||
const connects = lines[index].connects;
|
const a = lineIndexes[0];
|
||||||
const faceNormals = lines[index].normals;
|
const b = lineIndexes[1];
|
||||||
|
const c = lineIndexes[2];
|
||||||
|
|
||||||
for (let i = 0; i < connects.length; i ++) {
|
let pointA;
|
||||||
index = connects[i];
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if (firstPoints.includes(index) && shape.length > 2) {
|
const segmentNormal = normalize(normal(subtract(points[pointA], points[pointB])));
|
||||||
openShape = false;
|
if (dot(segmentNormal, flatNormal) < 0) {
|
||||||
index = -1;
|
const temp = pointB;
|
||||||
break;
|
pointB = pointA;
|
||||||
}
|
pointA = temp;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if index has an intersection or is already used
|
if (endConnects[pointA]) {
|
||||||
if (typeof intersectionPoints[index] !== 'undefined') {
|
const lineSegment = endConnects[pointA];
|
||||||
const faceNormal = faceNormals[Math.floor(i / 2)];
|
delete endConnects[pointA];
|
||||||
|
if (startConnects[pointB]) {
|
||||||
const a = new THREE.Vector2(intersection.x, intersection.y);
|
if (startConnects[pointB] === lineSegment) {
|
||||||
const b = new THREE.Vector2(intersectionPoints[index].x, intersectionPoints[index].y);
|
delete startConnects[pointB];
|
||||||
|
lineSegment.push(pointB);
|
||||||
// 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 {
|
} else {
|
||||||
index = -1;
|
lineSegment.push(...startConnects[pointB]);
|
||||||
|
endConnects[lineSegment[lineSegment.length - 1]] = lineSegment;
|
||||||
|
shapes[objectIndex].splice(shapes[objectIndex].indexOf(startConnects[pointB]), 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
isFirstPoint = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openShape) {
|
|
||||||
index = firstPoints[0];
|
|
||||||
|
|
||||||
while (index !== -1) {
|
|
||||||
if (!firstPoints.includes(index)) {
|
|
||||||
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 (typeof intersectionPoints[index] !== 'undefined') {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
index = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openGeometry) {
|
|
||||||
if (openShape) {
|
|
||||||
lineShapesOpen.push(shape);
|
|
||||||
} else {
|
} else {
|
||||||
lineShapesClosed.push(shape);
|
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 {
|
} else {
|
||||||
fillShapes.push(shape);
|
const lineSegment = [pointA, pointB];
|
||||||
|
startConnects[pointA] = lineSegment;
|
||||||
|
endConnects[pointB] = lineSegment;
|
||||||
|
|
||||||
|
if (!shapes[objectIndex]) shapes[objectIndex] = [];
|
||||||
|
shapes[objectIndex].push(lineSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import * as THREE from 'three';
|
import { distanceTo } from './helpers/vector2.js';
|
||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
|
|
||||||
export default function optimizePaths(slices, settings) {
|
export default function optimizePaths(slices) {
|
||||||
const start = new THREE.Vector2(0, 0);
|
let start = { x: 0, y: 0 };
|
||||||
|
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
|
|
||||||
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
|
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
|
||||||
slice.brim = optimizeShape(slice.brim, start);
|
slice.brim = optimizeShape(slice.brim, start);
|
||||||
start.copy(slice.brim.lastPoint(true));
|
start = slice.brim.lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@ -54,42 +54,40 @@ export default function optimizePaths(slices, settings) {
|
|||||||
if (shell.paths.length === 0) continue;
|
if (shell.paths.length === 0) continue;
|
||||||
|
|
||||||
part.shell[i] = optimizeShape(shell, start);
|
part.shell[i] = optimizeShape(shell, start);
|
||||||
start.copy(part.shell[i].lastPoint(true));
|
start = part.shell[i].lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.outerFill.paths.length > 0) {
|
if (part.outerFill.paths.length > 0) {
|
||||||
part.outerFill = optimizeShape(part.outerFill, start);
|
part.outerFill = optimizeShape(part.outerFill, start);
|
||||||
start.copy(part.outerFill.lastPoint(true));
|
start = part.outerFill.lastPoint(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.innerFill.paths.length > 0) {
|
if (part.innerFill.paths.length > 0) {
|
||||||
part.innerFill = optimizeShape(part.innerFill, start);
|
part.innerFill = optimizeShape(part.innerFill, start);
|
||||||
start.copy(part.innerFill.lastPoint(true));
|
start = part.innerFill.lastPoint(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
part.shape = optimizeShape(part.shape, start);
|
part.shape = optimizeShape(part.shape, start);
|
||||||
start.copy(part.shape.lastPoint(true));
|
start = part.shape.lastPoint(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slice.parts = parts;
|
slice.parts = parts;
|
||||||
|
|
||||||
if (typeof slice.support !== 'undefined' && slice.support.length > 0) {
|
if (typeof slice.support !== 'undefined' && slice.support.paths.length > 0) {
|
||||||
slice.support = optimizeShape(slice.support, start);
|
slice.support = optimizeShape(slice.support, start);
|
||||||
start.copy(slice.support.lastPoint(true));
|
start = slice.support.lastPoint(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function optimizeShape(shape, start) {
|
function optimizeShape(shape, start) {
|
||||||
start = start.clone();
|
const inputPaths = shape.mapToLower().filter(path => path.length > 0);
|
||||||
|
|
||||||
const inputPaths = shape.mapToLower();
|
|
||||||
const optimizedPaths = [];
|
const optimizedPaths = [];
|
||||||
const donePaths = [];
|
const donePaths = [];
|
||||||
|
|
||||||
while (optimizedPaths.length !== inputPaths.length) {
|
while (optimizedPaths.length !== inputPaths.length) {
|
||||||
let minLength = false;
|
let minLength = Infinity;
|
||||||
let reverse;
|
let reverse;
|
||||||
let minPath;
|
let minPath;
|
||||||
let offset;
|
let offset;
|
||||||
@ -102,9 +100,8 @@ function optimizeShape(shape, start) {
|
|||||||
|
|
||||||
if (shape.closed) {
|
if (shape.closed) {
|
||||||
for (let j = 0; j < path.length; j += 1) {
|
for (let j = 0; j < path.length; j += 1) {
|
||||||
const point = new THREE.Vector2().copy(path[j]);
|
const length = distanceTo(path[j], start);
|
||||||
const length = point.sub(start).length();
|
if (length < minLength) {
|
||||||
if (minLength === false || length < minLength) {
|
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = length;
|
minLength = length;
|
||||||
offset = j;
|
offset = j;
|
||||||
@ -112,17 +109,15 @@ function optimizeShape(shape, start) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const startPoint = new THREE.Vector2().copy(path[0]);
|
const lengthToStart = distanceTo(path[0], start);
|
||||||
const lengthToStart = startPoint.sub(start).length();
|
if (lengthToStart < minLength) {
|
||||||
if (minLength === false || lengthToStart < minLength) {
|
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = lengthToStart;
|
minLength = lengthToStart;
|
||||||
reverse = false;
|
reverse = false;
|
||||||
pathIndex = i;
|
pathIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endPoint = new THREE.Vector2().copy(path[path.length - 1]);
|
const lengthToEnd = distanceTo(path[path.length - 1], start);
|
||||||
const lengthToEnd = endPoint.sub(start).length();
|
|
||||||
if (lengthToEnd < minLength) {
|
if (lengthToEnd < minLength) {
|
||||||
minPath = path;
|
minPath = path;
|
||||||
minLength = lengthToEnd;
|
minLength = lengthToEnd;
|
||||||
@ -132,20 +127,15 @@ function optimizeShape(shape, start) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let point;
|
|
||||||
if (shape.closed) {
|
if (shape.closed) {
|
||||||
minPath = minPath.concat(minPath.splice(0, offset));
|
minPath = minPath.concat(minPath.splice(0, offset));
|
||||||
point = minPath[0];
|
start = minPath[0];
|
||||||
} else {
|
} else {
|
||||||
if (reverse) {
|
if (reverse) minPath.reverse();
|
||||||
minPath.reverse();
|
start = minPath[minPath.length - 1];
|
||||||
}
|
|
||||||
point = minPath[minPath.length - 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
donePaths.push(pathIndex);
|
donePaths.push(pathIndex);
|
||||||
start.copy(point);
|
|
||||||
|
|
||||||
optimizedPaths.push(minPath);
|
optimizedPaths.push(minPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import Shape from 'clipper-js';
|
import Shape from '@doodle3d/clipper-js';
|
||||||
import Slice from './helpers/Slice.js';
|
import Slice from './helpers/Slice.js';
|
||||||
|
|
||||||
import { PRECISION } from '../constants.js';
|
import { PRECISION, MIN_AREA } from '../constants.js';
|
||||||
|
|
||||||
export default function shapesToSlices(shapes, settings) {
|
export default function shapesToSlices(shapes) {
|
||||||
const sliceLayers = [];
|
const sliceLayers = [];
|
||||||
|
|
||||||
for (let layer = 0; layer < shapes.length; layer ++) {
|
for (let layer = 0; layer < shapes.length; layer ++) {
|
||||||
@ -13,7 +13,8 @@ export default function shapesToSlices(shapes, settings) {
|
|||||||
.fixOrientation()
|
.fixOrientation()
|
||||||
.simplify('pftNonZero')
|
.simplify('pftNonZero')
|
||||||
.clean(1)
|
.clean(1)
|
||||||
.seperateShapes();
|
.thresholdArea(MIN_AREA / Math.pow(PRECISION, 2))
|
||||||
|
.separateShapes();
|
||||||
|
|
||||||
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
|
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
|
||||||
.clean(1);
|
.clean(1);
|
||||||
@ -27,23 +28,16 @@ export default function shapesToSlices(shapes, settings) {
|
|||||||
|
|
||||||
for (let i = 0; i < fillShapes.length; i ++) {
|
for (let i = 0; i < fillShapes.length; i ++) {
|
||||||
const fillShape = fillShapes[i];
|
const fillShape = fillShapes[i];
|
||||||
|
if (fillShape.paths.length === 0) continue;
|
||||||
|
|
||||||
slice.add(fillShape, true);
|
slice.add(fillShape, true);
|
||||||
|
|
||||||
// if (lineShapesClosed.paths.length > 0) {
|
if (lineShapesClosed.paths.length > 0) lineShapesClosed = lineShapesClosed.difference(fillShape);
|
||||||
// lineShapesClosed = lineShapesClosed.difference(closedShape);
|
if (lineShapesOpen.paths.length > 0) lineShapesOpen = lineShapesOpen.difference(fillShape);
|
||||||
// }
|
|
||||||
// if (lineShapesOpen.paths.length > 0) {
|
|
||||||
// lineShapesOpen = lineShapesOpen.difference(closedShape);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lineShapesClosed.paths.length > 0) {
|
if (lineShapesClosed.paths.length > 0) slice.add(lineShapesClosed, false);
|
||||||
slice.add(lineShapesClosed, false);
|
if (lineShapesOpen.paths.length > 0) slice.add(lineShapesOpen, false);
|
||||||
}
|
|
||||||
|
|
||||||
if (lineShapesOpen.paths.length > 0) {
|
|
||||||
slice.add(lineShapesOpen, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
sliceLayers.push(slice);
|
sliceLayers.push(slice);
|
||||||
}
|
}
|
||||||
|
@ -9,43 +9,26 @@ import addBrim from './addBrim.js';
|
|||||||
import optimizePaths from './optimizePaths.js';
|
import optimizePaths from './optimizePaths.js';
|
||||||
import shapesToSlices from './shapesToSlices.js';
|
import shapesToSlices from './shapesToSlices.js';
|
||||||
import slicesToGCode from './slicesToGCode.js';
|
import slicesToGCode from './slicesToGCode.js';
|
||||||
import detectOpenClosed from './detectOpenClosed.js';
|
|
||||||
import applyPrecision from './applyPrecision.js';
|
import applyPrecision from './applyPrecision.js';
|
||||||
// import removePrecision from './removePrecision.js';
|
import { hslToRgb } from './helpers/color.js';
|
||||||
|
import removePrecision from './removePrecision.js';
|
||||||
|
|
||||||
export default function(settings, geometry, onProgress) {
|
export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
const totalStages = 12;
|
const total = 11;
|
||||||
let current = -1;
|
let done = -1;
|
||||||
const updateProgress = (action) => {
|
const updateProgress = action => {
|
||||||
current ++;
|
done ++;
|
||||||
if (typeof onProgress !== 'undefined') {
|
if (onProgress) onProgress({ progress: { done, total, action } });
|
||||||
onProgress({
|
|
||||||
progress: {
|
|
||||||
done: current,
|
|
||||||
total: totalStages,
|
|
||||||
action
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
geometry.computeFaceNormals();
|
|
||||||
|
|
||||||
// get unique lines from geometry;
|
|
||||||
updateProgress('Constructing unique lines from geometry');
|
updateProgress('Constructing unique lines from geometry');
|
||||||
const lines = createLines(geometry, settings);
|
const { lines, faces } = createLines(geometry, settings);
|
||||||
|
|
||||||
updateProgress('Detecting open vs closed shapes');
|
|
||||||
detectOpenClosed(lines);
|
|
||||||
|
|
||||||
updateProgress('Calculating layer intersections');
|
updateProgress('Calculating layer intersections');
|
||||||
const {
|
const { layerPoints, layerFaceIndexes } = calculateLayersIntersections(lines, settings);
|
||||||
layerIntersectionIndexes,
|
|
||||||
layerIntersectionPoints
|
|
||||||
} = calculateLayersIntersections(lines, settings);
|
|
||||||
|
|
||||||
updateProgress('Constructing shapes from intersections');
|
updateProgress('Constructing shapes from intersections');
|
||||||
const shapes = intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings);
|
const shapes = intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings);
|
||||||
|
|
||||||
applyPrecision(shapes);
|
applyPrecision(shapes);
|
||||||
|
|
||||||
@ -65,12 +48,75 @@ export default function(settings, geometry, onProgress) {
|
|||||||
updateProgress('Optimizing paths');
|
updateProgress('Optimizing paths');
|
||||||
optimizePaths(slices, settings);
|
optimizePaths(slices, settings);
|
||||||
|
|
||||||
// removePrecision(slices);
|
removePrecision(slices);
|
||||||
|
|
||||||
updateProgress('Constructing gcode');
|
updateProgress('Constructing gcode');
|
||||||
const gcode = slicesToGCode(slices, settings);
|
const gcode = slicesToGCode(slices, settings);
|
||||||
|
|
||||||
updateProgress('Finished');
|
updateProgress('Finished');
|
||||||
|
|
||||||
|
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
|
||||||
|
|
||||||
|
gcode.gcode = new Blob([gcodeToString(gcode.gcode)], { type: 'text/plain' });
|
||||||
|
|
||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PRECISION = 1000;
|
||||||
|
function toFixedTrimmed(value) {
|
||||||
|
return (Math.round(value * PRECISION) / PRECISION).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gcodeToString(gcode) {
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import GCode from './helpers/GCode.js';
|
import GCode from './helpers/GCode.js';
|
||||||
import comb from './helpers/comb.js';
|
import comb from './helpers/comb.js';
|
||||||
import { PRECISION } from '../constants.js';
|
import { Z_OFFSET } from '../constants.js';
|
||||||
|
|
||||||
const PROFILE_TYPES = ['support', 'innerShell', 'outerShell', 'innerInfill', 'outerInfill', 'brim'];
|
const PROFILE_TYPES = ['support', 'innerShell', 'outerShell', 'innerInfill', 'outerInfill', 'brim'];
|
||||||
|
|
||||||
@ -9,17 +9,15 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
layerHeight,
|
layerHeight,
|
||||||
filamentThickness,
|
filamentThickness,
|
||||||
nozzleDiameter,
|
nozzleDiameter,
|
||||||
travelSpeed,
|
|
||||||
retraction,
|
retraction,
|
||||||
travel,
|
travel,
|
||||||
combing
|
combing
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
|
const gcode = new GCode(settings);
|
||||||
const lineSurfaceArea = nozzleDiameter * layerHeight;
|
gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness);
|
||||||
const nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
|
|
||||||
|
|
||||||
const gcode = new GCode(nozzleToFilamentRatio);
|
if (settings.startCode) gcode.addGCode(settings.startCode, settings);
|
||||||
|
|
||||||
const defaultProfile = {
|
const defaultProfile = {
|
||||||
travelProfile: travel,
|
travelProfile: travel,
|
||||||
@ -29,19 +27,20 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
let isFirstLayer = true;
|
let isFirstLayer = true;
|
||||||
for (let layer = 0; layer < slices.length; layer ++) {
|
for (let layer = 0; layer < slices.length; layer ++) {
|
||||||
const slice = slices[layer];
|
const slice = slices[layer];
|
||||||
const z = layer * layerHeight + 0.2;
|
const z = layer * layerHeight + Z_OFFSET;
|
||||||
|
|
||||||
if (layer === 1) {
|
if (layer === 1) {
|
||||||
|
gcode.updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness);
|
||||||
gcode.turnFanOn();
|
gcode.turnFanOn();
|
||||||
isFirstLayer = false;
|
isFirstLayer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profiles = PROFILE_TYPES.reduce((profiles, profileType) => {
|
const profiles = PROFILE_TYPES.reduce((_profiles, profileType) => {
|
||||||
profiles[profileType] = {
|
_profiles[profileType] = {
|
||||||
...defaultProfile,
|
...defaultProfile,
|
||||||
lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType]
|
lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType]
|
||||||
};
|
};
|
||||||
return profiles;
|
return _profiles;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
if (typeof slice.brim !== 'undefined') {
|
if (typeof slice.brim !== 'undefined') {
|
||||||
@ -52,7 +51,7 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
const part = slice.parts[i];
|
const part = slice.parts[i];
|
||||||
|
|
||||||
if (part.closed) {
|
if (part.closed) {
|
||||||
const outline = part.shell[0];
|
const outline = part.shell[0].mapToLower();
|
||||||
|
|
||||||
for (let i = 0; i < part.shell.length; i ++) {
|
for (let i = 0; i < part.shell.length; i ++) {
|
||||||
const shell = part.shell[i];
|
const shell = part.shell[i];
|
||||||
@ -60,11 +59,11 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
|
|
||||||
const unRetract = isOuterShell;
|
const unRetract = isOuterShell;
|
||||||
const profile = isOuterShell ? profiles.outerShell : profiles.innerShell;
|
const profile = isOuterShell ? profiles.outerShell : profiles.innerShell;
|
||||||
pathToGCode(outline, combing && true, gcode, shell, false, unRetract, z, profile);
|
pathToGCode(outline, combing, gcode, shell, false, unRetract, z, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pathToGCode(outline, combing && true, gcode, part.outerFill, false, false, z, profiles.outerInfill);
|
pathToGCode(outline, combing, gcode, part.outerFill, false, false, z, profiles.outerInfill);
|
||||||
pathToGCode(outline, combing && true, gcode, part.innerFill, true, false, z, profiles.innerInfill);
|
pathToGCode(outline, combing, gcode, part.innerFill, true, false, z, profiles.innerInfill);
|
||||||
} else {
|
} else {
|
||||||
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
|
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
|
||||||
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
|
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
|
||||||
@ -72,14 +71,18 @@ export default function slicesToGCode(slices, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof slice.support !== 'undefined') {
|
if (typeof slice.support !== 'undefined') {
|
||||||
pathToGCode(null, false, gcode, slice.support, true, true, z, profiles.support);
|
const supportOutline = slice.supportOutline.mapToLower();
|
||||||
|
pathToGCode(supportOutline, combing, gcode, slice.support, true, true, z, profiles.support);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.endCode) gcode.addGCode(settings.endCode, settings);
|
||||||
|
|
||||||
return gcode.getGCode();
|
return gcode.getGCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, { lineProfile, travelProfile, retractionProfile }) {
|
function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, profiles) {
|
||||||
|
const { lineProfile, travelProfile, retractionProfile } = profiles;
|
||||||
const { closed } = shape;
|
const { closed } = shape;
|
||||||
const paths = shape.mapToLower();
|
const paths = shape.mapToLower();
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, { li
|
|||||||
|
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
if (combing) {
|
if (combing) {
|
||||||
const combPath = comb(outline, gcode._nozzlePosition.divideScalar(PRECISION), point);
|
const combPath = comb(outline, gcode._nozzlePosition, point);
|
||||||
for (let i = 0; i < combPath.length; i ++) {
|
for (let i = 0; i < combPath.length; i ++) {
|
||||||
const combPoint = combPath[i];
|
const combPoint = combPath[i];
|
||||||
gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);
|
gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);
|
||||||
|
106
src/slicer.js
106
src/slicer.js
@ -2,18 +2,18 @@ import * as THREE from 'three';
|
|||||||
import slice from './sliceActions/slice.js';
|
import slice from './sliceActions/slice.js';
|
||||||
import SlicerWorker from './slicer.worker.js';
|
import SlicerWorker from './slicer.worker.js';
|
||||||
|
|
||||||
export function sliceMesh(settings, mesh, sync = false, onProgress) {
|
export function sliceMesh(settings, mesh, sync = false, constructLinePreview = false, onProgress) {
|
||||||
if (typeof mesh === 'undefined' || !mesh.isMesh) {
|
if (!mesh || !mesh.isMesh) {
|
||||||
throw new Error('Provided mesh is not intance of THREE.Mesh');
|
throw new Error('Provided mesh is not intance of THREE.Mesh');
|
||||||
}
|
}
|
||||||
|
|
||||||
mesh.updateMatrix();
|
mesh.updateMatrix();
|
||||||
const { geometry, matrix } = mesh;
|
const { geometry, matrix, material } = mesh;
|
||||||
return sliceGeometry(settings, geometry, matrix, sync, onProgress);
|
return sliceGeometry(settings, geometry, material, matrix, sync, constructLinePreview, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sliceGeometry(settings, geometry, matrix, sync = false, onProgress) {
|
export function sliceGeometry(settings, geometry, materials, matrix, sync = false, constructLinePreview = false, onProgress) {
|
||||||
if (typeof geometry === 'undefined') {
|
if (!geometry) {
|
||||||
throw new Error('Missing required geometry argument');
|
throw new Error('Missing required geometry argument');
|
||||||
} else if (geometry.isBufferGeometry) {
|
} else if (geometry.isBufferGeometry) {
|
||||||
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
|
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
|
||||||
@ -23,34 +23,64 @@ export function sliceGeometry(settings, geometry, matrix, sync = false, onProgre
|
|||||||
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
|
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geometry.faces.length === 0) {
|
if (matrix && matrix.isMatrix4) geometry.applyMatrix(matrix);
|
||||||
throw new Error('Geometry does not contain any data');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matrix) {
|
const vertices = geometry.vertices.reduce((array, { x, y, z }, i) => {
|
||||||
geometry.applyMatrix(matrix);
|
const i3 = i * 3;
|
||||||
}
|
array[i3] = x;
|
||||||
|
array[i3 + 1] = y;
|
||||||
|
array[i3 + 2] = z;
|
||||||
|
return array;
|
||||||
|
}, new Float32Array(geometry.vertices.length * 3));
|
||||||
|
const faces = geometry.faces.reduce((array, { a, b, c }, i) => {
|
||||||
|
const i3 = i * 3;
|
||||||
|
array[i3] = a;
|
||||||
|
array[i3 + 1] = b;
|
||||||
|
array[i3 + 2] = c;
|
||||||
|
return array;
|
||||||
|
}, new Uint32Array(geometry.faces.length * 3));
|
||||||
|
const objectIndexes = geometry.faces.reduce((array, { materialIndex }, i) => {
|
||||||
|
array[i] = materialIndex;
|
||||||
|
return array;
|
||||||
|
}, new Uint8Array(geometry.faces.length));
|
||||||
|
|
||||||
|
if (faces.length === 0) throw new Error('Geometry does not contain any data');
|
||||||
|
|
||||||
|
geometry = { vertices, faces, objectIndexes };
|
||||||
|
|
||||||
|
const openObjectIndexes = materials instanceof Array ? materials.map(({ side }) => {
|
||||||
|
switch (side) {
|
||||||
|
case THREE.FrontSide:
|
||||||
|
return false;
|
||||||
|
case THREE.DoubleSide:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}) : [false];
|
||||||
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
return sliceSync(settings, geometry, onProgress);
|
return sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
||||||
} else {
|
} else {
|
||||||
return sliceAsync(settings, geometry, onProgress);
|
return sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceSync(settings, geometry, onProgress) {
|
function sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
return slice(settings, geometry, onProgress);
|
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
|
||||||
|
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
|
||||||
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceAsync(settings, geometry, onProgress) {
|
function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// create the slicer worker
|
// create the slicer worker
|
||||||
const slicerWorker = new SlicerWorker();
|
const slicerWorker = new SlicerWorker();
|
||||||
|
|
||||||
slicerWorker.onerror = error => {
|
slicerWorker.addEventListener('error', event => {
|
||||||
slicerWorker.terminate();
|
slicerWorker.terminate();
|
||||||
reject(error);
|
reject(event);
|
||||||
};
|
});
|
||||||
|
|
||||||
// listen to messages send from worker
|
// listen to messages send from worker
|
||||||
slicerWorker.addEventListener('message', (event) => {
|
slicerWorker.addEventListener('message', (event) => {
|
||||||
@ -58,26 +88,40 @@ function sliceAsync(settings, geometry, onProgress) {
|
|||||||
switch (message) {
|
switch (message) {
|
||||||
case 'SLICE': {
|
case 'SLICE': {
|
||||||
slicerWorker.terminate();
|
slicerWorker.terminate();
|
||||||
resolve(data.gcode);
|
|
||||||
|
const { gcode } = data;
|
||||||
|
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
|
||||||
|
|
||||||
|
resolve(gcode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'PROGRESS': {
|
case 'PROGRESS': {
|
||||||
if (typeof onProgress !== 'undefined') {
|
if (typeof onProgress !== 'undefined') onProgress(data);
|
||||||
onProgress(data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// send geometry and settings to worker to start the slicing progress
|
const { vertices, faces, objectIndexes } = geometry;
|
||||||
geometry = geometry.toJSON();
|
const buffers = [vertices.buffer, faces.buffer, objectIndexes.buffer];
|
||||||
|
|
||||||
slicerWorker.postMessage({
|
slicerWorker.postMessage({
|
||||||
message: 'SLICE',
|
message: 'SLICE',
|
||||||
data: {
|
data: { settings, geometry, openObjectIndexes, constructLinePreview }
|
||||||
settings,
|
}, buffers);
|
||||||
geometry
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function constructLineGeometry(linePreview) {
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
|
||||||
|
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(linePreview.positions), 3));
|
||||||
|
geometry.addAttribute('color', new THREE.BufferAttribute(new Float32Array(linePreview.colors), 3));
|
||||||
|
|
||||||
|
const material = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors });
|
||||||
|
const mesh = new THREE.LineSegments(geometry, material);
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
@ -1,30 +1,34 @@
|
|||||||
import 'core-js'; // polyfills
|
import 'core-js'; // polyfills
|
||||||
import slice from './sliceActions/slice.js';
|
import slice from './sliceActions/slice.js';
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
const loader = new THREE.JSONLoader();
|
|
||||||
|
|
||||||
const onProgress = progress => {
|
const onProgress = progress => {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
message: 'PROGRESS',
|
message: 'PROGRESS',
|
||||||
data: progress
|
data: progress
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener('message', (event) => {
|
||||||
const { message, data } = event.data;
|
const { message, data } = event.data;
|
||||||
switch (message) {
|
switch (message) {
|
||||||
case 'SLICE': {
|
case 'SLICE': {
|
||||||
const { settings, geometry: JSONGeometry } = data;
|
const { settings, geometry, constructLinePreview, openObjectIndexes } = data;
|
||||||
const { geometry } = loader.parse(JSONGeometry.data);
|
|
||||||
|
|
||||||
const gcode = slice(settings, geometry, onProgress);
|
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({
|
self.postMessage({
|
||||||
message: 'SLICE',
|
message: 'SLICE',
|
||||||
data: { gcode }
|
data: { gcode }
|
||||||
});
|
}, buffers);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
92
webpack.config.js
Normal file
92
webpack.config.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user