Compare commits

...

245 Commits

Author SHA1 Message Date
Rick Companje
41be8dcfd1 Update package.json 2021-05-27 00:30:18 +02:00
casper
a54a716d6d update css 2021-05-24 17:05:55 +02:00
casper
c472739baa update refs 2021-05-22 17:37:41 +02:00
casper
8e01a5c2e2 reify code formatting fix 2021-05-22 15:23:52 +02:00
casper
06efd35119 Fix install
need to install with `npm i --force` now
2021-05-16 13:22:05 +02:00
Casper Lamboo
3b440b346a change font bold 2019-02-21 20:54:59 +01:00
Casper Lamboo
8015cd780d disable selecting titles 2019-02-21 20:54:48 +01:00
Casper Lamboo
01f40eb5f8 Display settings inside accordion element 2019-02-21 20:39:05 +01:00
Rick Companje
49b4a2be59 separate Download and Print buttons 2019-02-21 20:13:31 +01:00
Rick Companje
7df13f6db0 remove Doodle3D Printer, move Duplicator i3 Mini 2019-02-21 20:11:54 +01:00
Rick Companje
766cadcd44 fix point sorting 2018-11-27 15:47:34 +01:00
Rick Companje
cbc439b4dc Update printer.yml 2018-11-27 14:14:39 +01:00
Rick Companje
7da0b2fa17 Merge branch 'master' of https://github.com/Doodle3D/Doodle3D-Slicer 2018-11-27 14:13:25 +01:00
Rick Companje
5cb91e6c5a Revert "printer duplicator i3 mini"
This reverts commit 92593cef14d97461d507b57321a62816736004a9.
2018-11-27 14:13:08 +01:00
Rick Companje
92593cef14 printer duplicator i3 mini 2018-11-27 14:12:29 +01:00
Casper Lamboo
ebbc985d67 remove react on touch tap 2018-06-26 16:08:33 +02:00
Casper Lamboo
246f1627c5 Merge branch 'develop' 2018-05-29 11:36:43 +02:00
Casper Lamboo
6971c3c4b5 Merge branch 'feature/comb' into develop 2018-05-28 15:17:33 +02:00
Casper Lamboo
fc3dc7355c reformat 2018-05-28 13:53:53 +02:00
Casper Lamboo
3944202a83 add tau constant 2018-05-28 13:53:32 +02:00
Casper Lamboo
fd6e5cebbd remove normals 2018-05-28 13:53:09 +02:00
Casper Lamboo
ead6be081f Merge branch 'develop' into feature/comb 2018-05-28 11:57:24 +02:00
Casper Lamboo
d63754dbd0 fix slicing single walled paths 2018-05-28 11:57:16 +02:00
Casper Lamboo
2044929808 turn of combing by default 2018-05-24 16:27:22 +02:00
Casper Lamboo
7dceeda291 update combing 2018-05-24 16:14:03 +02:00
Casper Lamboo
86eed64255 remove eruct 2018-05-24 16:13:56 +02:00
Casper Lamboo
caf36a5505 remove unused imports 2018-05-24 16:13:44 +02:00
Casper Lamboo
1493ae3536 add extra option within containLineInPath 2018-05-05 10:19:30 +02:00
Casper Lamboo
5900bbcc50 remove unused imports 2018-05-05 10:18:20 +02:00
Casper Lamboo
2c953496f7 share functions in comb.js 2018-05-02 17:42:31 +02:00
Casper Lamboo
c642375295 update containLineInPath func 2018-05-02 17:39:27 +02:00
Casper Lamboo
942addf8d7 Merge branch 'develop' into feature/comb 2018-05-02 17:18:54 +02:00
Casper Lamboo
b7269da172 update example 2018-05-02 17:18:48 +02:00
Casper Lamboo
50ff72a037 change distance check to 3 2018-05-02 16:54:19 +02:00
Casper Lamboo
60fb966ccb add hash to scripts 2018-05-02 16:34:35 +02:00
Casper Lamboo
0f70855989 update example 2018-05-02 16:29:44 +02:00
Casper Lamboo
e966bc89b2 performance 2018-05-02 16:27:54 +02:00
Casper Lamboo
6c8b8e9d44 implement new combing 2018-05-02 15:12:45 +02:00
Casper Lamboo
79e4acd3d1 add demo 2018-05-02 15:07:03 +02:00
Casper Lamboo
59681e8023 reverse order of commands 2018-04-23 14:22:40 +02:00
Casper Lamboo
f109147e62 only request doodle printers when adding or managing printers 2018-04-23 12:27:16 +02:00
Casper Lamboo
0119c91001 make case sensitive 2018-04-23 11:57:48 +02:00
Casper Lamboo
8467da3894 Merge branch 'master' into develop 2018-04-23 11:56:52 +02:00
Casper Lamboo
808d585f2a remove whole line for heated bed instead of making comment 2018-04-23 11:56:41 +02:00
Rick Companje
0804dd5282
fix {if heatedBed} 2018-04-23 11:46:26 +02:00
Casper Lamboo
0957d53b41 fix font loading
@companje
2018-04-23 10:55:11 +02:00
Casper Lamboo
0be1ee6d51 comply with linter 2018-04-17 14:17:28 +02:00
Casper Lamboo
5a40c7c647 update web pack config 2018-04-17 11:44:51 +02:00
Casper Lamboo
141c38d878 setting up linter 2018-04-17 11:29:17 +02:00
Casper Lamboo
97cb6e062d fix percentage of malyan printer 2018-04-16 21:03:15 +02:00
Casper Lamboo
264ea9ff00 make filename configurable through query 2018-04-16 16:26:44 +02:00
Casper Lamboo
374fc4a32e remove old path when joining two paths
#42
2018-04-12 11:06:47 +02:00
Casper Lamboo
396502948b Fix bug where some shapes are skipped 2018-04-03 21:33:34 +02:00
Casper Lamboo
a0b9cd1306 don't trim 2018-04-03 16:07:49 +02:00
Casper Lamboo
75c3e9d632 Merge branch 'master' into develop 2018-04-03 15:22:10 +02:00
Casper Lamboo
06ce0b0d6b bump version 2018-04-03 14:02:58 +02:00
Casper Lamboo
74ed9e58e4 Merge branch 'develop' 2018-04-03 14:02:36 +02:00
Casper Lamboo
6be1291923 cleanup 2018-04-03 11:38:49 +02:00
Casper Lamboo
701d736cc0 store gcode as blob instead of string
Less prone to crashing
2018-04-03 11:38:40 +02:00
Casper Lamboo
61722a6985 change order of multiplying matrix 2018-03-29 15:37:32 +02:00
Rick Companje
ef7ceb7849
Update printer.yml 2018-03-28 09:15:42 +02:00
Rick Companje
5302857b5e
Update printer.yml 2018-03-28 09:14:57 +02:00
Rick Companje
5a51ddf0f6
Update default.yml 2018-03-28 09:11:04 +02:00
Rick Companje
14c33c0225
Update infill.yml 2018-03-28 09:02:56 +02:00
Casper Lamboo
5a6468baef fix orientation of printed doodles
#43
2018-03-26 10:28:46 +02:00
Rick Companje
763c6e08e8
Update printer.yml 2018-03-19 16:50:01 +01:00
Casper Lamboo
5b2c3bb80a fix key property of map function 2018-03-15 14:26:04 +01:00
Casper Lamboo
f65ab470bb fix heatedBed
@companje
2018-03-15 14:25:53 +01:00
Casper Lamboo
991586dc4c add missing break 2018-03-07 21:25:15 +01:00
Casper Lamboo
5e88cd8c17 fix unknown target 2018-03-07 19:13:05 +01:00
Casper Lamboo
38ab39f7de add custom actions 2018-03-07 18:21:44 +01:00
Rick Companje
df6e084503
Update default.yml 2018-03-07 16:53:45 +01:00
Casper Lamboo
c149667409 Only change value of form onblur 2018-03-07 14:41:42 +01:00
Casper Lamboo
d341cc28c9 use flat shading 2018-03-07 11:52:42 +01:00
Casper Lamboo
e1c4e2c1d4 open printer popup with ip if printer is unknown 2018-03-07 11:46:50 +01:00
Casper Lamboo
bc67cab75f Add selected printer query param
@companje
2018-03-07 11:14:02 +01:00
Casper Lamboo
229929e4e8 update docs 2018-03-06 17:49:39 +01:00
Casper Lamboo
a3f5c398da fix ordering connect points 2018-03-06 17:44:01 +01:00
Casper Lamboo
cd8687d148 fix filled shapes 2018-03-06 17:35:03 +01:00
Casper Lamboo
e056b37677 rename vars 2018-03-06 17:26:32 +01:00
Casper Lamboo
9a8f96a844 update web pack config 2018-03-06 17:17:05 +01:00
Casper Lamboo
b29198edd4 fix rounding errors
#38
2018-03-06 17:16:33 +01:00
Casper Lamboo
8bb97527f4 Fix incorrect order of points
#41
2018-03-06 17:01:52 +01:00
Casper Lamboo
16e6e11e2f Fix ignored last line 2018-03-06 12:41:00 +01:00
Casper Lamboo
c4d8d1136a use flat shading 2018-03-06 12:25:49 +01:00
Casper Lamboo
34a2b6cafc remove comma 2018-02-19 18:02:57 +01:00
Casper Lamboo
f032d8c267 Fix support
#39
2018-02-19 16:26:13 +01:00
Casper Lamboo
1107353290 lock doodle3d core version 2018-02-19 13:53:44 +01:00
Casper Lamboo
3d308e2533 fix intersections to shapes step 2018-02-19 13:53:18 +01:00
Casper Lamboo
793f8100fb change order of axis in dimensions preview 2018-02-19 13:48:55 +01:00
casperlamboo
120b49dfb7 fix some edge cases in intersections to shapes 2018-02-13 16:37:50 +01:00
casperlamboo
1434006f95 fix mapping to object 2018-02-13 00:01:41 +01:00
casperlamboo
8313982342 slice in worker 2018-02-12 23:58:12 +01:00
casperlamboo
e087fadd80 cleanup 2018-02-12 17:36:07 +01:00
casperlamboo
2b4941eefb allow adding doodle3d printer without ip 2018-02-12 15:34:46 +01:00
casperlamboo
c835ea12b2 don't export internal checkbox 2018-02-12 14:20:50 +01:00
casperlamboo
caf5e655da calculate face normals in slice process 2018-02-12 11:41:51 +01:00
casperlamboo
ca886afa25 rename file 2018-02-12 11:13:50 +01:00
casperlamboo
dbc01167e5 fix line preview 2018-02-12 11:07:06 +01:00
casperlamboo
45b0f541b1 send typed array instead of string across worker 2018-02-12 10:28:42 +01:00
casperlamboo
4352293e95 don't combine layerpoints and layer face indexes into objects 2018-02-12 00:36:05 +01:00
casperlamboo
4b17325c3f don't send instance of geometry to slicer
making importing three obsolete, reducing te worker from 700kb to 200kb
2018-02-12 00:10:44 +01:00
casperlamboo
03f95b7570 rename vectorutils 2018-02-11 23:50:57 +01:00
casperlamboo
5944153de7 disable line priview 2018-02-11 23:28:25 +01:00
casperlamboo
774289895e update doodle box url 2018-02-06 17:28:25 +01:00
casperlamboo
4bf14d3a1d rename cancel close 2018-02-06 17:21:39 +01:00
casperlamboo
34147a918e Unable to close add printer popup when no printer is added 2018-02-06 11:21:40 +01:00
casperlamboo
78764383e1 allow for empty ip in add doodle printer 2018-02-06 11:16:41 +01:00
casperlamboo
a08223f243 print can now parse urls with credentials 2018-02-05 14:38:43 +01:00
casperlamboo
89d4a659db reduce tolerance of min combing distance to 1mm 2018-02-01 18:29:20 +01:00
casperlamboo
0b6a4a1588 don't use inverse 2018-02-01 18:19:45 +01:00
casperlamboo
bc06f703ac move casting to string in slice.js 2018-02-01 18:13:55 +01:00
casperlamboo
6a63077b55 don't use three.js in worker 2018-02-01 17:57:20 +01:00
casperlamboo
c927c7bec1 typo 2018-02-01 16:59:35 +01:00
casperlamboo
0ef85cc918 fix intersecties to shapes code 2018-02-01 16:13:04 +01:00
casperlamboo
4fc34d0272 updated startcode
@companje
2018-02-01 14:40:26 +01:00
casperlamboo
aad4c1564c can connect to both start and end in one cycle 2018-02-01 14:39:14 +01:00
casperlamboo
8f475195b8 clean up create lines 2018-02-01 12:23:34 +01:00
casperlamboo
64d28affe6 improve intersections to shapes step
#36
2018-02-01 12:11:57 +01:00
casperlamboo
491067b070 use blob instead of file
for iOS 9
2018-01-31 14:17:21 +01:00
casperlamboo
3b99a3c494 update doodle3d api 2018-01-31 14:04:08 +01:00
casperlamboo
4cea3b3086 use blob instead of file so it can be used on ios9 2018-01-31 13:32:16 +01:00
casperlamboo
dd007b8bbf Link to connect.doodle3d.com
#37
2018-01-31 11:46:03 +01:00
casperlamboo
4d93ee2e9d add favicon 2018-01-30 19:41:30 +01:00
casperlamboo
8fde65a78a remove unused import 2018-01-30 17:01:54 +01:00
casperlamboo
fded534a10 remove log 2018-01-30 14:32:31 +01:00
casperlamboo
9cc64f44a5 fix component will unmount 2018-01-30 12:01:37 +01:00
casperlamboo
07f8255a37 move styles of logo to jss 2018-01-30 11:07:52 +01:00
Rick Companje
f809c28d26 added logo 2018-01-30 00:19:05 +01:00
casperlamboo
9eb01f2f21 decrease z offset 2018-01-29 17:17:11 +01:00
casperlamboo
377088fa34 fix wegpack config 2018-01-29 17:13:19 +01:00
casperlamboo
47e44dfed6 Trim extra zero in gcode 2018-01-29 17:13:19 +01:00
Rick Companje
602b5b35fe sliceInfo 2018-01-29 17:12:27 +01:00
casperlamboo
13cc1238f1 Fix infill settings 2018-01-29 16:21:12 +01:00
Rick Companje
4a3a5d832c Merge branch 'develop' of https://github.com/Doodle3D/Doodle3D-Slicer into develop 2018-01-29 16:14:44 +01:00
Rick Companje
a422956146 adding infill settings 2018-01-29 16:14:28 +01:00
casperlamboo
556d2f3eab Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	package-lock.json
2018-01-29 16:13:45 +01:00
casperlamboo
a88db15c96 fix building dist 2018-01-29 16:11:22 +01:00
Rick Companje
407d3355c0 adding infill settings 2018-01-29 16:11:19 +01:00
casperlamboo
4482bf1f73 compensate for higher layer height 2018-01-29 15:15:27 +01:00
casperlamboo
0c7f08735f implement printing to ultimaker2go 2018-01-29 13:59:29 +01:00
casperlamboo
cd2b0322d8 fix setting state in unmounted 2018-01-29 12:30:05 +01:00
casperlamboo
635e01fd01 update fetch progress code 2018-01-29 12:03:22 +01:00
casperlamboo
67b4084e55 add idle check for doodle3d printer 2018-01-29 12:03:01 +01:00
casperlamboo
21bd6c4714 decrease autoupdate interval 2018-01-29 11:04:08 +01:00
casperlamboo
cdb12f4d78 remove double setting 2018-01-26 09:31:49 +01:00
casperlamboo
a3c028a71c configure web pack for doodle3d api 2018-01-25 18:03:39 +01:00
casperlamboo
61b3d61f8a disable malign and wifi box controls 2018-01-25 18:03:39 +01:00
casperlamboo
abf426c5f2 Add export to wifi box 2018-01-25 18:03:39 +01:00
casperlamboo
fb06bfb135 add comment 2018-01-25 18:03:39 +01:00
Rick Companje
e289fd848a z offset 2018-01-25 17:47:42 +01:00
Rick Companje
8c3d444eed design 2018-01-25 17:47:36 +01:00
casperlamboo
0067c7a9e1 update scripts 2018-01-25 12:17:59 +01:00
casperlamboo
109cd0898c move malyan controls next to print button 2018-01-25 12:17:14 +01:00
casperlamboo
e091f425a1 simplify progress updating
by @companje
2018-01-25 12:13:11 +01:00
casperlamboo
e09ed8012c use fetch no-cors mode to make form posts to malyan
so we can upload from cross origin
because fetch has no progress we fake malyan upload progress by
updating 20kb every second

