Compare commits

...

324 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
casperlamboo
6b84572931 use three as modules 2017-12-06 11:54:09 +01:00
Rick Companje
5f1e628952 add 'name' to slice function and fix gcode object/string 2017-12-05 13:03:04 +01:00
casperlamboo
a1b4a9c454 update material 2017-12-05 11:20:06 +01:00
casperlamboo
bcf0bb254d add focus function 2017-12-05 11:10:38 +01:00
casperlamboo
d2c70f3b2f change rendering 2017-12-05 11:10:31 +01:00
casperlamboo
95ba0cfeb1 change rorate code 2017-12-05 11:10:17 +01:00
casperlamboo
84e28bc598 add on cancel 2017-12-04 19:31:15 +01:00
casperlamboo
31073e7122 remove unused code 2017-12-04 19:31:09 +01:00
casperlamboo
3dfce6a610 add title 2017-12-04 17:51:56 +01:00
casperlamboo
a79dd30abc update copy 2017-12-04 17:45:28 +01:00
casperlamboo
9d14e40c21 update tabs 2017-12-04 17:44:08 +01:00
casperlamboo
65d44db405 Make changes to UI 2017-12-04 15:08:29 +01:00
casperlamboo
bc9f0e673e add info to all printers
we should really add info like dimensions to all printers
2017-11-17 00:07:50 +01:00
casperlamboo
ed8ccd3f68 more flexibele code 2017-11-16 23:54:05 +01:00
casperlamboo
8c546e31b3 fix z offset 2017-11-16 23:32:50 +01:00
casperlamboo
8e5e000a12 semi z offset
isn’t real fix
2017-11-16 23:30:53 +01:00
casperlamboo
994a1caa98 update settings 2017-11-16 23:24:30 +01:00
casperlamboo
b47e98c005 add font family 2017-11-16 22:51:29 +01:00
casperlamboo
2aa72566db remove primary 2017-11-16 22:42:11 +01:00
casperlamboo
2d0f628743 move example 2017-11-16 22:40:39 +01:00
casperlamboo
2c2bbeda53 add z offset 2017-11-16 22:39:54 +01:00
casperlamboo
5bc7d09e8d add super() 2017-11-16 15:06:19 +01:00
casperlamboo
e33c967934 fix props 2017-11-16 15:05:28 +01:00
casperlamboo
5f1a7e3e74 simplify calculating center 2017-11-16 14:54:55 +01:00
casperlamboo
72c7c91b27 mov static functions 2017-11-16 14:54:47 +01:00
casperlamboo
aef67db205 remove height offseting 2017-11-16 14:54:33 +01:00
casperlamboo
b9b1f59af2 update displaying progress 2017-11-14 11:22:24 +01:00
casperlamboo
22298c9cb6 remove canvas width height updating 2017-11-14 11:21:58 +01:00
casperlamboo
1f7b48662a rename var printer to printers 2017-11-13 15:47:19 +01:00
casperlamboo
3221278853 rename var 2017-11-13 15:41:11 +01:00
casperlamboo
aa339fda3a fix last commit 2017-11-13 15:32:23 +01:00
casperlamboo
6e43994305 pass more data 2017-11-13 15:12:59 +01:00
casperlamboo
f4eaac16ff add browser target 2017-11-13 14:18:18 +01:00
casperlamboo
9f7242e0e4 change padding 2017-11-13 13:37:36 +01:00
casperlamboo
9e28b3b317 enable backface culling 2017-11-13 13:32:06 +01:00
casperlamboo
4e291b2370 force react v16 2017-11-13 13:21:36 +01:00
casperlamboo
d60b8d1ddb Merge branch 'develop' 2017-11-13 12:53:59 +01:00
casperlamboo
71c271bb59 Merge branch 'feature/interface' into develop 2017-11-13 12:53:32 +01:00
casperlamboo
1eedb88f0b fix building 2017-11-13 12:53:21 +01:00
casperlamboo
597ec406de set antialiasing to true 2017-11-13 12:50:45 +01:00
casperlamboo
23087af9fc hack to disable control 2017-11-13 12:44:03 +01:00
casperlamboo
8e985669ed better resize handling 2017-11-13 12:42:35 +01:00
casperlamboo
81d842cc8c move default props 2017-11-13 11:26:52 +01:00
casperlamboo
fcfe7f7bc6 update size when props change 2017-11-13 11:15:00 +01:00
casperlamboo
10055824aa space buttons 2017-11-13 11:01:57 +01:00
casperlamboo
83d96d88ec move on change to context 2017-11-13 10:45:23 +01:00
casperlamboo
45e0f02936 typo 2017-11-13 10:43:42 +01:00
casperlamboo
cd737da6d9 make settings passable 2017-11-13 10:40:58 +01:00
casperlamboo
53d2023047 rename to example 2017-11-13 03:05:29 +01:00
casperlamboo
6bdcb6cb23 also pass settings to callback 2017-11-13 03:05:06 +01:00
casperlamboo
057fd4e094 styles 2017-11-13 03:04:53 +01:00
casperlamboo
6768810fd6 remove example 2017-11-13 03:04:37 +01:00
casperlamboo
dcd3dc1614 settings now work 2017-11-13 02:47:53 +01:00
casperlamboo
6cd899f32b make settings editable 2017-11-13 02:09:39 +01:00
casperlamboo
c1117a8ce5 move static outside class 2017-11-12 19:12:32 +01:00
casperlamboo
88e056aece add basic settings drop downs 2017-11-12 18:41:00 +01:00
casperlamboo
dc6c1d7575 replace with material ui 2017-11-12 16:58:59 +01:00
casperlamboo
9c233b1ab6 don't convert geometry 2017-11-12 13:50:49 +01:00
casperlamboo
c65a5a3df4 make interface fullscreen 2017-11-12 13:08:51 +01:00
casperlamboo
3a09b93f46 set enabled of control
doesn’t do anything
2017-11-12 12:58:19 +01:00
casperlamboo
16158a3e3c add slice logging 2017-11-12 12:34:50 +01:00
casperlamboo
245e1b705a construct geometry in worker 2017-11-12 11:53:45 +01:00
casperlamboo
b6f94f6edb add draw range slider 2017-11-12 11:28:32 +01:00
casperlamboo
a5547ac070 simplify place on ground 2017-11-12 01:51:41 +01:00
casperlamboo
2c2b547ea2 focus camera 2017-11-12 01:51:33 +01:00
casperlamboo
9f49bb7b8c add border 2017-11-12 01:41:09 +01:00
casperlamboo
40d505d754 use printer from state 2017-11-12 01:41:05 +01:00
casperlamboo
ccc676ebd1 add download code 2017-11-12 01:15:38 +01:00
casperlamboo
3b4df148ed add slice again button 2017-11-12 01:04:26 +01:00
casperlamboo
849f3f893a move line preview to slice 2017-11-12 00:57:28 +01:00
casperlamboo
4654858c3e hide buttons bar when sliced 2017-11-12 00:47:06 +01:00
casperlamboo
624a178bb2 fix setting dimensions 2017-11-12 00:46:39 +01:00
casperlamboo
2b783e2889 beter type checking 2017-11-12 00:46:30 +01:00
casperlamboo
bacca4099c add comment 2017-11-12 00:46:12 +01:00
casperlamboo
c30bd0440e position geometry 2017-11-12 00:46:00 +01:00
casperlamboo
cd0406f0a9 add code preview 2017-11-12 00:11:05 +01:00
casperlamboo
ae24974e31 create basic component 2017-11-11 20:23:45 +01:00
casperlamboo
3f3408747c Merge branch 'develop'
# Conflicts:
#	src/settings/printer.yml
2017-10-17 12:07:52 +02:00
Rick Companje
dcfd1bf318 added ultimaker2+ and wanhao_duplicator_i3+ 2017-10-17 11:47:55 +02:00
73 changed files with 23497 additions and 195268 deletions

View File

@ -1,19 +1,26 @@
{ {
"env": { "env": {
// transpile to common node & browser compatible js, keeping modules
"module": { "module": {
"presets": [ "presets": [
["latest", { ["env", {
"targets": {
"node": "6",
"browsers": ["last 2 versions", "safari >= 7", "not ie < 11"]
},
"modules": false "modules": false
}] }],
"stage-0",
"react"
] ]
}, },
// transpile to common node & browser compatible js, using commonjs
"main": { "main": {
"presets": ["latest"] "presets": ["env", "stage-0", "react"]
} }
}, },
"plugins": [ "plugins": [
"babel-plugin-transform-object-rest-spread" "transform-class-properties",
"transform-object-rest-spread",
"transform-runtime",
"transform-es2015-classes"
] ]
} }

33
.eslintrc Normal file
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
}
}

4
.gitignore vendored
View File

@ -1,10 +1,8 @@
*.DS_Store *.DS_Store
jspm_package
node_modules node_modules
lib lib
module module
dist dist

View File

@ -1,4 +1,2 @@
node_modules node_modules
jspm_packages jspm_packages
example
simpleExample

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'; import { defaultSettings, sliceGeometry } from 'Doodle3D/Doodle3D-Slicer';
const settings = { const settings = {
...defaultSettings.base, ...defaultSettings.default,
...defaultSettings.material.pla, ...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go, ...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high ...defaultSettings.quality.high
@ -27,7 +27,7 @@ const gcode = await sliceGeometry(settings, geometry);
import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer'; import { defaultSettings } from 'Doodle3D/Doodle3D-Slicer';
const settings = { const settings = {
...defaultSettings.base, ...defaultSettings.default,
...defaultSettings.material.pla, ...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go, ...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high ...defaultSettings.quality.high

97
comb.js Normal file
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.

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,27 +0,0 @@
import * as THREE from 'three';
import { defaultSettings, sliceGeometry } from 'src/index.js';
import fileSaver from 'file-saver';
const settings = {
...defaultSettings.base,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high
};
const jsonLoader = new THREE.JSONLoader();
jsonLoader.load('models/airplane.json', async geometry => {
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.0, 50)));
geometry.computeFaceNormals();
const onProgress = ({ progress: { done, total, action } }) => {
const percentage = `${(done / total * 100).toFixed()}%`
document.write(`<p>${action}, ${percentage}</p>`);
};
const gcode = await sliceGeometry(settings, geometry, null, false, onProgress);
const file = new File([gcode], 'gcode.gcode', { type: 'text/plain' });
fileSaver.saveAs(file);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

BIN
favicon.ico Normal file

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

47
index.js Normal file
View File

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

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

22604
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,17 @@
{ {
"name": "@doodle3d/doodle3d-slicer", "name": "@doodle3d/doodle3d-slicer",
"version": "0.0.13", "version": "0.0.18",
"description": "JavaScript gcode slicer, Intended to use with the Doodle3D WiFi-Box # Usage", "description": "JavaScript gcode slicer for Doodle3D Transform",
"main": "lib/index.js", "main": "lib/index.js",
"module": "module/index.js", "module": "module/index.js",
"esnext": "src/index.js", "esnext": "src/index.js",
"scripts": { "scripts": {
"start": "webpack-dev-server -w",
"dist": "NODE_ENV=production webpack -p",
"lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"upload": "npm run dist && scp -r dist/* doodle3d.com:/domains/doodle3d.com/print",
"analyze": "NODE_ENV=production ANALYZE_BUNDLE=true webpack -p",
"build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ", "build": "npm run build:main && npm run build:main:settings && npm run build:module && npm run build:module:settings ",
"build:main": "BABEL_ENV=main babel src -s -d lib", "build:main": "BABEL_ENV=main babel src -s -d lib",
"build:module": "BABEL_ENV=module babel src -s -d module", "build:module": "BABEL_ENV=module babel src -s -d module",
@ -14,21 +19,61 @@
"build:module:settings": "cp -r src/settings module" "build:module:settings": "cp -r src/settings module"
}, },
"dependencies": { "dependencies": {
"@doodle3d/clipper-js": "^1.0.7", "@doodle3d/clipper-js": "^1.0.10",
"three": "^0.83.0" "lodash": "^4.17.4",
"material-ui": "^0.19.4",
"material-ui-icons": "^1.0.0-beta.17",
"material-ui-textfield-icon": "^0.2.2-1",
"proptypes": "^1.1.0",
"react": "^16.0.0",
"react-addons-update": "^15.6.2",
"react-dom": "^16.0.0",
"react-jss": "^7.2.0",
"react-resize-detector": "^1.1.0",
"shortid": "^2.2.8",
"three": "^0.88.0",
"validate-ip": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.24.1", "file-saver": "^1.3.3",
"babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-cli": "6.24.1",
"babel-preset-latest": "^6.24.1" "babel-eslint": "^5.0.4",
"babel-loader": "7.0.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-classes": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"eslint": "^1.10.3",
"eslint-config-airbnb": "^3.1.0",
"eslint-plugin-react": "^3.16.1",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^2.29.0",
"html-webpack-template": "^6.0.2",
"query-string": "^5.0.1",
"image-webpack-loader": "^4.2.0",
"imports-loader": "^0.7.1",
"material-ui": "^0.19.4",
"normalize-jss": "^4.0.0",
"raw-loader": "^0.5.1",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.2",
"webpack-dev-server": "^2.5.1",
"worker-loader": "^0.8.1",
"yml-loader": "^2.1.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git" "url": "git+https://github.com/Doodle3D/Doodle3D-Slicer.git"
}, },
"author": "", "author": "Casper @Doodle3D",
"license": "UNLICENSED", "license": "MIT",
"private": true, "private": false,
"bugs": { "bugs": {
"url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues" "url": "https://github.com/Doodle3D/Doodle3D-Slicer/issues"
}, },

View File

@ -1,29 +0,0 @@
import * as THREE from 'three';
import { defaultSettings, sliceGeometry } from 'doodle3d-slicer';
import fileURL from '!url-loader!./models/combingtest.json';
import fileSaver from 'file-saver';
const settings = {
...defaultSettings.base,
...defaultSettings.material.pla,
...defaultSettings.printer.ultimaker2go,
...defaultSettings.quality.high
};
const jsonLoader = new THREE.JSONLoader();
jsonLoader.load(fileURL, geometry => {
geometry.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / -2));
geometry.applyMatrix(new THREE.Matrix4().setPosition(new THREE.Vector3(50, -0.0, 50)));
const onProgress = ({ progress: { done, total, action } }) => {
const percentage = `${(done / total * 100).toFixed()}%`
document.write(`<p>${action}, ${percentage}</p>`);
};
const { filament, duration, gcode } = sliceGeometry(settings, geometry, null, true, onProgress);
// console.log('filament: ', filament);
// console.log('duration: ', duration);
// document.body.innerHTML = gcode.replace(/(?:\r\n|\r|\n)/g, '<br />');
const file = new File([gcode], 'gcode.gcode', { type: 'text/plain' });
fileSaver.saveAs(file);
});

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
{
"name": "doodle3d-simple-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack -p",
"start": "webpack-dev-server -w"
},
"author": "",
"license": "ISC",
"dependencies": {
"babel-polyfill": "^6.23.0",
"file-saver": "^1.3.3",
"three": "^0.83.0",
"url-loader": "^0.5.9"
},
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-preset-latest": "^6.24.1",
"html-webpack-plugin": "^2.29.0",
"webpack": "^3.3.0",
"webpack-dev-server": "^2.5.1",
"worker-loader": "^0.8.1",
"yml-loader": "^2.1.0"
}
}

View File

@ -1,58 +0,0 @@
const path = require('path');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HTMLWebpackPlugin = require('html-webpack-plugin');
const babelLoader = {
loader: 'babel-loader',
options: {
presets: [
['latest', {
'modules': false,
'loose': true
}]
],
plugins: [require('babel-plugin-transform-object-rest-spread')],
babelrc: false
}
}
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
alias: {
'doodle3d-slicer': path.resolve(__dirname, '../src/index.js'),
'clipper-lib': '@doodle3d/clipper-lib',
'clipper-js': '@doodle3d/clipper-js'
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: babelLoader
},
{
test: /\.yml$/,
use: 'yml-loader'
},
{
test: /\.worker\.js$/,
use: ['worker-loader', babelLoader]
}
]
},
plugins: [
new HTMLWebpackPlugin({
title: 'Doodle3D Slicer - Simple example'
}),
],
devtool: "source-map",
devServer: {
contentBase: 'dist'
}
};

View File

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

View File

@ -1,18 +1,22 @@
import { sliceGeometry, sliceMesh } from './slicer.js'; import { sliceGeometry, sliceMesh } from './slicer.js';
import baseSettings from './settings/default.yml'; import Interface from './interface/index.js';
import _defaultSettings from './settings/default.yml';
import printerSettings from './settings/printer.yml'; import printerSettings from './settings/printer.yml';
import materialSettings from './settings/material.yml'; import materialSettings from './settings/material.yml';
import qualitySettings from './settings/quality.yml'; import qualitySettings from './settings/quality.yml';
import infillSettings from './settings/infill.yml';
const defaultSettings = { const defaultSettings = {
base: baseSettings, default: _defaultSettings,
printer: printerSettings, printer: printerSettings,
material: materialSettings, material: materialSettings,
quality: qualitySettings quality: qualitySettings,
infill: infillSettings
}; };
export { export {
sliceGeometry, sliceGeometry,
sliceMesh, sliceMesh,
Interface,
defaultSettings defaultSettings
}; };

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

@ -0,0 +1,104 @@
import React from 'react';
import PropTypes from 'proptypes';
import _ from 'lodash';
import MaterialUISelectField from 'material-ui/SelectField';
import MaterialUICheckbox from 'material-ui/Checkbox';
import TextFieldIcon from 'material-ui-textfield-icon';
import RefreshIcon from 'material-ui-icons/Refresh';
import muiThemeable from 'material-ui/styles/muiThemeable';
export const contextTypes = {
settings: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
addPrinter: PropTypes.object.isRequired,
managePrinter: PropTypes.object.isRequired,
advancedFields: PropTypes.array.isRequired,
activePrinter: PropTypes.string
};
const propTypes = {
name: PropTypes.string.isRequired,
muiTheme: PropTypes.object.isRequired
};
export const _SelectField = ({ name, muiTheme, ...props }, context) => (
<MaterialUISelectField
{...props}
disabled={context.disabled}
value={_.get(context, name)}
onChange={(event, index, value) => context.onChange(name, value)}
/>
);
_SelectField.contextTypes = contextTypes;
_SelectField.propTypes = propTypes;
export const SelectField = muiThemeable()(_SelectField);
const _TextField = ({ name, muiTheme: { palette }, ...props }, context) => (
<TextFieldIcon
{...props}
icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }}
onClick={() => context.onChange(name, null)}
/>}
floatingLabelStyle={{
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
value={_.get(context, name)}
onChange={(event, value) => context.onChange(name, value)}
/>
);
_TextField.contextTypes = contextTypes;
_TextField.propTypes = propTypes;
export const TextField = muiThemeable()(_TextField);
const _NumberField = ({ name, min, max, muiTheme: { palette }, ...props }, context) => (
<TextFieldIcon
{...props}
type="number"
icon={context.advancedFields.includes(name) && <RefreshIcon
style={{ fill: palette.textColor }}
onClick={() => context.onChange(name, null)}
/>}
floatingLabelStyle={{
color: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
value={_.get(context, name.toString())}
onChange={(event, value) => {
value = parseFloat(value);
context.onChange(name, value);
}}
onBlur={() => {
const value = _.get(context, name.toString());
let newValue = value;
if (typeof min === 'number') newValue = Math.max(newValue, min);
if (typeof max === 'number') newValue = Math.min(newValue, max);
if (newValue !== value) context.onChange(name, newValue);
}}
/>
);
_NumberField.contextTypes = contextTypes;
_NumberField.propTypes = propTypes;
export const NumberField = muiThemeable()(_NumberField);
const _Checkbox = ({ name, muiTheme: { palette }, ...props }, context) => (
<span style={{ display: 'flex', position: 'relative' }}>
<MaterialUICheckbox
{...props}
style={{ display: 'block' }}
iconStyle={{
fill: context.advancedFields.includes(name) ? palette.primary1Color : palette.primary3Color
}}
disabled={context.disabled}
checked={_.get(context, name)}
onCheck={(event, value) => context.onChange(name, value)}
/>
{context.advancedFields.includes(name) && <RefreshIcon
onClick={() => context.onChange(name, null)}
/>}
</span>
);
_Checkbox.contextTypes = contextTypes;
_Checkbox.propTypes = propTypes;
export const Checkbox = muiThemeable()(_Checkbox);

568
src/interface/Settings.js Normal file
View File

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

377
src/interface/index.js Normal file
View File

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

143
src/interface/utils.js Normal file
View File

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

View File

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

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

@ -1,40 +1,79 @@
_3Dison_plus: _3Dison_plus:
title: 3Dison plus title: 3Dison plus
heatedBed: false
filamentThickness: 2.85
dimensions: dimensions:
x: 227 x: 227
y: 147 y: 147
z: 150 z: 150
bigbuilder3d: bigbuilder3d:
title: Big Builder 3D title: Big Builder 3D
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
builder3d: builder3d:
title: Builder 3D title: Builder 3D
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
bukobot: bukobot:
title: Bukobot title: Bukobot
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
cartesio: cartesio:
title: Cartesio title: Cartesio
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
colido_2_0_plus: colido_2_0_plus:
title: ColiDo 2.0 Plus title: ColiDo 2.0 Plus
heatedBed: true heatedBed: true
filamentThickness: 2.85
dimensions: dimensions:
x: 230 x: 230
y: 150 y: 150
z: 140 z: 140
colido_compact: colido_compact:
title: ColiDo Compact title: ColiDo Compact
heatedBed: false
filamentThickness: 2.85
dimensions: dimensions:
x: 130 x: 130
y: 130 y: 130
z: 115 z: 115
colido_diy: colido_diy:
title: ColiDo DIY title: ColiDo DIY
heatedBed: false
filamentThickness: 2.85
dimensions: dimensions:
x: 200
y: 200
z: 170 z: 170
colido_m2020: colido_m2020:
title: ColiDo M2020 title: ColiDo M2020
heatedBed: true heatedBed: true
filamentThickness: 2.85
dimensions:
x: 200
y: 200
z: 200
colido_x3045: colido_x3045:
title: ColiDo X3045 title: ColiDo X3045
heatedBed: true heatedBed: true
filamentThickness: 2.85
dimensions: dimensions:
x: 300 x: 300
y: 300 y: 300
@ -42,23 +81,27 @@ colido_x3045:
craftbot_plus: craftbot_plus:
title: CraftBot PLUS title: CraftBot PLUS
heatedBed: true heatedBed: true
filamentThickness: 1.75 filamentThickness: 2.85
dimensions: dimensions:
x: 250 x: 250
y: 200
z: 200
cyrus: cyrus:
title: Cyrus title: Cyrus
heatedBed: false
dimensions:
x: 195
y: 195
z: 200
delta_rostockmax: delta_rostockmax:
title: Delta RostockMax title: Delta RostockMax
dimensions: heatedBed: false
x: 0
y: 0
deltamaker: deltamaker:
title: Deltamaker title: Deltamaker
dimensions: heatedBed: false
x: 0
y: 0
doodle_dream: doodle_dream:
title: Doodle Dream title: Doodle Dream
heatedBed: false
filamentThickness: 1.75 filamentThickness: 1.75
dimensions: dimensions:
x: 120 x: 120
@ -66,68 +109,119 @@ doodle_dream:
z: 80 z: 80
eventorbot: eventorbot:
title: EventorBot title: EventorBot
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 203
y: 250
z: 150
felix: felix:
title: Felix title: Felix
heatedBed: false
filamentThickness: 2.85
dimensions:
x: 237
y: 244
z: 235
gigabot: gigabot:
title: Gigabot title: Gigabot
heatedBed: false
filamentThickness: 2.85
kossel: kossel:
title: Kossel title: Kossel
dimensions: heatedBed: false
x: 0 filamentThickness: 2.85
y: 0
leapfrog_creatr: leapfrog_creatr:
title: LeapFrog Creatr title: LeapFrog Creatr
heatedBed: false
filamentThickness: 2.85
lulzbot_aO_101: lulzbot_aO_101:
title: LulzBot AO-101 title: LulzBot AO-101
heatedBed: false
filamentThickness: 2.85
lulzbot_taz_4: lulzbot_taz_4:
title: LulzBot TAZ 4 title: LulzBot TAZ 4
heatedBed: true
filamentThickness: 2.85
dimensions: dimensions:
x: 298 x: 298
y: 275 y: 275
z: 250 z: 250
heatedBed: true
makerbot_generic: makerbot_generic:
title: Generic Makerbot Printer title: Generic Makerbot Printer
heatedBed: false
filamentThickness: 2.85
makerbot_replicator2: makerbot_replicator2:
title: MakerBot Replicator2 title: MakerBot Replicator2
heatedBed: false
filamentThickness: 2.85
makerbot_replicator2x: makerbot_replicator2x:
title: MakerBot Replicator2x title: MakerBot Replicator2x
heatedBed: true heatedBed: true
filamentThickness: 2.85
makerbot_thingomatic: makerbot_thingomatic:
title: MakerBot Thing-o-matic title: MakerBot Thing-o-matic
heatedBed: false
filamentThickness: 2.85
makergear_m2: makergear_m2:
title: MakerGear M2 title: MakerGear M2
heatedBed: false
filamentThickness: 2.85
makergear_prusa: makergear_prusa:
title: MakerGear Prusa title: MakerGear Prusa
heatedBed: false
filamentThickness: 2.85
makibox: makibox:
title: Makibox title: Makibox
heatedBed: false
filamentThickness: 2.85
mamba3d: mamba3d:
title: Mamba3D title: Mamba3D
heatedBed: false
filamentThickness: 2.85
marlin_generic: marlin_generic:
title: Generic Marlin Printer title: Generic Marlin Printer
heatedBed: false
filamentThickness: 2.85
minifactory: minifactory:
title: miniFactory title: miniFactory
heatedBed: true
filamentThickness: 2.85
dimensions: dimensions:
x: 150 x: 150
y: 150 y: 150
z: 155 z: 155
heatedBed: true
orca_0_3: orca_0_3:
title: Orca 0.3 title: Orca 0.3
heatedBed: false
filamentThickness: 2.85
ord_bot_hadron: ord_bot_hadron:
title: ORD Bot Hadron title: ORD Bot Hadron
heatedBed: false
filamentThickness: 2.85
printrbot: printrbot:
title: Printrbot title: Printrbot
heatedBed: false
filamentThickness: 2.85
printxel_3d: printxel_3d:
title: Printxel 3D title: Printxel 3D
heatedBed: false
filamentThickness: 2.85
prusa_i3: prusa_i3:
title: Prusa I3 title: Prusa I3
heatedBed: false
filamentThickness: 2.85
prusa_iteration_2: prusa_iteration_2:
title: Prusa Iteration 2 title: Prusa Iteration 2
heatedBed: false
filamentThickness: 2.85
rapman: rapman:
title: RapMan title: RapMan
heatedBed: false
filamentThickness: 2.85
renkforce_rf100: renkforce_rf100:
title: Renkforce RF100 title: Renkforce RF100
heatedBed: false
filamentThickness: 1.75 filamentThickness: 1.75
dimensions: dimensions:
x: 100 x: 100
@ -135,27 +229,91 @@ renkforce_rf100:
z: 100 z: 100
reprappro_huxley: reprappro_huxley:
title: RepRapPro Huxley title: RepRapPro Huxley
heatedBed: false
filamentThickness: 2.85
reprappro_mendel: reprappro_mendel:
title: RepRapPro Mendel title: RepRapPro Mendel
heatedBed: false
filamentThickness: 2.85
rigidbot: rigidbot:
title: Rigidbot title: Rigidbot
heatedBed: false
filamentThickness: 2.85
robo_3d_printer: robo_3d_printer:
title: RoBo 3D Printer title: RoBo 3D Printer
heatedBed: false
filamentThickness: 2.85
shapercube: shapercube:
title: ShaperCube title: ShaperCube
heatedBed: false
filamentThickness: 2.85
tantillus: tantillus:
title: Tantillus title: Tantillus
heatedBed: false
filamentThickness: 2.85
ultimaker: ultimaker:
title: Ultimaker Original title: Ultimaker Original
heatedBed: false
filamentThickness: 2.85
ultimaker2: ultimaker2:
title: Ultimaker 2 title: Ultimaker 2
heatedBed: true heatedBed: true
filamentThickness: 2.85
dimensions: dimensions:
x: 223 x: 223
y: 223 y: 223
z: 205 z: 205
ultimaker2_plus:
title: Ultimaker 2+
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 205
ultimaker2_plus_extended:
title: Ultimaker 2+ Extended
heatedBed: true
filamentThickness: 2.85
dimensions:
x: 223
y: 223
z: 305
ultimaker2go: ultimaker2go:
startCode: |-
M10000
M10000
M10001 X8 Y28 SDoodle3D heat up...
M109 S{temperature} ;set target temperature
{if heatedBed}M190 S{bedTemperature} ;set target bed temperature
G21 ;metric values
G90 ;absolute positioning
M107 ;start with the fan off
G28 ; home to endstops
G1 Z15 F9000 ;move the platform down 15mm
G92 E0 ;zero the extruded length
G1 F200 E10 ;extrude 10mm of feed stock
G92 E0 ;zero the extruded length again
G1 F9000
M10000
M10000
M10001 X8 Y28 SDoodle3D printing...
endCode: |-
M10000
M10000
M10001 X20 Y28 SDoodle3D done!
M107 ;fan off
G91 ;relative positioning
G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure
G1 Z+5.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more
G28 ;home the printer
M84 ;disable axes / steppers
G90 ;absolute positioning
M104 S0
{if heatedBed}M140 S0
title: Ultimaker 2 Go title: Ultimaker 2 Go
heatedBed: false
filamentThickness: 2.85
dimensions: dimensions:
x: 120 x: 120
y: 120 y: 120
@ -163,13 +321,47 @@ ultimaker2go:
ultimaker_original_plus: ultimaker_original_plus:
title: Ultimaker Original Plus title: Ultimaker Original Plus
heatedBed: true heatedBed: true
filamentThickness: 2.85
vision_3d_printer: vision_3d_printer:
title: Vision 3D Printer title: Vision 3D Printer
heatedBed: false
filamentThickness: 2.85
wanhao_duplicator4: wanhao_duplicator4:
title: Wanhao Duplicator 4 title: Wanhao Duplicator 4
filamentThickness: 1.75
heatedBed: true heatedBed: true
filamentThickness: 1.75
dimensions: dimensions:
x: 210 x: 210
y: 140 y: 140
z: 140 z: 140
wanhao_duplicator_i3_plus:
title: Wanhao Duplicator i3 Plus
heatedBed: false
filamentThickness: 1.75
wanhao_duplicator_i3_mini:
duplicator_i3_mini:
startCode: |-
M104 S{temperature}
G28
M109 S{temperature}
G90
M82
G1 Z10.0 F6000
G92 E0
G1 F200 E3
G92 E0
endCode: |-
M104 S0
G92 E1
G1 E-1 F300
G28 X0 Y0
M84
M82
M104 S0
title: Wanhao Duplicator i3 Mini
heatedBed: false
filamentThickness: 1.75
dimensions:
x: 120
y: 135
z: 120

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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 ++) { for (let layer = 0; layer < slices.length; layer ++) {
const slice = slices[layer]; const slice = slices[layer];

View File

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

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

View File

@ -1,22 +1,24 @@
import * as THREE from 'three'; import { distanceTo } from './vector2.js';
import { PRECISION } from '../../constants.js'; import { VERSION } from '../../constants.js';
const MOVE = 'G'; export const MOVE = 'G';
const M_COMMAND = 'M'; export const M_COMMAND = 'M';
const FAN_SPEED = 'S'; export const FAN_SPEED = 'S';
const SPEED = 'F'; export const SPEED = 'F';
const EXTRUDER = 'E'; export const EXTRUDER = 'E';
const POSITION_X = 'X'; export const POSITION_X = 'X';
const POSITION_Y = 'Y'; export const POSITION_Y = 'Y';
const POSITION_Z = 'Z'; export const POSITION_Z = 'Z';
export default class { export default class GCode {
constructor(nozzleToFilamentRatio) { constructor(settings) {
this._nozzleToFilamentRatio = nozzleToFilamentRatio; this._nozzleToFilamentRatio = 1;
this._gcode = [
this._gcode = ''; `; ${JSON.stringify(settings)}`,
`; Generated with Doodle3D Slicer V${VERSION}`
];
this._currentValues = {}; this._currentValues = {};
this._nozzlePosition = new THREE.Vector2(0, 0); this._nozzlePosition = { x: 0, y: 0 };
this._extruder = 0.0; this._extruder = 0.0;
this._duration = 0.0; this._duration = 0.0;
this._isRetracted = false; this._isRetracted = false;
@ -24,30 +26,19 @@ export default class {
} }
_addGCode(command) { _addGCode(command) {
let str = ''; this._gcode.push(command);
}
let first = true; updateLayerHeight(layerHeight, nozzleDiameter, filamentThickness) {
for (const action in command) { const filamentSurfaceArea = Math.pow((filamentThickness / 2), 2) * Math.PI;
const value = command[action]; const lineSurfaceArea = nozzleDiameter * layerHeight;
const currentValue = this._currentValues[action]; this._nozzleToFilamentRatio = lineSurfaceArea / filamentSurfaceArea;
if (first) {
str = action + value;
first = false;
} else if (currentValue !== value) {
str += ` ${action}${value}`;
this._currentValues[action] = value;
}
}
this._gcode += `${str}\n`;
} }
turnFanOn(fanSpeed) { turnFanOn(fanSpeed) {
this._isFanOn = true; this._isFanOn = true;
const gcode = { [M_COMMAND]: 106 } const gcode = { [M_COMMAND]: 106 };
if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed; if (typeof fanSpeed !== 'undefined') gcode[FAN_SPEED] = fanSpeed;
this._addGCode(gcode); this._addGCode(gcode);
@ -64,41 +55,41 @@ export default class {
} }
moveTo(x, y, z, { speed }) { moveTo(x, y, z, { speed }) {
const newNozzlePosition = new THREE.Vector2(x, y).multiplyScalar(PRECISION); const newNozzlePosition = { x, y };
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition); const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._duration += lineLength / speed; this._duration += lineLength / speed;
this._addGCode({ this._addGCode({
[MOVE]: 0, [MOVE]: 0,
[POSITION_X]: newNozzlePosition.x.toFixed(3), [POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y.toFixed(3), [POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z.toFixed(3), [POSITION_Z]: z,
[SPEED]: (speed * 60).toFixed(3) [SPEED]: speed * 60
}); });
this._nozzlePosition.copy(newNozzlePosition); this._nozzlePosition = newNozzlePosition;
return this; return this;
} }
lineTo(x, y, z, { speed, flowRate }) { lineTo(x, y, z, { speed, flowRate }) {
const newNozzlePosition = new THREE.Vector2(x, y).multiplyScalar(PRECISION); const newNozzlePosition = { x, y };
const lineLength = this._nozzlePosition.distanceTo(newNozzlePosition); const lineLength = distanceTo(this._nozzlePosition, newNozzlePosition);
this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate; this._extruder += this._nozzleToFilamentRatio * lineLength * flowRate;
this._duration += lineLength / speed; this._duration += lineLength / speed;
this._addGCode({ this._addGCode({
[MOVE]: 1, [MOVE]: 1,
[POSITION_X]: newNozzlePosition.x.toFixed(3), [POSITION_X]: newNozzlePosition.x,
[POSITION_Y]: newNozzlePosition.y.toFixed(3), [POSITION_Y]: newNozzlePosition.y,
[POSITION_Z]: z.toFixed(3), [POSITION_Z]: z,
[SPEED]: (speed * 60).toFixed(3), [SPEED]: speed * 60,
[EXTRUDER]: this._extruder.toFixed(3) [EXTRUDER]: this._extruder
}); });
this._nozzlePosition.copy(newNozzlePosition); this._nozzlePosition = newNozzlePosition;
return this; return this;
} }
@ -112,8 +103,8 @@ export default class {
this._addGCode({ this._addGCode({
[MOVE]: 0, [MOVE]: 0,
[EXTRUDER]: this._extruder.toFixed(3), [EXTRUDER]: this._extruder,
[SPEED]: (speed * 60).toFixed(3) [SPEED]: speed * 60
}); });
} }
} }
@ -130,8 +121,8 @@ export default class {
this._addGCode({ this._addGCode({
[MOVE]: 0, [MOVE]: 0,
[EXTRUDER]: (this._extruder - amount).toFixed(3), [EXTRUDER]: this._extruder - amount,
[SPEED]: (speed * 60).toFixed(3) [SPEED]: speed * 60
}); });
} }
} }
@ -139,6 +130,15 @@ export default class {
return this; return this;
} }
addGCode(gcode, { temperature, bedTemperature, heatedBed }) {
gcode = gcode
.replace(/{temperature}/g, temperature)
.replace(/{if heatedBed}.*?\n/g, str => heatedBed ? str.replace(/{if heatedBed}/g, '') : '')
.replace(/{bedTemperature}/g, bedTemperature);
this._addGCode(gcode);
}
getGCode() { getGCode() {
return { return {
gcode: this._gcode, gcode: this._gcode,

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

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

View File

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

View File

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

View File

@ -9,43 +9,26 @@ import addBrim from './addBrim.js';
import optimizePaths from './optimizePaths.js'; import optimizePaths from './optimizePaths.js';
import shapesToSlices from './shapesToSlices.js'; import shapesToSlices from './shapesToSlices.js';
import slicesToGCode from './slicesToGCode.js'; import slicesToGCode from './slicesToGCode.js';
import detectOpenClosed from './detectOpenClosed.js';
import applyPrecision from './applyPrecision.js'; import applyPrecision from './applyPrecision.js';
// import removePrecision from './removePrecision.js'; import { hslToRgb } from './helpers/color.js';
import removePrecision from './removePrecision.js';
export default function(settings, geometry, onProgress) { export default function slice(settings, geometry, openObjectIndexes, constructLinePreview, onProgress) {
const totalStages = 12; const total = 11;
let current = -1; let done = -1;
const updateProgress = (action) => { const updateProgress = action => {
current ++; done ++;
if (typeof onProgress !== 'undefined') { if (onProgress) onProgress({ progress: { done, total, action } });
onProgress({
progress: {
done: current,
total: totalStages,
action
}
});
}
}; };
geometry.computeFaceNormals();
// get unique lines from geometry;
updateProgress('Constructing unique lines from geometry'); updateProgress('Constructing unique lines from geometry');
const lines = createLines(geometry, settings); const { lines, faces } = createLines(geometry, settings);
updateProgress('Detecting open vs closed shapes');
detectOpenClosed(lines);
updateProgress('Calculating layer intersections'); updateProgress('Calculating layer intersections');
const { const { layerPoints, layerFaceIndexes } = calculateLayersIntersections(lines, settings);
layerIntersectionIndexes,
layerIntersectionPoints
} = calculateLayersIntersections(lines, settings);
updateProgress('Constructing shapes from intersections'); updateProgress('Constructing shapes from intersections');
const shapes = intersectionsToShapes(layerIntersectionIndexes, layerIntersectionPoints, lines, settings); const shapes = intersectionsToShapes(layerPoints, layerFaceIndexes, faces, openObjectIndexes, settings);
applyPrecision(shapes); applyPrecision(shapes);
@ -65,12 +48,75 @@ export default function(settings, geometry, onProgress) {
updateProgress('Optimizing paths'); updateProgress('Optimizing paths');
optimizePaths(slices, settings); optimizePaths(slices, settings);
// removePrecision(slices); removePrecision(slices);
updateProgress('Constructing gcode'); updateProgress('Constructing gcode');
const gcode = slicesToGCode(slices, settings); const gcode = slicesToGCode(slices, settings);
updateProgress('Finished'); updateProgress('Finished');
if (constructLinePreview) gcode.linePreview = createGcodeGeometry(gcode.gcode);
gcode.gcode = new Blob([gcodeToString(gcode.gcode)], { type: 'text/plain' });
return gcode; return gcode;
} }
const PRECISION = 1000;
function toFixedTrimmed(value) {
return (Math.round(value * PRECISION) / PRECISION).toString();
}
function gcodeToString(gcode) {
const currentValues = {};
return gcode.reduce((string, command) => {
if (typeof command === 'string') {
string += command;
} else {
let first = true;
for (const action in command) {
const value = toFixedTrimmed(command[action]);
const currentValue = currentValues[action];
if (first) {
string += `${action}${value}`;
first = false;
} else if (currentValue !== value) {
string += ` ${action}${value}`;
currentValues[action] = value;
}
}
}
string += '\n';
return string;
}, '');
}
const MAX_SPEED = 100 * 60;
function createGcodeGeometry(gcode) {
const positions = [];
const colors = [];
let lastPoint = [0, 0, 0];
for (let i = 0; i < gcode.length; i ++) {
const command = gcode[i];
if (typeof command === 'string') continue;
const { G, F, X, Y, Z } = command;
if (X || Y || Z) {
if (G === 1) {
positions.push(lastPoint.Y, lastPoint.Z, lastPoint.X);
positions.push(Y, Z, X);
const color = (G === 0) ? [0, 1, 0] : hslToRgb(F / MAX_SPEED, 0.5, 0.5);
colors.push(...color, ...color);
}
lastPoint = { X, Y, Z };
}
}
return {
positions: new Float32Array(positions),
colors: new Float32Array(colors)
};
}

View File

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

View File

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

View File

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

92
webpack.config.js Normal file
View File

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