@companje
2018-01-25 12:12:47 +01:00
casperlamboo
56929c6af7 concat doodle3d printer and the doodle3d wifi box into one target 2018-01-24 16:06:39 +01:00
casperlamboo
de2acfe6be Add malyan controls
not styled yet
2018-01-24 16:03:44 +01:00
casperlamboo
6628b9cf13 Merge branch 'feature/support' into develop 2018-01-24 14:29:28 +01:00
casperlamboo
5cef2777fb save precision squared in const 2018-01-24 14:00:49 +01:00
casperlamboo
70aa39d89f support infill is always bidirectional 2018-01-24 13:59:39 +01:00
casperlamboo
1af76e6ef1 Merge branch 'develop' into feature/support 2018-01-24 12:50:47 +01:00
casperlamboo
88291ba549 implement support
#33
2018-01-24 12:50:41 +01:00
casperlamboo
5b934f0e71 rename infill density to density 2018-01-24 12:39:56 +01:00
casperlamboo
7906bfe43d remove weird code 2018-01-24 12:39:34 +01:00
casperlamboo
5d49ee0c74 send code "M563 S4" before uploading 2018-01-23 16:58:28 +01:00
casperlamboo
67981872aa implement print to malyan 2018-01-23 16:35:04 +01:00
casperlamboo
e384c74f8e fix typo 2018-01-23 12:36:05 +01:00
casperlamboo
d5ea670967 add ip in settings config for the doodle3d printer 2018-01-23 12:19:43 +01:00
casperlamboo
4dc5e4849e update fetch progress 2018-01-22 18:12:33 +01:00
casperlamboo
f28722aec5 rename percentage to density 2018-01-18 16:32:54 +01:00
casperlamboo
485f741077 change min area 2018-01-18 15:33:12 +01:00
casperlamboo
2f04aa9c50 fix threshold area function
#18
2018-01-18 15:30:08 +01:00
casperlamboo
ca3718e492 Remove tiny holes
#18
2018-01-18 15:27:08 +01:00
casperlamboo
0bb646a5ac move z offset to constant 2018-01-18 14:24:01 +01:00
casperlamboo
60c70cdbd5 difference line shapes with fill shapes 2018-01-18 14:02:34 +01:00
casperlamboo
198ca783f7 update package lock 2018-01-18 13:59:42 +01:00
casperlamboo
961337138b fix grid size calculation 2018-01-18 12:38:35 +01:00
casperlamboo
a892d6ff89 update package lock 2018-01-18 12:24:20 +01:00
casperlamboo
d4743ef867 improve bundle size 2018-01-18 12:06:14 +01:00
casperlamboo
46408e0668 add analyse script 2018-01-18 11:40:57 +01:00
casperlamboo
bbd8bc529d clean up 2018-01-18 11:14:21 +01:00
casperlamboo
0f7da85453 Revert "Fix brim"
This reverts commit eee2682f7064a64093c3025fdb3629291561e2e9.
2018-01-17 23:53:49 +01:00
casperlamboo
082329b810 fix loading mesh 2018-01-17 23:53:40 +01:00
casperlamboo
b562d3c2e2 allow for drag and dropping files 2018-01-17 17:42:58 +01:00
casperlamboo
a568a79ede don't require a model for the slicer interface 2018-01-17 17:18:28 +01:00
casperlamboo
742783e4db add margin to percentage container 2018-01-17 16:52:14 +01:00
casperlamboo
ac85bbc6d5 use primary buttons 2018-01-17 16:15:13 +01:00
casperlamboo
53e961b9bb fix min and max props 2018-01-17 16:10:22 +01:00
casperlamboo
11222aaa82 close url dialog by default 2018-01-17 15:55:21 +01:00
casperlamboo
1f4ca15442 Show dialog for closed popups
#30
2018-01-17 15:53:03 +01:00
casperlamboo
222a27d5b5 fix catching errors in the worker 2018-01-17 15:27:42 +01:00
casperlamboo
9f1958563d remove primary color 3 2018-01-17 14:58:36 +01:00
casperlamboo
a8b3d68845 order advanced settings based on most use 2018-01-17 14:21:59 +01:00
casperlamboo
2f4adbbb47 use mui theme for colors 2018-01-17 13:26:30 +01:00
casperlamboo
2fdc5ca16b disable selecting for title and details 2018-01-17 12:23:03 +01:00
casperlamboo
291e11fecf update style 2018-01-17 12:21:36 +01:00
casperlamboo
89e67882a0 cast number as string 2018-01-17 12:00:19 +01:00
casperlamboo
137f95fdba Add constrains to fields
#32
2018-01-17 11:56:42 +01:00
casperlamboo
9b78f4e2c8 move ultimaker 2go 2018-01-17 11:22:50 +01:00
casperlamboo
879667fa05 add manage printer dialog 2018-01-17 11:04:22 +01:00
casperlamboo
a48768e268 replace clear icon with refresh icon 2018-01-17 10:23:23 +01:00
casperlamboo
5fbd7f50ec Update title 2018-01-17 09:11:15 +01:00
casperlamboo
e06281667a change html title 2018-01-17 09:02:50 +01:00
casperlamboo
d190625f14 Fix form element 2018-01-17 09:01:40 +01:00
casperlamboo
9764e0a374 Add dimensions details to 3d panel 2018-01-17 08:40:48 +01:00
casperlamboo
e1d833d4f3 syntax 2018-01-16 18:52:08 +01:00
casperlamboo
f20f5b95b8 Change infill gridsize to infill percentage
#31
2018-01-16 18:52:03 +01:00
casperlamboo
7b59ba1108 implement local storage 2018-01-16 17:57:34 +01:00
casperlamboo
9d47e8dc23 change default brim size 2018-01-15 17:47:50 +01:00
casperlamboo
eee2682f70 Fix brim
brim now always prints the most outer layer first
2018-01-15 16:41:06 +01:00
casperlamboo
1ebbe7fc6a implement brim 2018-01-15 16:30:20 +01:00
casperlamboo
9a37e8a928 Revert "lower z offset"
This reverts commit 55eadc73debcd10969f640ba0e7fd9a8d5d2910c.
2018-01-15 15:55:48 +01:00
casperlamboo
da3ab2b0e6 Added default line to gcode 2018-01-15 15:43:58 +01:00
casperlamboo
700b27e6e0 add combing setting 2018-01-15 15:29:40 +01:00
casperlamboo
55eadc73de lower z offset 2018-01-15 15:27:12 +01:00
casperlamboo
03b9570014 change default bed temperature 2018-01-15 15:24:09 +01:00
casperlamboo
457f110dd2 slicer can now fetch d3 files from cloud 2018-01-15 15:17:38 +01:00
casperlamboo
0d64b62f12 update bed temperature of doodle3d printer 2018-01-15 14:40:52 +01:00
casperlamboo
212075e306 pack scene state in scene object 2018-01-15 14:21:42 +01:00
casperlamboo
ecc37273ca better error throwing 2018-01-15 13:47:16 +01:00
casperlamboo
10fb3714c7 add support for start and end code 2018-01-15 13:44:59 +01:00
casperlamboo
43af4e05ab add doodle3d printer 2018-01-15 13:43:53 +01:00
casperlamboo
2aee317d42 move ultimaker 2 go 2018-01-15 13:09:31 +01:00
casperlamboo
6f735b5de4 Merge branch 'master' into develop 2018-01-15 11:53:48 +01:00
casperlamboo
246522ee5f Add download g-code button 2017-12-24 17:18:33 +01:00
casperlamboo
c1cbe4f280 clean up 2017-12-24 14:53:34 +01:00
casperlamboo
d9edfe9bde disable control buttons instead of hiding them while slicing 2017-12-24 14:52:24 +01:00
casperlamboo
54811b27e9 rename base to default 2017-12-24 14:46:00 +01:00
casperlamboo
745c675f67 add transform-class-properties 2017-12-21 12:49:44 +01:00
casperlamboo
9fc1ba1cb8 babel rc 2017-12-19 16:23:45 +01:00
casperlamboo
68df877e1d babelrc 2017-12-19 16:16:48 +01:00
casperlamboo
346256ff47 babel rc 2017-12-19 16:11:17 +01:00
casperlamboo
a7b2852c6e Revert "remove babelrc"
This reverts commit 175689bd646b87f125f1610e65da1ef9c2dac4b7.
2017-12-19 16:03:42 +01:00
casperlamboo
175689bd64 remove babelrc 2017-12-19 16:03:28 +01:00
casperlamboo
c91ee0a5e9 fix babel rc 2017-12-19 16:00:27 +01:00
casperlamboo
5e959fa634 fix babelrc 2017-12-19 15:56:48 +01:00
casperlamboo
264ed096a4 Add layer height property 2017-12-19 14:37:43 +01:00
casperlamboo
b830cc611b edit margins 2017-12-19 14:34:58 +01:00
casperlamboo
6e55ca7a79 slice async 2017-12-19 13:42:48 +01:00
casperlamboo
a4d8e255cc mesh now slices geometry again
No need for doodle3d-core import
2017-12-19 12:38:58 +01:00
casperlamboo
7b57d5c7b0 version bumb 2017-12-18 16:38:00 +01:00
casperlamboo
b85781620e Slicer now slices d3sketch files instead of stl's
Easier to differentiate between open and closed shapes
2017-12-18 16:37:03 +01:00
casperlamboo
db0d82c396 remove save code 2017-12-14 15:19:09 +01:00
casperlamboo
6c02343da3 update css 2017-12-14 11:55:14 +01:00
49 changed files with 21542 additions and 6532 deletions

View File

@ -18,11 +18,9 @@
}
},
"plugins": [
"babel-plugin-transform-regenerator",
"babel-plugin-transform-object-rest-spread",
"babel-plugin-inline-import",
"babel-plugin-transform-class-properties",
"babel-plugin-transform-es2015-classes",
"babel-plugin-syntax-dynamic-import"
"transform-class-properties",
"transform-object-rest-spread",
"transform-runtime",
"transform-es2015-classes"
]
}

33
.eslintrc Normal file
View File

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

136
DOCS.md Normal file
View 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

View File

@ -9,7 +9,7 @@ import * as THREE from 'three';
import { defaultSettings, sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
const settings = {
...defaultSettings.base,
...defaultSettings.default,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high
@ -27,7 +27,7 @@ const gcode = await sliceGeometry(settings, geometry);
import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer';
const settings = {
...defaultSettings.base,
...defaultSettings.default,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high

97
comb.js Normal file
View File

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

BIN
data/bunny.stl Normal file

Binary file not shown.

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,30 +1,47 @@
import 'babel-polyfill';
import React from 'react';
import { JSONLoader } from 'three/src/loaders/JSONLoader.js';
import { Interface } from 'doodle3d-slicer';
import fileURL from '!url-loader!./models/shape.json';
import { Interface } from './src/index.js';
import { render } from 'react-dom';
import fileSaver from 'file-saver';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import injectTapEventPlugin from 'react-tap-event-plugin';
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';
injectTapEventPlugin();
const muiTheme = getMuiTheme({
palette: {
primary1Color: blue500,
primary2Color: blue700,
accent1Color: blue500,
}
});
document.body.style.margin = 0;
document.body.style.padding = 0;
document.body.style.height = '100%';
document.documentElement.style.height = '100%'
document.getElementById('app').style.height = '100%';
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();
const downloadGCode = ({ gcode: { gcode } }) => {
const file = new File([gcode], 'gcode.gcode', { type: 'text/plain' });
fileSaver.saveAs(file);
};
const jsonLoader = new JSONLoader();
jsonLoader.load(fileURL, geometry => {
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>
<Interface geometry={geometry} name="Doodle3D"/>
<MuiThemeProvider muiTheme={muiTheme}>
<Interface
mesh={mesh}
onSliceSucces={({ gcode }) => fileSaver.saveAs(gcode, 'bunny.gcode')}
/>
</MuiThemeProvider>
), document.getElementById('app'));
});

1
models/Doodle.d3sketch Normal file
View 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
View 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"}

24780
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,17 @@
{
"name": "@doodle3d/doodle3d-slicer",
"version": "0.0.13",
"description": "JavaScript gcode slicer, Intended to use with the Doodle3D WiFi-Box # Usage",
"version": "0.0.18",
"description": "JavaScript gcode slicer for Doodle3D Transform",
"main": "lib/index.js",
"module": "module/index.js",
"esnext": "src/index.js",
"scripts": {
"start": "webpack-dev-server -w",
"dist": "NODE_ENV=production webpack -p",
"lint": "eslint src",
"prepare": "npm run build",
"upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print",
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p",
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
"build:main": "BABEL_ENV=main babel src -s -d lib",
"build:module": "BABEL_ENV=module babel src -s -d module",
@ -15,41 +19,50 @@
"build:module:settings": "cp -r src/settings module"
},
"dependencies": {
"@doodle3d/clipper-js": "^1.0.7",
"@doodle3d/clipper-js": "^1.0.10",
"lodash": "^4.17.4",
"material-ui": "^0.19.4",
"material-ui-icons": "^1.0.0-beta.17",
"material-ui-textfield-icon": "^0.2.2-1",
"proptypes": "^1.1.0",
"react": "^16.0.0",
"react-addons-update": "^15.6.2",
"react-dom": "^16.0.0",
"react-jss": "^7.2.0",
"react-resize-detector": "^1.1.0",
"three": "^0.88.0"
"shortid": "^2.2.8",
"three": "^0.88.0",
"validate-ip": "^1.0.1"
},
"devDependencies": {
"raw-loader": "^0.5.1",
"babel-plugin-inline-import": "^2.0.6",
"babel-preset-stage-0": "^6.24.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"file-saver": "^1.3.3",
"babel-cli": "6.24.1",
"babel-eslint": "^5.0.4",
"babel-loader": "7.0.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-cli": "6.24.1",
"babel-core": "6.24.1",
"babel-loader": "7.0.0",
"babel-plugin-add-module-exports": "0.2.1",
"babel-preset-es2015": "6.24.1",
"babel-polyfill": "^6.23.0",
"file-saver": "^1.3.3",
"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",
"react-tap-event-plugin": "^3.0.2",
"url-loader": "^0.5.9",
"babel-plugin-transform-runtime": "^6.23.0",
"html-webpack-plugin": "^2.29.0",
"webpack": "^3.3.0",
"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"
@ -58,9 +71,9 @@
"type": "git",
"url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git"
},
"author": "",
"license": "UNLICENSED",
"private": true,
"author": "Casper @Doodle3D",
"license": "MIT",
"private": false,
"bugs": {
"url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues"
},

View File

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

View File

@ -1,15 +1,17 @@
import { sliceGeometry, sliceMesh } from './slicer.js';
import Interface from './interface/index.js';
import baseSettings from './settings/default.yml';
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';
const defaultSettings = {
base: baseSettings,
default: _defaultSettings,
printer: printerSettings,
material: materialSettings,
quality: qualitySettings
quality: qualitySettings,
infill: infillSettings
};
export {

View 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);

View File

@ -1,39 +1,104 @@
import React from 'react';
import PropTypes from 'proptypes';
import _ from 'lodash';
import injectSheet from 'react-jss';
import MaterialUISelectField from 'material-ui/SelectField'
import MaterialUISelectField from 'material-ui/SelectField';
import MaterialUICheckbox from 'material-ui/Checkbox';
import MaterialUITextField from 'material-ui/TextField';
import TextFieldIcon from 'material-ui-textfield-icon';
import RefreshIcon from 'material-ui-icons/Refresh';
import muiThemeable from 'material-ui/styles/muiThemeable';
const contextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
export const contextTypes = {
settings: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
addPrinter: PropTypes.object.isRequired,
managePrinter: PropTypes.object.isRequired,
advancedFields: PropTypes.array.isRequired,
activePrinter: PropTypes.string
};
const propTypes = {
name: PropTypes.string.isRequired,
muiTheme: PropTypes.object.isRequired
};
export const SelectField = (props, context) => (
export const _SelectField = ({ name, muiTheme, ...props }, context) => (
<MaterialUISelectField
{ ...props }
{...props}
disabled={context.disabled}
value={_.get(context.state, props.name)}
onChange={(event, index, value) => context.onChange(props.name, value)}
value={_.get(context, name)}
onChange={(event, index, value) => context.onChange(name, value)}
/>
);
SelectField.contextTypes = contextTypes;
_SelectField.contextTypes = contextTypes;
_SelectField.propTypes = propTypes;
export const SelectField = muiThemeable()(_SelectField);
export const TextField = (props, context) => (
<MaterialUITextField
{ ...props }
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.state, props.name)}
onChange={(event, value) => context.onChange(props.name, value)}
value={_.get(context, name)}
onChange={(event, value) => context.onChange(name, value)}
/>
);
TextField.contextTypes = contextTypes;
_TextField.contextTypes = contextTypes;
_TextField.propTypes = propTypes;
export const TextField = muiThemeable()(_TextField);
export const Checkbox = (props, context) => (
<MaterialUICheckbox
{ ...props }
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}
checked={_.get(context.state, props.name)}
onCheck={(event, value) => context.onChange(props.name, value)}
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);
}}
/>
);
Checkbox.contextTypes = contextTypes;
_NumberField.contextTypes = contextTypes;
_NumberField.propTypes = propTypes;
export const NumberField = muiThemeable()(_NumberField);
const _Checkbox = ({ name, muiTheme: { palette }, ...props }, context) => (
<span style={{ display: 'flex', position: 'relative' }}>
<MaterialUICheckbox
{...props}
style={{ display: 'block' }}
iconStyle={{
fill: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
checked={_.get(context, name)}
onCheck={(event, value) => context.onChange(name, value)}
/>
{context.advancedFields.includes(name) && <RefreshIcon
onClick={() => context.onChange(name, null)}
/>}
</span>
);
_Checkbox.contextTypes = contextTypes;
_Checkbox.propTypes = propTypes;
export const Checkbox = muiThemeable()(_Checkbox);

View File

@ -4,97 +4,411 @@ import _ from 'lodash';
import { Tabs, Tab } from 'material-ui/Tabs';
import MenuItem from 'material-ui/MenuItem';
import injectSheet from 'react-jss';
import { SelectField, TextField, Checkbox } from './FormComponents.js';
import { grey800, cyan500 } from 'material-ui/styles/colors';
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'
},
text: {
fontWeight: 'bold'
display: 'flex',
alignItems: 'center'
},
container: {
width: '100%',
flexGrow: 1,
overflowY: 'auto'
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 childContextTypes = { state: PropTypes.object, onChange: PropTypes.func, disabled: PropTypes.bool };
static propTypes = {
selectedPrinter: PropTypes.string,
classes: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func,
disabled: PropTypes.bool.isRequired
};
static defaultProps: {
disabled: false
};
static propTypes = {
classes: PropTypes.objectOf(PropTypes.string),
onChange: PropTypes.func,
printers: PropTypes.object.isRequired,
defaultPrinter: PropTypes.string,
quality: PropTypes.object.isRequired,
defaultQuality: PropTypes.string.isRequired,
material: PropTypes.object.isRequired,
defaultMaterial: PropTypes.string.isRequired,
initialSettings: PropTypes.object.isRequired,
disabled: PropTypes.bool.isRequired
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
};
constructor(props) {
super();
this.state = {
settings: props.initialSettings,
printers: props.defaultPrinter,
quality: props.defaultQuality,
material: props.defaultMaterial
};
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);
let state;
switch (fieldName) {
case 'printers':
case 'quality':
case 'material':
state = {
[fieldName]: value,
settings: _.merge({}, this.state.settings, this.props[fieldName][value])
};
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:
state = _.set(_.cloneDeep(this.state), fieldName, value);
break;
}
if (onChange) onChange(state);
if (state) this.setState(state);
};
this.setState(state);
if (localStorage.active) {
if (onChange) onChange(this.constructSettings(state.localStorage));
updateLocalStorage(state.localStorage);
}
return state;
}
getChildContext() {
return { state: this.state, onChange: this.changeSettings, disabled: this.props.disabled };
const { localStorage, addPrinter, managePrinter } = this.state;
return {
addPrinter,
managePrinter,
activePrinter: localStorage.active,
advancedFields: localStorage.active ? Object.keys(localStorage.printers[localStorage.active].settings.advanced) : [],
settings: this.constructSettings(localStorage),
onChange: this.changeSettings,
disabled: this.props.disabled
};
}
constructSettings(localStorage) {
if (!localStorage.active) return defaultSettings;
const { ip, settings: { printer, material, quality, infill, advanced } } = localStorage.printers[localStorage.active];
let settings = {
...defaultSettings,
printer,
material,
quality,
infill,
ip
};
settings = _.merge({}, settings, printerSettings[printer]);
settings = _.merge({}, settings, qualitySettings[quality]);
settings = _.merge({}, settings, infillSettings[infill]);
settings = _.merge({}, settings, materialSettings[material]);
for (const key in advanced) {
const value = advanced[key];
settings = _.set(_.cloneDeep(settings), key.replace('settings.', ''), value);
}
return settings;
}
addPrinter = () => {
const { name, printer, ip } = this.state.addPrinter;
if (!name || !printer) {
this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a name and printer' } } }));
return;
}
if (printer === 'doodle3d_printer' && ip !== '' && !validateIp(ip)) {
this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a valid IP adress' } } }));
return;
}
const id = shortid.generate();
const localStorage = {
active: id,
printers: {
...this.state.localStorage.printers,
[id]: { name, ip, settings: { printer, material: 'pla', infill: '20pct', quality: 'medium', advanced: {} } }
}
};
this.setState({ localStorage });
updateLocalStorage(localStorage);
this.closeAddPrinterDialog();
const { onChange } = this.props;
if (onChange) onChange(this.constructSettings(localStorage));
};
editPrinter = () => {
const { localStorage: { active }, 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 { classes, printers, quality, material, disabled } = this.props;
const { addPrinter, managePrinter, localStorage } = this.state;
const { classes } = this.props;
return (
<div className={classes.container}>
<SelectField name="printers" floatingLabelText="Printer" fullWidth>
{Object.entries(printers).map(([value, { title }]) => (
<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>
<SelectField name="material" floatingLabelText="Material" fullWidth>
{Object.entries(material).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
))}
</SelectField>
<h3 className={classes.text}>Printer Setup</h3>
<Tabs inkBarStyle={{ backgroundColor: cyan500 }}>
<h3>Print Setup</h3>
<Tabs>
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Basic">
<div>
<SelectField name="quality" floatingLabelText="Quality" fullWidth>
{Object.entries(quality).map(([value, { title }]) => (
<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>
@ -102,58 +416,153 @@ class Settings extends React.Component {
</Tab>
<Tab buttonStyle={{ color: grey800, backgroundColor: 'white' }} label="Advanced">
<div>
<p className={classes.text}>Printer dimensions</p>
<div className={classes.textFieldRow}>
<TextField name="settings.dimensions.x" fullWidth floatingLabelText="X" type="number" />
<TextField name="settings.dimensions.y" fullWidth floatingLabelText="Y" type="number" />
<TextField name="settings.dimensions.z" fullWidth floatingLabelText="Z" type="number" />
</div>
<p className={classes.text}>Nozzle</p>
<TextField name="settings.nozzleDiameter" fullWidth floatingLabelText="Diameter" type="number" />
<p className={classes.text}>Bed</p>
<TextField name="settings.bedTemperature" fullWidth floatingLabelText="Temperature" type="number" />
<Checkbox name="settings.heatedBed" label="Heated" />
<p className={classes.text}>Material</p>
<TextField name="settings.filamentThickness" fullWidth floatingLabelText="Thickness" type="number" />
<TextField name="settings.temperature" fullWidth floatingLabelText="Temperature" type="number" />
<p className={classes.text}>Thickness</p>
<TextField name="settings.thickness.top" fullWidth floatingLabelText="top" type="number" />
<TextField name="settings.thickness.bottom" fullWidth floatingLabelText="bottom" type="number" />
<TextField name="settings.thickness.shell" fullWidth floatingLabelText="shell" type="number" />
<p className={classes.text}>Retraction</p>
<Checkbox name="settings.retraction.enabled" label="Enabled" />
<TextField name="settings.retraction.amount" fullWidth floatingLabelText="Amount" type="number" />
<TextField name="settings.retraction.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.retraction.minDistance" fullWidth floatingLabelText="Min distance" type="number" />
<p className={classes.text}>Travel</p>
<TextField name="settings.travel.speed" fullWidth floatingLabelText="Speed" type="number" />
<Checkbox name="settings.combing" label="Combing" />
<p className={classes.text}>Inner shell</p>
<TextField name="settings.innerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.innerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<p className={classes.text}>Outer shell</p>
<TextField name="settings.outerShell.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.outerShell.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<p className={classes.text}>Inner infill</p>
<TextField name="settings.innerInfill.gridSize" fullWidth floatingLabelText="Grid size" type="number" />
<TextField name="settings.innerInfill.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.innerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<p className={classes.text}>Outer infill</p>
<TextField name="settings.outerInfill.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.outerInfill.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<p className={classes.text}>Brim</p>
<TextField name="settings.brim.offset" fullWidth floatingLabelText="Offset" type="number" />
<TextField name="settings.brim.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.brim.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<p className={classes.text}>First layer</p>
<TextField name="settings.firstLayer.speed" fullWidth floatingLabelText="Speed" type="number" />
<TextField name="settings.firstLayer.flowRate" fullWidth floatingLabelText="Flow rate" type="number" />
<Accordion elements={[{
title: 'Layer',
body: (<NumberField name="settings.layerHeight" min={0.05} max={3} fullWidth floatingLabelText="Height" />)
}, {
title: 'Thickness',
body: (<span>
<NumberField name="settings.thickness.top" min={0} fullWidth floatingLabelText="top" />
<NumberField name="settings.thickness.bottom" min={0} fullWidth floatingLabelText="bottom" />
<NumberField name="settings.thickness.shell" min={0} fullWidth floatingLabelText="shell" />
</span>)
}, {
title: 'Material',
body: (<span>
<NumberField name="settings.filamentThickness" min={0.1} max={10} fullWidth floatingLabelText="Thickness" />
<NumberField name="settings.temperature" min={100} max={400} fullWidth floatingLabelText="Temperature" />
</span>)
}, {
title: 'Bed',
body: (<span>
<NumberField name="settings.bedTemperature" min={30} max={150} fullWidth floatingLabelText="Temperature" />
<Checkbox name="settings.heatedBed" label="Heated" />
</span>)
}, {
title: 'Brim',
body: (<span>
<NumberField name="settings.brim.size" min={0} max={20} fullWidth floatingLabelText="Size" />
<NumberField name="settings.brim.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.brim.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Support',
body: (<span>
<Checkbox name="settings.support.enabled" label="Enabled" />
<NumberField name="settings.support.distanceY" min={0.1} fullWidth floatingLabelText="Distance Y" />
<NumberField name="settings.support.density" min={0} max={100} fullWidth floatingLabelText="Density" />
<NumberField name="settings.support.margin" min={0.1} fullWidth floatingLabelText="Margin" />
<NumberField name="settings.support.minArea" min={1} fullWidth floatingLabelText="Min Area" />
<NumberField name="settings.support.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.support.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'First layer',
body: (<span>
<NumberField name="settings.firstLayer.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.firstLayer.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Inner shell',
body: (<span>
<NumberField name="settings.innerShell.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.innerShell.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Outer shell',
body: (<span>
<NumberField name="settings.outerShell.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.outerShell.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Inner infill',
body: (<span>
<NumberField name="settings.innerInfill.density" min={0} max={100} fullWidth floatingLabelText="Density" />
<NumberField name="settings.innerInfill.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.innerInfill.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Outer infill',
body: (<span>
<NumberField name="settings.outerInfill.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.outerInfill.flowRate" min={0.1} max={4} fullWidth floatingLabelText="Flow rate" />
</span>)
}, {
title: 'Travel',
body: (<span>
<NumberField name="settings.travel.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<Checkbox name="settings.combing" label="Combing" />
</span>)
}, {
title: 'Retraction',
body: (<span>
<Checkbox name="settings.retraction.enabled" label="Enabled" />
<NumberField name="settings.retraction.amount" min={0} max={10} fullWidth floatingLabelText="Amount" />
<NumberField name="settings.retraction.speed" min={10} max={200} fullWidth floatingLabelText="Speed" />
<NumberField name="settings.retraction.minDistance" min={0} fullWidth floatingLabelText="Min distance" />
</span>)
}, {
title: 'Printer dimensions',
body: (<span>
<div className={classes.textFieldRow}>
<NumberField name="settings.dimensions.x" min={1} fullWidth floatingLabelText="X" />
<NumberField name="settings.dimensions.y" min={1} fullWidth floatingLabelText="Y" />
<NumberField name="settings.dimensions.z" min={1} fullWidth floatingLabelText="Z" />
</div>
</span>)
}, {
title: 'Nozzle',
body: (<span>
<NumberField name="settings.nozzleDiameter" min={0.1} max={5} fullWidth floatingLabelText="Diameter" />
</span>)
}]} />
</div>
</Tab>
</Tabs>
{printDialog(this.props, this.state, 'Add Printer', 'addPrinter', 'Add', addPrinter, localStorage.active && this.closeAddPrinterDialog, null, this.addPrinter)}
{printDialog(this.props, this.state, 'Manage Printer', 'managePrinter', 'Save', managePrinter, this.closeManagePrinterDialog, this.removeActivePrinter, this.editPrinter)}
</div>
);
}
}
function printDialog(props, state, title, form, submitText, data, closeDialog, removeActivePrinter, save) {
const { classes } = props;
return (
<Dialog
title={title}
open={data.open}
onRequestClose={closeDialog ? closeDialog : null}
contentStyle={{ maxWidth: '400px' }}
autoScrollBodyContent
actions={[
closeDialog && <FlatButton
label="Close"
onClick={closeDialog}
/>,
removeActivePrinter && <FlatButton
label="Remove Printer"
onClick={removeActivePrinter}
/>,
<RaisedButton
label={submitText}
primary
onClick={save}
/>
]}
>
<SelectField name={`${form}.printer`} floatingLabelText="Printer" fullWidth>
{Object.entries(printerSettings).map(([value, { title }]) => (
<MenuItem key={value} value={value} primaryText={title} />
))}
</SelectField>
{data.error && <p className={classes.error}>{data.error}</p>}
</Dialog>
);
}
printDialog.propTypes = {
classes: PropTypes.objectOf(PropTypes.string)
};
export default injectSheet(styles)(Settings);

View File

@ -1,22 +1,19 @@
import _ from 'lodash';
import * as THREE from 'three';
import React from 'react';
import { Quaternion } from 'three/src/math/Quaternion.js';
import { Vector3 } from 'three/src/math/Vector3.js';
import PropTypes from 'proptypes';
import { placeOnGround, createScene, fetchProgress, slice, TabTemplate } from './utils.js';
import { centerGeometry, placeOnGround, createScene, slice, TabTemplate } from './utils.js';
import injectSheet from 'react-jss';
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton';
import Slider from 'material-ui/Slider';
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 baseSettings from '../settings/default.yml';
import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
import qualitySettings from '../settings/quality.yml';
import ReactResizeDetector from 'react-resize-detector';
import muiThemeable from 'material-ui/styles/muiThemeable';
import logo from '../../img/logo.png';
const MAX_FULLSCREEN_WIDTH = 720;
@ -45,14 +42,22 @@ const styles = {
settingsBar: {
display: 'flex',
flexDirection: 'column',
maxWidth: '380px',
maxWidth: '320px',
boxSizing: 'border-box',
padding: '10px',
padding: '10px 20px',
backgroundColor: 'white',
overflowY: 'auto',
borderLeft: `1px solid ${grey300}`
},
sliceActions: {
flexShrink: 0,
flexShrink: 0
},
sliceInfo: {
margin: '10px 0',
'& p': {
marginBottom: '5px',
fontSize: '11px'
}
},
sliceButtons: {
justifyContent: 'flex-end',
@ -62,7 +67,7 @@ const styles = {
margin: '5px 0 5px 5px'
},
controlButton: {
marginRight: '2px'
marginRight: '5px'
},
buttonContainer: {
width: '100%',
@ -72,80 +77,94 @@ const styles = {
color: red500
},
title: {
position: 'absolute'
userSelect: 'none',
position: 'absolute',
left: '10px'
},
detail: {
userSelect: 'none',
marginTop: '10px',
marginBottom: '10px'
},
logo: {
position: 'absolute',
left: '20px',
top: '20px',
width: '150px',
height: '51px'
}
};
class Interface extends React.Component {
static propTypes = {
geometry(props, propName) {
if (!(props[propName].isGeometry || props[propName].isBufferGeometry)) {
throw new Error('invalid prop, is not geometry');
}
},
selectedPrinter: PropTypes.string,
mesh: PropTypes.shape({ isMesh: PropTypes.oneOf([true]) }),
classes: PropTypes.objectOf(PropTypes.string),
defaultSettings: PropTypes.object.isRequired,
printers: PropTypes.object.isRequired,
defaultPrinter: PropTypes.string,
quality: PropTypes.object.isRequired,
defaultQuality: PropTypes.string.isRequired,
material: PropTypes.object.isRequired,
defaultMaterial: PropTypes.string.isRequired,
pixelRatio: PropTypes.number.isRequired,
onCancel: PropTypes.func,
name: PropTypes.string.isRequired
onSliceSucces: PropTypes.func.isRequired,
muiTheme: PropTypes.object.isRequired
};
static defaultProps = {
defaultSettings: baseSettings,
printers: printerSettings,
quality: qualitySettings,
defaultQuality: 'medium',
material: materialSettings,
defaultMaterial: 'pla',
pixelRatio: 1,
name: 'Doodle3D'
pixelRatio: 1
};
constructor(props) {
super(props);
const { defaultPrinter, defaultQuality, defaultMaterial, printers, quality, material, defaultSettings } = props;
this.canvasElement = React.createRef();
const scene = createScene(this.props);
this.state = {
controlMode: 'translate',
showFullScreen: false,
scene,
settings: null,
showFullScreen: window.innerWidth > MAX_FULLSCREEN_WIDTH,
isSlicing: false,
error: null,
printers: defaultPrinter,
quality: defaultQuality,
material: defaultMaterial,
settings: _.merge(
{},
defaultSettings,
printers[defaultPrinter],
quality[defaultQuality],
material[defaultMaterial]
)
mesh: null,
objectDimensions: '0x0x0mm',
popover: { open: false, element: null }
};
}
componentDidMount() {
const { canvas } = this.refs;
const scene = createScene(canvas, this.props, this.state);
this.setState({ ...scene });
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() {
if (this.state.editorControls) this.state.editorControls.dispose();
const { scene: { editorControls, mesh: { material }, renderer } } = this.state;
editorControls.dispose();
material.dispose();
renderer.dispose();
}
resetMesh = () => {
const { mesh, render } = this.state;
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();
}
};
@ -153,73 +172,92 @@ class Interface extends React.Component {
scaleUp = () => this.scaleMesh(0.9);
scaleDown = () => this.scaleMesh(1.0 / 0.9);
scaleMesh = (factor) => {
const { mesh, render } = this.state;
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 Vector3(0, 0, 1), Math.PI / 2.0);
rotateY = () => this.rotate(new Vector3(1, 0, 0), Math.PI / 2.0);
rotateZ = () => this.rotate(new Vector3(0, 1, 0), Math.PI / 2.0);
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 { mesh, render } = this.state;
const { scene: { mesh, render }, isSlicing } = this.state;
if (isSlicing) return;
if (mesh) {
mesh.rotateOnWorldAxis(axis, angle);
placeOnGround(mesh);
this.calculateDimensions();
render();
}
};
slice = async () => {
const { mesh, settings, isSlicing, printers, quality, material } = this.state;
const { name } = this.props;
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.setState({ isSlicing: true, progress: { action: '', slicing: 0, uploading: 0 }, error: null });
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 {
await slice(name, mesh, settings, printers, quality, material, progress => {
this.setState({ progress: { ...this.state.progress, ...progress } });
});
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 });
}
this.setState({ isSlicing: false });
};
onChangeSettings = (settings) => {
this.setState(settings);
};
openPopover = (event) => {
event.preventDefault();
componentWillUpdate(nextProps, nextState) {
const { box, render, setSize } = this.state;
let changed = false;
if (box && nextState.settings.dimensions !== this.state.settings.dimensions) {
const { dimensions } = nextState.settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x);
box.updateMatrix();
changed = true;
}
if (changed) render();
}
this.setState({
popover: {
element: event.currentTarget,
open: true
}
});
};
closePopover = () => {
this.setState({
popover: {
element: null,
open: false
}
});
};
componentDidUpdate() {
const { updateCanvas } = this.state;
const { canvas } = this.refs;
if (updateCanvas && canvas) updateCanvas(canvas);
const { scene: { updateCanvas } } = this.state;
if (updateCanvas && this.canvasElement.current) updateCanvas(this.canvasElement.current);
}
onResize3dView = (width, height) => {
window.requestAnimationFrame(() => {
const { setSize } = this.state;
const { scene: { setSize } } = this.state;
const { pixelRatio } = this.props;
setSize(width, height, pixelRatio);
if (setSize) setSize(width, height, pixelRatio);
});
};
@ -227,41 +265,58 @@ class Interface extends React.Component {
this.setState({ showFullScreen: width > MAX_FULLSCREEN_WIDTH });
};
render() {
const { classes, defaultPrinter, defaultQuality, defaultMaterial, onCancel } = this.props;
const { isSlicing, progress, settings, printers, quality, material, showFullScreen, error } = this.state;
onChangeSettings = (settings) => {
const { scene: { box, render } } = this.state;
const percentage = progress ? (progress.uploading + progress.slicing) / 2.0 * 100.0 : 0.0;
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={{ ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) }}>
<div className={classes.settingsBar} style={style}>
<Settings
selectedPrinter={selectedPrinter}
disabled={isSlicing}
printers={printerSettings}
defaultPrinter={defaultPrinter}
quality={qualitySettings}
defaultQuality={defaultQuality}
material={materialSettings}
defaultMaterial={defaultMaterial}
initialSettings={settings}
onChange={this.onChangeSettings}
/>
<div className={classes.sliceActions}>
{error && <p className={classes.error}>{error}</p>}
{isSlicing && <p>{progress.action}</p>}
{isSlicing && <LinearProgress mode="determinate" value={percentage} />}
<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="Cancel"
label="Close"
className={`${classes.button}`}
onTouchTap={onCancel}
onClick={onCancel}
/>}
<RaisedButton
label="Print"
label="Download GCODE"
ref="button"
primary
className={`${classes.button}`}
onTouchTap={this.slice}
disabled={isSlicing}
onClick={() => this.slice()}
/>
</div>
</div>
@ -271,15 +326,18 @@ class Interface extends React.Component {
const d3Panel = (
<div className={classes.d3View}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize3dView} />
<canvas className={classes.canvas} ref="canvas" />
{!isSlicing && <div className={classes.controlBar}>
<RaisedButton className={classes.controlButton} onTouchTap={this.resetMesh} label="reset" />
<RaisedButton className={classes.controlButton} onTouchTap={this.scaleUp} label="scale down" />
<RaisedButton className={classes.controlButton} onTouchTap={this.scaleDown} label="scale up" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateX} label="rotate x" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateY} label="rotate y" />
<RaisedButton className={classes.controlButton} onTouchTap={this.rotateZ} label="rotate z" />
</div>}
<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>
);
@ -287,7 +345,7 @@ class Interface extends React.Component {
return (
<div className={classes.container}>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResizeContainer} />
<h1 className={classes.title}>Print</h1>
<img src={logo} className={classes.logo} />
{d3Panel}
{settingsPanel}
</div>
@ -316,4 +374,4 @@ class Interface extends React.Component {
}
}
export default injectSheet(styles)(Interface);
export default muiThemeable()(injectSheet(styles)(Interface));

View File

@ -1,16 +1,4 @@
import * as THREE from 'three';
import { Box3 } from 'three/src/math/Box3.js';
import { Matrix4 } from 'three/src/math/Matrix4.js';
import { Scene } from 'three/src/scenes/Scene.js';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
import { DirectionalLight } from 'three/src/lights/DirectionalLight.js';
import { MeshPhongMaterial } from 'three/src/materials/MeshPhongMaterial.js';
import { BoxGeometry } from 'three/src/geometries/BoxGeometry.js';
import { Mesh } from 'three/src/objects/Mesh.js';
import { BoxHelper } from 'three/src/helpers/BoxHelper.js';
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
import { DoubleSide } from 'three/src/constants.js';
import 'three/examples/js/controls/EditorControls';
import printerSettings from '../settings/printer.yml';
import materialSettings from '../settings/material.yml';
@ -20,47 +8,48 @@ import React from 'react';
import PropTypes from 'prop-types';
export function placeOnGround(mesh) {
const boundingBox = new Box3().setFromObject(mesh);
const boundingBox = new THREE.Box3().setFromObject(mesh);
mesh.position.y -= boundingBox.min.y;
mesh.updateMatrix();
}
export function createScene(canvas, props, state) {
const { geometry, pixelRatio } = props;
const { controlMode, settings } = state;
export function centerGeometry(mesh) {
// center geometry
geometry.computeBoundingBox();
const center = geometry.boundingBox.getCenter();
geometry.applyMatrix(new Matrix4().makeTranslation(-center.x, -center.y, -center.z));
mesh.geometry.computeBoundingBox();
const center = mesh.geometry.boundingBox.getCenter();
mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
}
const scene = new Scene();
export function createScene({ muiTheme }) {
const scene = new THREE.Scene();
const camera = new PerspectiveCamera(50, 1, 1, 10000);
const camera = new THREE.PerspectiveCamera(50, 1, 1, 10000);
camera.position.set(0, 400, 300);
camera.lookAt(new THREE.Vector3(0, 0, 0));
const directionalLightA = new DirectionalLight(0xa2a2a2);
const directionalLightA = new THREE.DirectionalLight(0xa2a2a2);
directionalLightA.position.set(1, 1, 1);
scene.add(directionalLightA);
const directionalLightB = new DirectionalLight(0xa2a2a2);
const directionalLightB = new THREE.DirectionalLight(0xa2a2a2);
directionalLightB.position.set(-1, 1, -1);
scene.add(directionalLightB);
const light = new AmbientLight(0x656565);
const light = new THREE.AmbientLight(0x656565);
scene.add(light);
const material = new MeshPhongMaterial({ color: 0x2194ce, side: DoubleSide, specular: 0xc5c5c5, shininess: 5 });
const mesh = new Mesh(geometry, material);
placeOnGround(mesh);
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 BoxHelper(new Mesh(new BoxGeometry(1, 1, 1).applyMatrix(new Matrix4().makeTranslation(0, 0.5, 0))), 0x72bcd4);
const box = new THREE.BoxHelper(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1).applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5, 0))), muiTheme.palette.primary2Color);
scene.add(box);
const { dimensions } = settings;
box.scale.set(dimensions.y, dimensions.z, dimensions.x);
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);
@ -73,120 +62,72 @@ export function createScene(canvas, props, state) {
render();
};
let editorControls;
let renderer;
const updateCanvas = (canvas) => {
if (!renderer || renderer.domElement !== canvas) {
if (renderer) renderer.dispose();
renderer = new WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setClearColor(0xffffff, 0);
}
if (!editorControls || editorControls.domElement !== canvas) {
if (editorControls) editorControls.dispose();
editorControls = new THREE.EditorControls(camera, canvas);
editorControls.focus(mesh);
editorControls.addEventListener('change', render);
}
render();
};
updateCanvas(canvas);
const focus = () => editorControls.focus(mesh);
return { editorControls, scene, mesh, camera, renderer, render, box, setSize, updateCanvas, focus };
}
export function fetchProgress(url, { method = 'get', headers = {}, body = {} } = {}, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
if (headers) {
for (const key in headers) {
const header = headers[key];
xhr.setRequestHeader(key, header);
}
}
xhr.onload = event => resolve(event.target.responseText);
xhr.onerror = reject;
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress;
xhr.send(body);
});
export function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com';
const CONNECT_URL = 'http://connect.doodle3d.com/';
export async function slice(name, mesh, settings, printers, quality, material, updateProgress) {
if (!printers) throw new Error('Please select a printer');
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 geometry = mesh.geometry.clone();
mesh.updateMatrix();
const matrix = new THREE.Matrix4().makeTranslation(centerY, 0, centerX)
.multiply(new THREE.Matrix4().makeRotationY(-Math.PI / 2.0))
.multiply(mesh.matrix);
const matrix = new Matrix4().makeTranslation(centerY, 0, centerX).multiply(mesh.matrix);
const { gcode } = await sliceGeometry(settings, geometry, matrix, false, false, ({ progress }) => {
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,
slicing: progress.done / progress.total
percentage: (currentStep + progress.done / progress.total) / steps
});
}).catch(error => {
throw { message: `error during slicing: ${error.message}`, code: 2 };
});
currentStep ++;
// upload G-code file to AWS S3
const { data: { reservation, id } } = await fetch(`${GCODE_SERVER_URL}/upload`, { method: 'POST' })
.then(response => response.json());
const body = new FormData();
const { fields } = reservation;
for (const key in fields) {
body.append(key, fields[key]);
}
const file = ';' + JSON.stringify({
name: `${name}.gcode`,
...settings,
printer: {
type: printers,
title: printerSettings[printers].title
},
material: {
type: material,
title: materialSettings[material].title
},
quality: {
type: quality,
title: qualitySettings[quality].title
}
}).trim() + '\n' + gcode;
body.append('file', file);
await fetchProgress(reservation.url, { method: 'POST', body }, (progess) => {
updateProgress({
action: 'Uploading',
uploading: progess.loaded / progess.total
});
});
const popup = window.open(`${CONNECT_URL}?uuid=${id}`, '_blank');
if (!popup) throw new Error('popup was blocked by browser');
return sliceResult;
}
const styles = {
width: '100%',
position: 'relative',
textAlign: 'initial',
};
export const TabTemplate = ({children, selected, style}) => {
const templateStyle = Object.assign({}, styles, style);
if (!selected) {
templateStyle.height = 0;
templateStyle.width = 0;
templateStyle.overflow = 'hidden';
}
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}>
@ -198,5 +139,5 @@ export const TabTemplate = ({children, selected, style}) => {
TabTemplate.propTypes = {
children: PropTypes.node,
selected: PropTypes.bool,
style: PropTypes.object,
style: PropTypes.object
};

View File

@ -1,4 +1,30 @@
zOffset: 0.3
startCode: |-
M109 S{temperature} ;set target temperature
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
G21 ;metric values
M107 ;start with the fan off
G28 X0 Y0 ;move X/Y to min endstops
G28 Z0 ;move Z to min endstops
G1 Z15 F9000 ;move the platform down 15mm
G92 E0 ;zero the extruded length
G91 ;relative positioning
G1 F200 E10 ;extrude 10mm of feed stock
G92 E0 ;zero the extruded length again
G92 E0 ;zero the extruded length again
G1 F9000
G90 ;absolute positioning
M117 Printing Doodle...
endCode: |-
M107 ;fan off
G91 ;relative positioning
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
G1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way
M84 ;disable axes / steppers
G90 ;absolute positioning
M104 S0
{if heatedBed}M140 S0
M117 Done
dimensions:
x: 200
y: 200
@ -7,9 +33,9 @@ heatedBed: false
nozzleDiameter: 0.4
filamentThickness: 2.85
temperature: 210
bedTemperature: 70
bedTemperature: 50
layerHeight: 0.15
combing: true
combing: false
thickness:
top: 0.45
bottom: 0.45
@ -23,11 +49,10 @@ travel:
speed: 200.0
support:
enabled: false
acceptanceMargin: 1.5
minArea: 2
distanceY: 0.4
gridSize: 6.0
density: 5.0
margin: 2.0
plateSize: 4.0
flowRate: 0.8
speed: 40.0
innerShell:
@ -39,12 +64,12 @@ outerShell:
innerInfill:
flowRate: 1.0
speed: 80.0
gridSize: 15.0
density: 20.0
outerInfill:
flowRate: 1.0
speed: 50.0
brim:
offset: 4.0
size: 8.0
flowRate: 1.0
speed: 40.0
firstLayer:

21
src/settings/infill.yml Normal file
View 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

View File

@ -267,11 +267,50 @@ ultimaker2_plus:
title: Ultimaker 2+
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 205
ultimaker2_plus_extended:
title: Ultimaker 2+ Extended
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 305
ultimaker2go:
startCode: |-
M10000
M10000
M10001 X8 Y28 SDoodle3D heat up...
M109 S{temperature} ;set target temperature
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
G21 ;metric values
G90 ;absolute positioning
M107 ;start with the fan off
G28 ; home to endstops
G1 Z15 F9000 ;move the platform down 15mm
G92 E0 ;zero the extruded length
G1 F200 E10 ;extrude 10mm of feed stock
G92 E0 ;zero the extruded length again
G1 F9000
M10000
M10000
M10001 X8 Y28 SDoodle3D printing...
endCode: |-
M10000
M10000
M10001 X20 Y28 SDoodle3D done!
M107 ;fan off
G91 ;relative positioning
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
G1 Z+5.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
G28 ;home the printer
M84 ;disable axes / steppers
G90 ;absolute positioning
M104 S0
{if heatedBed}M140 S0
title: Ultimaker 2 Go
heatedBed: false
filamentThickness: 2.85
@ -299,3 +338,30 @@ wanhao_duplicator_i3_plus:
title: Wanhao Duplicator i3 Plus
heatedBed: false
filamentThickness: 1.75
wanhao_duplicator_i3_mini:
duplicator_i3_mini:
startCode: |-
M104 S{temperature}
G28
M109 S{temperature}
G90
M82
G1 Z10.0 F6000
G92 E0
G1 F200 E3
G92 E0
endCode: |-
M104 S0
G92 E1
G1 E-1 F300
G28 X0 Y0
M84
M82
M104 S0
title: Wanhao Duplicator i3 Mini
heatedBed: false
filamentThickness: 1.75
dimensions:
x: 120
y: 135
z: 120

View File

@ -15,7 +15,7 @@ low:
speed: 70.0
innerInfill:
speed: 80.0
gridSize: 25.0
density: 10.0
medium:
title: "Medium"
layerHeight: .15
@ -33,7 +33,7 @@ medium:
speed: 40.0
innerInfill:
speed: 80.0
gridSize: 25.0
density: 10.0
high:
title: "High"
thickness:
@ -51,4 +51,4 @@ high:
speed: 30.0
innerInfill:
speed: 70.0
gridSize: 10.0
density: 20.0

View File

@ -1,24 +1,36 @@
import Shape from 'clipper-js';
import Shape from '@doodle3d/clipper-js';
import { PRECISION } from '../constants.js';
const offsetOptions = {
const OFFSET_OPTIONS = {
jointType: 'jtRound',
miterLimit: 2.0,
roundPrecision: 0.25
roundPrecision: 0.25,
endType: 'etClosedPolygon'
};
export default function addBrim(slices, settings) {
let {
brim: { offset: brimOffset }
brim: { size: brimSize },
nozzleDiameter
} = settings;
brimOffset /= PRECISION;
nozzleDiameter /= PRECISION;
brimSize /= PRECISION;
const nozzleRadius = nozzleDiameter / 2;
const [firstLayer] = slices;
firstLayer.brim = firstLayer.parts.reduce((brim, { shape }) => (
brim.join(shape.offset(brimOffset, {
...offsetOptions,
const brim = firstLayer.parts.reduce((_brim, { shape }) => (
_brim.join(shape.offset(nozzleRadius, {
...OFFSET_OPTIONS,
endType: shape.closed ? 'etClosedPolygon' : 'etOpenRound'
}))
), new Shape([], true)).simplify('pftNonZero');
firstLayer.brim = new Shape([], true);
for (let offset = 0; offset < brimSize; offset += nozzleDiameter) {
const brimPart = brim.offset(offset, OFFSET_OPTIONS);
firstLayer.brim = firstLayer.brim.join(brimPart);
}
}

View File

@ -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) {
for (let i = 0; i < shapes.length; i ++) {
const { fillShapes, lineShapesOpen, lineShapesClosed } = shapes[i];
export default function applyPrecision(layers) {
for (let layer = 0; layer < layers.length; layer ++) {
const { fillShapes, lineShapesOpen, lineShapesClosed } = layers[layer];
scaleUpShape(fillShapes);
scaleUpShape(lineShapesOpen);
@ -15,9 +16,7 @@ function scaleUpShape(shape) {
const path = shape[i];
for (let i = 0; i < path.length; i ++) {
const point = path[i];
point.copy(point.divideScalar(PRECISION));
path[i] = divide(path[i], PRECISION);
}
}
}

View File

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

View File

@ -1,49 +1,70 @@
import { Line3 } from 'three/src/math/Line3.js';
import { Vector2 } from 'three/src/math/Vector2.js';
import * as vector2 from './helpers/vector2.js';
import * as vector3 from './helpers/vector3.js';
function addLine(geometry, lineLookup, lines, a, b, isFlat) {
const index = lines.length;
lineLookup[`${a}_${b}`] = index;
lines.push({
line: new Line3(geometry.vertices[a], geometry.vertices[b]),
connects: [],
normals: [],
isFlat
});
return index;
}
export default function createLines(geometry, settings) {
export default function createLines(geometry) {
const faces = [];
const lines = [];
const lineLookup = {};
for (let i = 0; i < geometry.faces.length; i ++) {
const face = geometry.faces[i];
for (let i = 0; i < geometry.objectIndexes.length; i ++) {
const objectIndex = geometry.objectIndexes[i];
const { x: a, y: b, z: c } = getVertex(geometry.faces, i);
const normal = calculateNormal(geometry.vertices, a, b, c);
const lookupA = lineLookup[`${face.b}_${face.a}`];
const lookupB = lineLookup[`${face.c}_${face.b}`];
const lookupC = lineLookup[`${face.a}_${face.c}`];
// skip faces that point up or down
if (normal.y > 0.999 || normal.y < -0.999) {
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
// returns index of said line
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);
const flatNormal = vector2.normalize({ x: normal.z, y: normal.x });
const lineIndexes = [indexA, indexB, indexC];
// set connecting lines (based on face)
lines[lineIndexA].connects.push(lineIndexB, lineIndexC);
lines[lineIndexB].connects.push(lineIndexC, lineIndexA);
lines[lineIndexC].connects.push(lineIndexA, lineIndexB);
const normal = new Vector2(face.normal.z, face.normal.x).normalize();
lines[lineIndexA].normals.push(normal);
lines[lineIndexB].normals.push(normal);
lines[lineIndexC].normals.push(normal);
faces.push({ lineIndexes, flatNormal, objectIndex });
}
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]
};
}

View File

@ -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);
}

View File

@ -1,11 +1,10 @@
import { PRECISION } from '../constants.js'
import { PRECISION } from '../constants.js';
import getFillTemplate from './getFillTemplate.js';
import Shape from 'clipper-js';
export default function generateInfills(slices, settings) {
let {
layerHeight,
innerInfill: { gridSize: infillGridSize },
innerInfill: { density },
thickness: {
top: topThickness,
bottom: bottomThickness
@ -13,13 +12,16 @@ export default function generateInfills(slices, settings) {
nozzleDiameter
} = settings;
infillGridSize /= PRECISION;
density /= 100;
nozzleDiameter /= PRECISION;
const bottomSkinCount = Math.ceil(bottomThickness/layerHeight);
const topSkinCount = Math.ceil(topThickness/layerHeight);
const bidirectionalInfill = density < 0.8;
const infillGridSize = nozzleDiameter * (bidirectionalInfill ? 2 : 1) / density;
const bottomSkinCount = Math.ceil(bottomThickness / layerHeight);
const topSkinCount = Math.ceil(topThickness / layerHeight);
const nozzleRadius = nozzleDiameter / 2;
const outerFillTemplateSize = Math.sqrt(2 * Math.pow(nozzleDiameter, 2));
const outerFillTemplateSize = nozzleDiameter;
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
@ -32,6 +34,7 @@ export default function generateInfills(slices, settings) {
}
for (let i = 0; i < slice.parts.length; i ++) {
const even = (layer % 2 === 0);
const part = slice.parts[i];
if (!part.closed) continue;
@ -52,14 +55,13 @@ export default function generateInfills(slices, settings) {
if (innerFillArea && innerFillArea.paths.length > 0) {
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));
}
if (outerFillArea.paths.length > 0) {
const bounds = outerFillArea.shapeBounds();
const even = (layer % 2 === 0);
const outerFillTemplate = getFillTemplate(bounds, outerFillTemplateSize, even, !even);
part.outerFill.join(outerFillTemplate.intersect(outerFillArea));

View File

@ -1,6 +1,6 @@
import { PRECISION } from '../constants.js'
import { PRECISION } from '../constants.js';
const offsetOptions = {
const OFFSET_OPTIONS = {
jointType: 'jtSquare',
endType: 'etClosedPolygon',
miterLimit: 2.0,
@ -10,7 +10,6 @@ const offsetOptions = {
export default function generateInnerLines(slices, settings) {
// need to scale up everything because of clipper rounding errors
let {
layerHeight,
nozzleDiameter,
thickness: { shell: shellThickness }
} = settings;
@ -29,7 +28,7 @@ export default function generateInnerLines(slices, settings) {
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;
@ -39,7 +38,7 @@ export default function generateInnerLines(slices, settings) {
for (let inset = 1; inset < numShells; inset += 1) {
const offset = inset * nozzleDiameter;
const shell = outerLine.offset(-offset, offsetOptions);
const shell = outerLine.offset(-offset, OFFSET_OPTIONS);
if (shell.paths.length === 0) {
break;

View File

@ -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 ++) {
const slice = slices[layer];

View File

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

View File

@ -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 size = Math.sqrt(2 * Math.pow(gridSize, 2));
const left = Math.floor(bounds.left / size) * size;
const right = Math.ceil(bounds.right / size) * size;
const top = Math.floor(bounds.top / size) * size;

View File

@ -1,5 +1,5 @@
import { Vector2 } from 'three/src/math/Vector2.js';
import { PRECISION } from '../../constants.js';
import { distanceTo } from './vector2.js';
import { VERSION } from '../../constants.js';
export const MOVE = 'G';
export const M_COMMAND = 'M';
@ -10,13 +10,15 @@ export const POSITION_X = 'X';
export const POSITION_Y = 'Y';
export const POSITION_Z = 'Z';
export default class {
constructor(nozzleToFilamentRatio) {
this._nozzleToFilamentRatio = nozzleToFilamentRatio;
this._gcode = [];
export default class GCode {
constructor(settings) {
this._nozzleToFilamentRatio = 1;
this._gcode = [
`; ${JSON.stringify(settings)}`,
`; Generated with Doodle3D Slicer V${VERSION}`
];
this._currentValues = {};
this._nozzlePosition = new Vector2(0, 0);
this._nozzlePosition = { x: 0, y: 0 };
this._extruder = 0.0;
this._duration = 0.0;
this._isRetracted = false;
@ -27,10 +29,16 @@ export default class {
this._gcode.push(command);
}
updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness) {
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
const lineSurfaceArea = nozzleDiameter * layerHeight;
this._nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
}
turnFanOn(fanSpeed) {
this._isFanOn = true;
const gcode = { [M_COMMAND]: 106 }
const gcode = { [M_COMMAND]: 106 };
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
this._addGCode(gcode);
@ -47,41 +55,41 @@ export default class {
}
moveTo(x, y, z, { speed }) {
const newNozzlePosition = new Vector2(x, y).multiplyScalar(PRECISION);
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._duration += lineLength / speed;
this._addGCode({
[MOVE]: 0,
[POSITION_X]: newNozzlePosition.x.toFixed(3),
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
[POSITION_Z]: z.toFixed(3),
[SPEED]: (speed * 60).toFixed(3)
[POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z,
[SPEED]: speed * 60
});
this._nozzlePosition.copy(newNozzlePosition);
this._nozzlePosition = newNozzlePosition;
return this;
}
lineTo(x, y, z, { speed, flowRate }) {
const newNozzlePosition = new Vector2(x, y).multiplyScalar(PRECISION);
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition);
const newNozzlePosition = { x, y };
const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
this._duration += lineLength / speed;
this._addGCode({
[MOVE]: 1,
[POSITION_X]: newNozzlePosition.x.toFixed(3),
[POSITION_Y]: newNozzlePosition.y.toFixed(3),
[POSITION_Z]: z.toFixed(3),
[SPEED]: (speed * 60).toFixed(3),
[EXTRUDER]: this._extruder.toFixed(3)
[POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z,
[SPEED]: speed * 60,
[EXTRUDER]: this._extruder
});
this._nozzlePosition.copy(newNozzlePosition);
this._nozzlePosition = newNozzlePosition;
return this;
}
@ -95,8 +103,8 @@ export default class {
this._addGCode({
[MOVE]: 0,
[EXTRUDER]: this._extruder.toFixed(3),
[SPEED]: (speed * 60).toFixed(3)
[EXTRUDER]: this._extruder,
[SPEED]: speed * 60
});
}
}
@ -113,8 +121,8 @@ export default class {
this._addGCode({
[MOVE]: 0,
[EXTRUDER]: (this._extruder - amount).toFixed(3),
[SPEED]: (speed * 60).toFixed(3)
[EXTRUDER]: this._extruder - amount,
[SPEED]: speed * 60
});
}
}
@ -122,6 +130,15 @@ export default class {
return this;
}
addGCode(gcode, { temperature, bedTemperature, heatedBed }) {
gcode = gcode
.replace(/{temperature}/g, temperature)
.replace(/{if heatedBed}.*?\n/g, str => heatedBed ? str.replace(/{if heatedBed}/g, '') : '')
.replace(/{bedTemperature}/g, bedTemperature);
this._addGCode(gcode);
}
getGCode() {
return {
gcode: this._gcode,

View File

@ -1,6 +1,6 @@
import Shape from 'clipper-js';
import Shape from '@doodle3d/clipper-js';
export default class {
export default class Slice {
constructor() {
this.parts = [];
}

View File

@ -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
};
}

View 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;
}

View File

@ -1,131 +1,197 @@
import Shape from 'clipper-js';
import { subtract, add, scale, normalize, dot, length, distanceTo } from './VectorUtils.js';
import { PRECISION } from '../../constants.js';
import { angle, subtract, distanceTo } from './vector2.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) {
if (distanceTo(start, end) < TOLERANCE) {
return [start, end];
}
points = [...points, start, end];
graph = [...graph];
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 outlinePart = new Shape([outline.paths[i]], true, false, false, true);
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));
let result;
if (graph[startNode].some(node => node.to === endNode)) {
result = [start, end];
} 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);
}

View 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
};
};

View 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
};
};

View File

@ -1,120 +1,159 @@
import { Vector2 } from 'three/src/math/Vector2.js';
import Shape from 'clipper-js';
import { subtract, normal, normalize, dot, almostEquals } from './helpers/vector2.js';
export default function intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings) {
export default function intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes) {
const layers = [];
for (let layer = 1; layer < layerIntersectionIndexes.length; layer ++) {
const intersectionIndexes = layerIntersectionIndexes[layer];
const intersectionPoints = layerIntersectionPoints[layer];
if (intersectionIndexes.length === 0) continue;
for (let layer = 0; layer < layerPoints.length; layer ++) {
const fillShapes = [];
const lineShapesOpen = [];
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 { openGeometry } = lines[index];
let isFirstPoint = true;
let openShape = true;
const shapes = {};
while (index !== -1) {
const intersection = intersectionPoints[index];
// uppercase X and Y because clipper vector
shape.push(intersection);
const startConnects = {};
const endConnects = {};
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 faceNormals = lines[index].normals;
const a = lineIndexes[0];
const b = lineIndexes[1];
const c = lineIndexes[2];
for (let i = 0; i < connects.length; i ++) {
index = connects[i];
let pointA;
let pointB;
if (points[a] && points[b]) {
pointA = a;
pointB = b;
} else if (points[b] && points[c]) {
pointA = b;
pointB = c;
} else if (points[c] && points[a]) {
pointA = c;
pointB = a;
} else {
// should never happen
continue;
}
if (firstPoints.includes(index) && shape.length > 2) {
openShape = false;
index = -1;
break;
}
const segmentNormal = normalize(normal(subtract(points[pointA], points[pointB])));
if (dot(segmentNormal, flatNormal) < 0) {
const temp = pointB;
pointB = pointA;
pointA = temp;
}
// Check if index has an intersection or is already used
if (typeof intersectionPoints[index] !== 'undefined') {
const faceNormal = faceNormals[Math.floor(i / 2)];
const a = new Vector2(intersection.x, intersection.y);
const b = new Vector2(intersectionPoints[index].x, intersectionPoints[index].y);
// can't calculate normal between points if distance is smaller as 0.0001
if ((faceNormal.x === 0 && faceNormal.y === 0) || a.distanceTo(b) < 0.0001) {
if (isFirstPoint) {
firstPoints.push(index);
}
delete intersectionPoints[index];
connects.push(...lines[index].connects);
faceNormals.push(...lines[index].normals);
index = -1;
} else {
// make sure the path goes the right direction
// 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;
}
}
if (endConnects[pointA]) {
const lineSegment = endConnects[pointA];
delete endConnects[pointA];
if (startConnects[pointB]) {
if (startConnects[pointB] === lineSegment) {
delete startConnects[pointB];
lineSegment.push(pointB);
} 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 {
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 {
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);
}
}
}

View File

@ -1,15 +1,15 @@
import { Vector2 } from 'three/src/math/Vector2.js';
import Shape from 'clipper-js';
import { distanceTo } from './helpers/vector2.js';
import Shape from '@doodle3d/clipper-js';
export default function optimizePaths(slices, settings) {
const start = new Vector2(0, 0);
export default function optimizePaths(slices) {
let start = { x: 0, y: 0 };
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
if (typeof slice.brim !== 'undefined' && slice.brim.paths.length > 0) {
slice.brim = optimizeShape(slice.brim, start);
start.copy(slice.brim.lastPoint(true));
start = slice.brim.lastPoint(true);
}
const parts = [];
@ -54,42 +54,40 @@ export default function optimizePaths(slices, settings) {
if (shell.paths.length === 0) continue;
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) {
part.outerFill = optimizeShape(part.outerFill, start);
start.copy(part.outerFill.lastPoint(true));
start = part.outerFill.lastPoint(true);
}
if (part.innerFill.paths.length > 0) {
part.innerFill = optimizeShape(part.innerFill, start);
start.copy(part.innerFill.lastPoint(true));
start = part.innerFill.lastPoint(true);
}
} else {
part.shape = optimizeShape(part.shape, start);
start.copy(part.shape.lastPoint(true));
start = part.shape.lastPoint(true);
}
}
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);
start.copy(slice.support.lastPoint(true));
start = slice.support.lastPoint(true);
}
}
}
function optimizeShape(shape, start) {
start = start.clone();
const inputPaths = shape.mapToLower();
const inputPaths = shape.mapToLower().filter(path => path.length > 0);
const optimizedPaths = [];
const donePaths = [];
while (optimizedPaths.length !== inputPaths.length) {
let minLength = false;
let minLength = Infinity;
let reverse;
let minPath;
let offset;
@ -102,9 +100,8 @@ function optimizeShape(shape, start) {
if (shape.closed) {
for (let j = 0; j < path.length; j += 1) {
const point = new Vector2().copy(path[j]);
const length = point.sub(start).length();
if (minLength === false || length < minLength) {
const length = distanceTo(path[j], start);
if (length < minLength) {
minPath = path;
minLength = length;
offset = j;
@ -112,17 +109,15 @@ function optimizeShape(shape, start) {
}
}
} else {
const startPoint = new Vector2().copy(path[0]);
const lengthToStart = startPoint.sub(start).length();
if (minLength === false || lengthToStart < minLength) {
const lengthToStart = distanceTo(path[0], start);
if (lengthToStart < minLength) {
minPath = path;
minLength = lengthToStart;
reverse = false;
pathIndex = i;
}
const endPoint = new Vector2().copy(path[path.length - 1]);
const lengthToEnd = endPoint.sub(start).length();
const lengthToEnd = distanceTo(path[path.length - 1], start);
if (lengthToEnd < minLength) {
minPath = path;
minLength = lengthToEnd;
@ -132,20 +127,15 @@ function optimizeShape(shape, start) {
}
}
let point;
if (shape.closed) {
minPath = minPath.concat(minPath.splice(0, offset));
point = minPath[0];
start = minPath[0];
} else {
if (reverse) {
minPath.reverse();
}
point = minPath[minPath.length - 1];
if (reverse) minPath.reverse();
start = minPath[minPath.length - 1];
}
donePaths.push(pathIndex);
start.copy(point);
optimizedPaths.push(minPath);
}

View File

@ -1,9 +1,9 @@
import Shape from 'clipper-js';
import Shape from '@doodle3d/clipper-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 = [];
for (let layer = 0; layer < shapes.length; layer ++) {
@ -13,7 +13,8 @@ export default function shapesToSlices(shapes, settings) {
.fixOrientation()
.simplify('pftNonZero')
.clean(1)
.seperateShapes();
.thresholdArea(MIN_AREA / Math.pow(PRECISION, 2))
.separateShapes();
lineShapesClosed = new Shape(lineShapesClosed, true, true, true, true)
.clean(1);
@ -27,23 +28,16 @@ export default function shapesToSlices(shapes, settings) {
for (let i = 0; i < fillShapes.length; i ++) {
const fillShape = fillShapes[i];
if (fillShape.paths.length === 0) continue;
slice.add(fillShape, true);
// if (lineShapesClosed.paths.length > 0) {
// lineShapesClosed = lineShapesClosed.difference(closedShape);
// }
// if (lineShapesOpen.paths.length > 0) {
// lineShapesOpen = lineShapesOpen.difference(closedShape);
// }
if (lineShapesClosed.paths.length > 0) lineShapesClosed = lineShapesClosed.difference(fillShape);
if (lineShapesOpen.paths.length > 0) lineShapesOpen = lineShapesOpen.difference(fillShape);
}
if (lineShapesClosed.paths.length > 0) {
slice.add(lineShapesClosed, false);
}
if (lineShapesOpen.paths.length > 0) {
slice.add(lineShapesOpen, false);
}
if (lineShapesClosed.paths.length > 0) slice.add(lineShapesClosed, false);
if (lineShapesOpen.paths.length > 0) slice.add(lineShapesOpen, false);
sliceLayers.push(slice);
}

View File

@ -1,9 +1,3 @@
import { Color } from 'three/src/math/Color.js';
import { BufferGeometry } from 'three/src/core/BufferGeometry.js';
import { BufferAttribute } from 'three/src/core/BufferAttribute.js';
import { LineBasicMaterial } from 'three/src/materials/LineBasicMaterial.js';
import { VertexColors } from 'three/src/constants.js';
import { LineSegments } from 'three/src/objects/LineSegments.js';
import calculateLayersIntersections from './calculateLayersIntersections.js';
import createLines from './createLines.js';
import generateInfills from './generateInfills.js';
@ -15,43 +9,26 @@ import addBrim from './addBrim.js';
import optimizePaths from './optimizePaths.js';
import shapesToSlices from './shapesToSlices.js';
import slicesToGCode from './slicesToGCode.js';
import detectOpenClosed from './detectOpenClosed.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, constructLinePreview, onProgress) {
const totalStages = 12;
let current = -1;
const updateProgress = (action) => {
current ++;
if (typeof onProgress !== 'undefined') {
onProgress({
progress: {
done: current,
total: totalStages,
action
}
});
}
export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
const total = 11;
let done = -1;
const updateProgress = action => {
done ++;
if (onProgress) onProgress({ progress: { done, total, action } });
};
geometry.computeFaceNormals();
// get unique lines from geometry;
updateProgress('Constructing unique lines from geometry');
const lines = createLines(geometry, settings);
updateProgress('Detecting open vs closed shapes');
detectOpenClosed(lines);
const { lines, faces } = createLines(geometry, settings);
updateProgress('Calculating layer intersections');
const {
layerIntersectionIndexes,
layerIntersectionPoints
} = calculateLayersIntersections(lines, settings);
const { layerPoints, layerFaceIndexes } = calculateLayersIntersections(lines, settings);
updateProgress('Constructing shapes from intersections');
const shapes = intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings);
const shapes = intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings);
applyPrecision(shapes);
@ -71,7 +48,7 @@ export default function(settings, geometry, constructLinePreview, onProgress) {
updateProgress('Optimizing paths');
optimizePaths(slices, settings);
// removePrecision(slices);
removePrecision(slices);
updateProgress('Constructing gcode');
const gcode = slicesToGCode(slices, settings);
@ -79,23 +56,34 @@ export default function(settings, geometry, constructLinePreview, onProgress) {
updateProgress('Finished');
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
gcode.gcode = gcodeToString(gcode.gcode);
gcode.gcode = new Blob([gcodeToString(gcode.gcode)], { type: 'text/plain' });
return gcode;
}
const PRECISION = 1000;
function toFixedTrimmed(value) {
return (Math.round(value * PRECISION) / PRECISION).toString();
}
function gcodeToString(gcode) {
const currentValues = {};
return gcode.reduce((string, command) => {
let first = true;
for (const action in command) {
const value = command[action];
const currentValue = currentValues[action];
if (first) {
string += action + value;
first = false;
} else if (currentValue !== value) {
string += ` ${action}${value}`;
currentValues[action] = value;
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';
@ -104,35 +92,31 @@ function gcodeToString(gcode) {
}
const MAX_SPEED = 100 * 60;
const COLOR = new Color();
function createGcodeGeometry(gcode) {
const positions = [];
const colors = [];
let lastPoint = [0, 0, 0];
for (let i = 0; i < gcode.length; i ++) {
const { G, F, X, Y, Z } = gcode[i];
const command = gcode[i];
if (typeof command === 'string') continue;
const { G, F, X, Y, Z } = command;
if (X || Y || Z) {
if (G === 1) {
positions.push(lastPoint.Y, lastPoint.Z, lastPoint.X);
positions.push(Y, Z, X);
const color = (G === 0) ? COLOR.setHex(0x00ff00) : COLOR.setHSL(F / MAX_SPEED, 0.5, 0.5);
colors.push(color.r, color.g, color.b);
colors.push(color.r, color.g, color.b);
const color = (G === 0) ? [0, 1, 0] : hslToRgb(F / MAX_SPEED, 0.5, 0.5);
colors.push(...color, ...color);
}
lastPoint = { X, Y, Z };
}
}
const geometry = new BufferGeometry();
geometry.addAttribute('position', new BufferAttribute(new Float32Array(positions), 3));
geometry.addAttribute('color', new BufferAttribute(new Float32Array(colors), 3));
const material = new LineBasicMaterial({ vertexColors: VertexColors });
const linePreview = new LineSegments(geometry, material);
return linePreview;
return {
positions: new Float32Array(positions),
colors: new Float32Array(colors)
};
}

View File

@ -1,6 +1,6 @@
import GCode from './helpers/GCode.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'];
@ -9,18 +9,15 @@ export default function slicesToGCode(slices, settings) {
layerHeight,
filamentThickness,
nozzleDiameter,
travelSpeed,
retraction,
travel,
combing,
zOffset
combing
} = settings;
const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
const lineSurfaceArea = nozzleDiameter * layerHeight;
const nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
const gcode = new GCode(settings);
gcode.updateLayerHeight(Z_OFFSET, nozzleDiameter, filamentThickness);
const gcode = new GCode(nozzleToFilamentRatio);
if (settings.startCode) gcode.addGCode(settings.startCode, settings);
const defaultProfile = {
travelProfile: travel,
@ -30,19 +27,20 @@ export default function slicesToGCode(slices, settings) {
let isFirstLayer = true;
for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer];
const z = layer * layerHeight + zOffset;
const z = layer * layerHeight + Z_OFFSET;
if (layer === 1) {
gcode.updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness);
gcode.turnFanOn();
isFirstLayer = false;
}
const profiles = PROFILE_TYPES.reduce((profiles, profileType) => {
profiles[profileType] = {
const profiles = PROFILE_TYPES.reduce((_profiles, profileType) => {
_profiles[profileType] = {
...defaultProfile,
lineProfile: isFirstLayer ? settings.firstLayer : settings[profileType]
};
return profiles;
return _profiles;
}, {});
if (typeof slice.brim !== 'undefined') {
@ -53,7 +51,7 @@ export default function slicesToGCode(slices, settings) {
const part = slice.parts[i];
if (part.closed) {
const outline = part.shell[0];
const outline = part.shell[0].mapToLower();
for (let i = 0; i < part.shell.length; i ++) {
const shell = part.shell[i];
@ -61,11 +59,11 @@ export default function slicesToGCode(slices, settings) {
const unRetract = isOuterShell;
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 && true, gcode, part.innerFill, true, false, z, profiles.innerInfill);
pathToGCode(outline, combing, gcode, part.outerFill, false, false, z, profiles.outerInfill);
pathToGCode(outline, combing, gcode, part.innerFill, true, false, z, profiles.innerInfill);
} else {
const retract = !(slice.parts.length === 1 && typeof slice.support === 'undefined');
pathToGCode(null, false, gcode, part.shape, retract, retract, z, profiles.outerShell);
@ -73,14 +71,18 @@ export default function slicesToGCode(slices, settings) {
}
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();
}
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 paths = shape.mapToLower();
@ -93,7 +95,7 @@ function pathToGCode(outline, combing, gcode, shape, retract, unRetract, z, { li
if (i === 0) {
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 ++) {
const combPoint = combPath[i];
gcode.moveTo(combPoint.x, combPoint.y, z, travelProfile);

View File

@ -1,9 +1,4 @@
import { Geometry } from 'three/src/core/Geometry.js';
import { BufferGeometry } from 'three/src/core/BufferGeometry.js';
import { VertexColors } from 'three/src/constants.js';
import { BufferAttribute } from 'three/src/core/BufferAttribute.js';
import { LineBasicMaterial } from 'three/src/materials/LineBasicMaterial.js';
import { LineSegments } from 'three/src/objects/LineSegments.js';
import * as THREE from 'three';
import slice from './sliceActions/slice.js';
import SlicerWorker from './slicer.worker.js';
@ -13,49 +8,79 @@ export function sliceMesh(settings, mesh, sync = false, constructLinePreview = f
}
mesh.updateMatrix();
const { geometry, matrix } = mesh;
return sliceGeometry(settings, geometry, matrix, sync, onProgress);
const { geometry, matrix, material } = mesh;
return sliceGeometry(settings, geometry, material, matrix, sync, constructLinePreview, onProgress);
}
export function sliceGeometry(settings, geometry, matrix, sync = false, constructLinePreview = false, onProgress) {
export function sliceGeometry(settings, geometry, materials, matrix, sync = false, constructLinePreview = false, onProgress) {
if (!geometry) {
throw new Error('Missing required geometry argument');
} else if (geometry.isBufferGeometry) {
geometry = new Geometry().fromBufferGeometry(geometry);
geometry = new THREE.Geometry().fromBufferGeometry(geometry);
} else if (geometry.isGeometry) {
geometry = geometry.clone();
} else {
throw new Error('Geometry is not an instance of BufferGeometry or Geometry');
}
if (geometry.faces.length === 0) {
throw new Error('Geometry does not contain any data');
}
if (matrix && matrix.isMatrix4) geometry.applyMatrix(matrix);
if (matrix && matrix.isMatrix4) {
geometry.applyMatrix(matrix);
}
const vertices = geometry.vertices.reduce((array, { x, y, z }, i) => {
const i3 = i * 3;
array[i3] = x;
array[i3 + 1] = y;
array[i3 + 2] = z;
return array;
}, new Float32Array(geometry.vertices.length * 3));
const faces = geometry.faces.reduce((array, { a, b, c }, i) => {
const i3 = i * 3;
array[i3] = a;
array[i3 + 1] = b;
array[i3 + 2] = c;
return array;
}, new Uint32Array(geometry.faces.length * 3));
const objectIndexes = geometry.faces.reduce((array, { materialIndex }, i) => {
array[i] = materialIndex;
return array;
}, new Uint8Array(geometry.faces.length));
if (faces.length === 0) throw new Error('Geometry does not contain any data');
geometry = { vertices, faces, objectIndexes };
const openObjectIndexes = materials instanceof Array ? materials.map(({ side }) => {
switch (side) {
case THREE.FrontSide:
return false;
case THREE.DoubleSide:
return true;
default:
return false;
}
}) : [false];
if (sync) {
return sliceSync(settings, geometry, constructLinePreview, onProgress);
return sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
} else {
return sliceAsync(settings, geometry, constructLinePreview, onProgress);
return sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
}
}
function sliceSync(settings, geometry, constructLinePreview, onProgress) {
return slice(settings, geometry, constructLinePreview, onProgress);
function sliceSync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
return gcode;
}
function sliceAsync(settings, geometry, constructLinePreview, onProgress) {
function sliceAsync(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
return new Promise((resolve, reject) => {
// create the slicer worker
const slicerWorker = new SlicerWorker();
slicerWorker.onerror = error => {
slicerWorker.addEventListener('error', event => {
slicerWorker.terminate();
reject(error);
};
reject(event);
});
// listen to messages send from worker
slicerWorker.addEventListener('message', (event) => {
@ -64,40 +89,39 @@ function sliceAsync(settings, geometry, constructLinePreview, onProgress) {
case 'SLICE': {
slicerWorker.terminate();
if (data.gcode.linePreview) {
const geometry = new BufferGeometry();
const { gcode } = data;
if (gcode.linePreview) gcode.linePreview = constructLineGeometry(gcode.linePreview);
const { position, color } = data.gcode.linePreview;
geometry.addAttribute('position', new BufferAttribute(new Float32Array(position), 3));
geometry.addAttribute('color', new BufferAttribute(new Float32Array(color), 3));
const material = new LineBasicMaterial({ vertexColors: VertexColors });
const linePreview = new LineSegments(geometry, material);
data.gcode.linePreview = linePreview;
}
resolve(data.gcode);
resolve(gcode);
break;
}
case 'PROGRESS': {
if (typeof onProgress !== 'undefined') {
onProgress(data);
}
if (typeof onProgress !== 'undefined') onProgress(data);
break;
}
default:
break;
}
});
// send geometry and settings to worker to start the slicing progress
geometry = geometry.toJSON();
const { vertices, faces, objectIndexes } = geometry;
const buffers = [vertices.buffer, faces.buffer, objectIndexes.buffer];
slicerWorker.postMessage({
message: 'SLICE',
data: {
settings,
geometry,
constructLinePreview
}
});
data: { settings, geometry, openObjectIndexes, constructLinePreview }
}, buffers);
});
}
function constructLineGeometry(linePreview) {
const geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(linePreview.positions), 3));
geometry.addAttribute('color', new THREE.BufferAttribute(new Float32Array(linePreview.colors), 3));
const material = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors });
const mesh = new THREE.LineSegments(geometry, material);
return mesh;
}

View File

@ -1,31 +1,25 @@
import 'core-js'; // polyfills
import slice from './sliceActions/slice.js';
import { JSONLoader } from 'three/src/loaders/JSONLoader.js';
const loader = new JSONLoader();
const onProgress = progress => {
self.postMessage({
message: 'PROGRESS',
data: progress
});
}
};
self.addEventListener('message', (event) => {
const { message, data } = event.data;
switch (message) {
case 'SLICE': {
const { settings, geometry, constructLinePreview, openObjectIndexes } = data;
const gcode = slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress);
const buffers = [];
const { settings, geometry: JSONGeometry, constructLinePreview } = data;
const { geometry } = loader.parse(JSONGeometry.data);
const gcode = slice(settings, geometry, constructLinePreview, onProgress);
if (gcode.linePreview) {
const position = gcode.linePreview.geometry.getAttribute('position').array;
const color = gcode.linePreview.geometry.getAttribute('color').array;
buffers.push(position.buffer, color.buffer);
gcode.linePreview = { position, color };
buffers.push(gcode.linePreview.positions.buffer);
buffers.push(gcode.linePreview.colors.buffer);
}
self.postMessage({
@ -34,5 +28,7 @@ self.addEventListener('message', (event) => {
}, buffers);
break;
}
default:
break;
}
}, false);

View File

@ -1,18 +1,23 @@
const path = require('path');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
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-object-rest-spread'),
require('babel-plugin-transform-class-properties'),
require('babel-plugin-transform-runtime')
require('babel-plugin-transform-object-rest-spread'),
require('babel-plugin-transform-runtime'),
require('babel-plugin-transform-es2015-classes')
],
babelrc: false
}
@ -24,15 +29,8 @@ module.exports = {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
alias: {
'doodle3d-slicer': path.resolve(__dirname, 'src/'),
'clipper-lib': '@doodle3d/clipper-lib',
'clipper-js': '@doodle3d/clipper-js'
}
},
module: {
rules: [
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
@ -45,22 +43,49 @@ module.exports = {
use: 'yml-loader'
}, {
test: /\.worker\.js$/,
use: ['worker-loader', babelLoader]
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: [
plugins: analyzeBundle ? [new BundleAnalyzerPlugin()] : [
new HTMLWebpackPlugin({
title: 'Doodle3D Slicer - Simple example',
favicon: 'favicon.ico',
title: 'Doodle3D Slicer',
template: require('html-webpack-template'),
inject: false,
hash: !devMode,
appMountId: 'app'
}),
],
devtool: "source-map",
devtool: devMode ? 'source-map' : false,
devServer: {
contentBase: 'dist'
}