├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── PostProcessingPlugin.py
├── PostProcessingPlugin.qml
├── README.md
├── Script.py
├── __init__.py
├── plugin.json
├── postprocessing.svg
└── scripts
├── BQ_PauseAtHeight.py
├── ColorChange.py
├── ExampleScript.py
├── PauseAtHeight.py
├── PauseAtHeightforRepetier.py
├── SearchAndReplace.py
├── Stretch.py
└── TweakAtZ.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *.qmlc
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 |
47 | # Translations
48 | *.mo
49 | *.pot
50 |
51 | # Django stuff:
52 | *.log
53 |
54 | # Sphinx documentation
55 | docs/_build/
56 |
57 | # PyBuilder
58 | target/
59 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | project(PostProcessingPlugin)
2 |
3 | cmake_minimum_required(VERSION 2.8.12)
4 |
5 | find_package(PythonInterp 3.4.0 REQUIRED)
6 |
7 | install(FILES
8 | plugin.json
9 | __init__.py
10 | PostProcessingPlugin.py
11 | PostProcessingPlugin.qml
12 | postprocessing.svg
13 | Script.py
14 | LICENSE
15 | README.md
16 | DESTINATION lib${LIB_SUFFIX}/cura/plugins/PostProcessingPlugin
17 | )
18 |
19 | install(DIRECTORY scripts DESTINATION lib${LIB_SUFFIX}/cura/plugins/PostProcessingPlugin PATTERN "ExampleScript.py" EXCLUDE)
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
663 |
--------------------------------------------------------------------------------
/PostProcessingPlugin.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
2 | # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
3 | from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
4 |
5 | from UM.PluginRegistry import PluginRegistry
6 | from UM.Resources import Resources
7 | from UM.Application import Application
8 | from UM.Extension import Extension
9 | from UM.Logger import Logger
10 |
11 | import os.path
12 | import pkgutil
13 | import sys
14 | import importlib.util
15 |
16 | from UM.i18n import i18nCatalog
17 | i18n_catalog = i18nCatalog("cura")
18 |
19 |
20 | ## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
21 | # g-code files.
22 | class PostProcessingPlugin(QObject, Extension):
23 | def __init__(self, parent = None):
24 | super().__init__(parent)
25 | self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
26 | self._view = None
27 |
28 | # Loaded scripts are all scripts that can be used
29 | self._loaded_scripts = {}
30 | self._script_labels = {}
31 |
32 | # Script list contains instances of scripts in loaded_scripts.
33 | # There can be duplicates, which will be executed in sequence.
34 | self._script_list = []
35 | self._selected_script_index = -1
36 |
37 | Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
38 |
39 | selectedIndexChanged = pyqtSignal()
40 | @pyqtProperty("QVariant", notify = selectedIndexChanged)
41 | def selectedScriptDefinitionId(self):
42 | try:
43 | return self._script_list[self._selected_script_index].getDefinitionId()
44 | except:
45 | return ""
46 |
47 | @pyqtProperty("QVariant", notify=selectedIndexChanged)
48 | def selectedScriptStackId(self):
49 | try:
50 | return self._script_list[self._selected_script_index].getStackId()
51 | except:
52 | return ""
53 |
54 | ## Execute all post-processing scripts on the gcode.
55 | def execute(self, output_device):
56 | scene = Application.getInstance().getController().getScene()
57 | gcode_dict = getattr(scene, "gcode_dict")
58 | if not gcode_dict:
59 | return
60 |
61 | # get gcode list for the active build plate
62 | active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
63 | gcode_list = gcode_dict[active_build_plate_id]
64 | if not gcode_list:
65 | return
66 |
67 | if ";POSTPROCESSED" not in gcode_list[0]:
68 | for script in self._script_list:
69 | try:
70 | gcode_list = script.execute(gcode_list)
71 | except Exception:
72 | Logger.logException("e", "Exception in post-processing script.")
73 | if len(self._script_list): # Add comment to g-code if any changes were made.
74 | gcode_list[0] += ";POSTPROCESSED\n"
75 | gcode_dict[active_build_plate_id] = gcode_list
76 | setattr(scene, "gcode_dict", gcode_dict)
77 | else:
78 | Logger.log("e", "Already post processed")
79 |
80 | @pyqtSlot(int)
81 | def setSelectedScriptIndex(self, index):
82 | self._selected_script_index = index
83 | self.selectedIndexChanged.emit()
84 |
85 | @pyqtProperty(int, notify = selectedIndexChanged)
86 | def selectedScriptIndex(self):
87 | return self._selected_script_index
88 |
89 | @pyqtSlot(int, int)
90 | def moveScript(self, index, new_index):
91 | if new_index < 0 or new_index > len(self._script_list) - 1:
92 | return # nothing needs to be done
93 | else:
94 | # Magical switch code.
95 | self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index]
96 | self.scriptListChanged.emit()
97 | self.selectedIndexChanged.emit() #Ensure that settings are updated
98 | self._propertyChanged()
99 |
100 | ## Remove a script from the active script list by index.
101 | @pyqtSlot(int)
102 | def removeScriptByIndex(self, index):
103 | self._script_list.pop(index)
104 | if len(self._script_list) - 1 < self._selected_script_index:
105 | self._selected_script_index = len(self._script_list) - 1
106 | self.scriptListChanged.emit()
107 | self.selectedIndexChanged.emit() # Ensure that settings are updated
108 | self._propertyChanged()
109 |
110 | ## Load all scripts from provided path.
111 | # This should probably only be done on init.
112 | # \param path Path to check for scripts.
113 | def loadAllScripts(self, path):
114 | scripts = pkgutil.iter_modules(path = [path])
115 | for loader, script_name, ispkg in scripts:
116 | # Iterate over all scripts.
117 | if script_name not in sys.modules:
118 | spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
119 | loaded_script = importlib.util.module_from_spec(spec)
120 | spec.loader.exec_module(loaded_script)
121 | sys.modules[script_name] = loaded_script
122 |
123 | loaded_class = getattr(loaded_script, script_name)
124 | temp_object = loaded_class()
125 | Logger.log("d", "Begin loading of script: %s", script_name)
126 | try:
127 | setting_data = temp_object.getSettingData()
128 | if "name" in setting_data and "key" in setting_data:
129 | self._script_labels[setting_data["key"]] = setting_data["name"]
130 | self._loaded_scripts[setting_data["key"]] = loaded_class
131 | else:
132 | Logger.log("w", "Script %s.py has no name or key", script_name)
133 | self._script_labels[script_name] = script_name
134 | self._loaded_scripts[script_name] = loaded_class
135 | except AttributeError:
136 | Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
137 | except NotImplementedError:
138 | Logger.log("e", "Script %s.py has no implemented settings", script_name)
139 | self.loadedScriptListChanged.emit()
140 |
141 | loadedScriptListChanged = pyqtSignal()
142 | @pyqtProperty("QVariantList", notify = loadedScriptListChanged)
143 | def loadedScriptList(self):
144 | return sorted(list(self._loaded_scripts.keys()))
145 |
146 | @pyqtSlot(str, result = str)
147 | def getScriptLabelByKey(self, key):
148 | return self._script_labels[key]
149 |
150 | scriptListChanged = pyqtSignal()
151 | @pyqtProperty("QVariantList", notify = scriptListChanged)
152 | def scriptList(self):
153 | script_list = [script.getSettingData()["key"] for script in self._script_list]
154 | return script_list
155 |
156 | @pyqtSlot(str)
157 | def addScriptToList(self, key):
158 | Logger.log("d", "Adding script %s to list.", key)
159 | new_script = self._loaded_scripts[key]()
160 | self._script_list.append(new_script)
161 | self.setSelectedScriptIndex(len(self._script_list) - 1)
162 | self.scriptListChanged.emit()
163 | self._propertyChanged()
164 |
165 | ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
166 | def _createView(self):
167 | Logger.log("d", "Creating post processing plugin view.")
168 |
169 | ## Load all scripts in the scripts folders
170 | for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
171 | try:
172 | path = os.path.join(root, "scripts")
173 | if not os.path.isdir(path):
174 | try:
175 | os.makedirs(path)
176 | except OSError:
177 | Logger.log("w", "Unable to create a folder for scripts: " + path)
178 | continue
179 |
180 | self.loadAllScripts(path)
181 | except Exception as e:
182 | Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
183 |
184 | # Create the plugin dialog component
185 | path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
186 | self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
187 | Logger.log("d", "Post processing view created.")
188 |
189 | # Create the save button component
190 | Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
191 |
192 | ## Show the (GUI) popup of the post processing plugin.
193 | def showPopup(self):
194 | if self._view is None:
195 | self._createView()
196 | self._view.show()
197 |
198 | ## Property changed: trigger re-slice
199 | # To do this we use the global container stack propertyChanged.
200 | # Re-slicing is necessary for setting changes in this plugin, because the changes
201 | # are applied only once per "fresh" gcode
202 | def _propertyChanged(self):
203 | global_container_stack = Application.getInstance().getGlobalContainerStack()
204 | global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
205 |
206 |
207 |
--------------------------------------------------------------------------------
/PostProcessingPlugin.qml:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
2 | // The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
3 |
4 | import QtQuick 2.2
5 | import QtQuick.Controls 1.1
6 | import QtQuick.Controls.Styles 1.1
7 | import QtQuick.Layouts 1.1
8 | import QtQuick.Dialogs 1.1
9 | import QtQuick.Window 2.2
10 |
11 | import UM 1.2 as UM
12 | import Cura 1.0 as Cura
13 |
14 | UM.Dialog
15 | {
16 | id: dialog
17 |
18 | title: catalog.i18nc("@title:window", "Post Processing Plugin")
19 | width: 700 * screenScaleFactor;
20 | height: 500 * screenScaleFactor;
21 | minimumWidth: 400 * screenScaleFactor;
22 | minimumHeight: 250 * screenScaleFactor;
23 |
24 | Item
25 | {
26 | UM.I18nCatalog{id: catalog; name:"cura"}
27 | id: base
28 | property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width)
29 | property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
30 | property string activeScriptName
31 | SystemPalette{ id: palette }
32 | SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled }
33 | anchors.fill: parent
34 |
35 | ExclusiveGroup
36 | {
37 | id: selectedScriptGroup
38 | }
39 | Item
40 | {
41 | id: activeScripts
42 | anchors.left: parent.left
43 | width: base.columnWidth
44 | height: parent.height
45 |
46 | Label
47 | {
48 | id: activeScriptsHeader
49 | text: catalog.i18nc("@label", "Post Processing Scripts")
50 | anchors.top: parent.top
51 | anchors.topMargin: base.textMargin
52 | anchors.left: parent.left
53 | anchors.leftMargin: base.textMargin
54 | anchors.right: parent.right
55 | anchors.rightMargin: base.textMargin
56 | font: UM.Theme.getFont("large")
57 | }
58 | ListView
59 | {
60 | id: activeScriptsList
61 | anchors.top: activeScriptsHeader.bottom
62 | anchors.topMargin: base.textMargin
63 | anchors.left: parent.left
64 | anchors.leftMargin: UM.Theme.getSize("default_margin").width
65 | anchors.right: parent.right
66 | anchors.rightMargin: base.textMargin
67 | height: childrenRect.height
68 | model: manager.scriptList
69 | delegate: Item
70 | {
71 | width: parent.width
72 | height: activeScriptButton.height
73 | Button
74 | {
75 | id: activeScriptButton
76 | text: manager.getScriptLabelByKey(modelData.toString())
77 | exclusiveGroup: selectedScriptGroup
78 | checkable: true
79 | checked: {
80 | if (manager.selectedScriptIndex == index)
81 | {
82 | base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
83 | return true
84 | }
85 | else
86 | {
87 | return false
88 | }
89 | }
90 | onClicked:
91 | {
92 | forceActiveFocus()
93 | manager.setSelectedScriptIndex(index)
94 | base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
95 | }
96 | width: parent.width
97 | height: UM.Theme.getSize("setting").height
98 | style: ButtonStyle
99 | {
100 | background: Rectangle
101 | {
102 | color: activeScriptButton.checked ? palette.highlight : "transparent"
103 | width: parent.width
104 | height: parent.height
105 | }
106 | label: Label
107 | {
108 | wrapMode: Text.Wrap
109 | text: control.text
110 | color: activeScriptButton.checked ? palette.highlightedText : palette.text
111 | }
112 | }
113 | }
114 | Button
115 | {
116 | id: removeButton
117 | text: "x"
118 | width: 20 * screenScaleFactor
119 | height: 20 * screenScaleFactor
120 | anchors.right:parent.right
121 | anchors.rightMargin: base.textMargin
122 | anchors.verticalCenter: parent.verticalCenter
123 | onClicked: manager.removeScriptByIndex(index)
124 | style: ButtonStyle
125 | {
126 | label: Item
127 | {
128 | UM.RecolorImage
129 | {
130 | anchors.verticalCenter: parent.verticalCenter
131 | anchors.horizontalCenter: parent.horizontalCenter
132 | width: Math.floor(control.width / 2.7)
133 | height: Math.floor(control.height / 2.7)
134 | sourceSize.width: width
135 | sourceSize.height: width
136 | color: palette.text
137 | source: UM.Theme.getIcon("cross1")
138 | }
139 | }
140 | }
141 | }
142 | Button
143 | {
144 | id: downButton
145 | text: ""
146 | anchors.right: removeButton.left
147 | anchors.verticalCenter: parent.verticalCenter
148 | enabled: index != manager.scriptList.length - 1
149 | width: 20 * screenScaleFactor
150 | height: 20 * screenScaleFactor
151 | onClicked:
152 | {
153 | if (manager.selectedScriptIndex == index)
154 | {
155 | manager.setSelectedScriptIndex(index + 1)
156 | }
157 | return manager.moveScript(index, index + 1)
158 | }
159 | style: ButtonStyle
160 | {
161 | label: Item
162 | {
163 | UM.RecolorImage
164 | {
165 | anchors.verticalCenter: parent.verticalCenter
166 | anchors.horizontalCenter: parent.horizontalCenter
167 | width: Math.floor(control.width / 2.5)
168 | height: Math.floor(control.height / 2.5)
169 | sourceSize.width: width
170 | sourceSize.height: width
171 | color: control.enabled ? palette.text : disabledPalette.text
172 | source: UM.Theme.getIcon("arrow_bottom")
173 | }
174 | }
175 | }
176 | }
177 | Button
178 | {
179 | id: upButton
180 | text: ""
181 | enabled: index != 0
182 | width: 20 * screenScaleFactor
183 | height: 20 * screenScaleFactor
184 | anchors.right: downButton.left
185 | anchors.verticalCenter: parent.verticalCenter
186 | onClicked:
187 | {
188 | if (manager.selectedScriptIndex == index)
189 | {
190 | manager.setSelectedScriptIndex(index - 1)
191 | }
192 | return manager.moveScript(index, index - 1)
193 | }
194 | style: ButtonStyle
195 | {
196 | label: Item
197 | {
198 | UM.RecolorImage
199 | {
200 | anchors.verticalCenter: parent.verticalCenter
201 | anchors.horizontalCenter: parent.horizontalCenter
202 | width: Math.floor(control.width / 2.5)
203 | height: Math.floor(control.height / 2.5)
204 | sourceSize.width: width
205 | sourceSize.height: width
206 | color: control.enabled ? palette.text : disabledPalette.text
207 | source: UM.Theme.getIcon("arrow_top")
208 | }
209 | }
210 | }
211 | }
212 | }
213 | }
214 | Button
215 | {
216 | id: addButton
217 | text: catalog.i18nc("@action", "Add a script")
218 | anchors.left: parent.left
219 | anchors.leftMargin: base.textMargin
220 | anchors.top: activeScriptsList.bottom
221 | anchors.topMargin: base.textMargin
222 | menu: scriptsMenu
223 | style: ButtonStyle
224 | {
225 | label: Label
226 | {
227 | text: control.text
228 | }
229 | }
230 | }
231 | Menu
232 | {
233 | id: scriptsMenu
234 |
235 | Instantiator
236 | {
237 | model: manager.loadedScriptList
238 |
239 | MenuItem
240 | {
241 | text: manager.getScriptLabelByKey(modelData.toString())
242 | onTriggered: manager.addScriptToList(modelData.toString())
243 | }
244 |
245 | onObjectAdded: scriptsMenu.insertItem(index, object);
246 | onObjectRemoved: scriptsMenu.removeItem(object);
247 | }
248 | }
249 | }
250 |
251 | Rectangle
252 | {
253 | color: UM.Theme.getColor("sidebar")
254 | anchors.left: activeScripts.right
255 | anchors.leftMargin: UM.Theme.getSize("default_margin").width
256 | anchors.right: parent.right
257 | height: parent.height
258 | id: settingsPanel
259 |
260 | Label
261 | {
262 | id: scriptSpecsHeader
263 | text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
264 | anchors.top: parent.top
265 | anchors.topMargin: base.textMargin
266 | anchors.left: parent.left
267 | anchors.leftMargin: base.textMargin
268 | anchors.right: parent.right
269 | anchors.rightMargin: base.textMargin
270 | height: 20 * screenScaleFactor
271 | font: UM.Theme.getFont("large")
272 | color: UM.Theme.getColor("text")
273 | }
274 |
275 | ScrollView
276 | {
277 | id: scrollView
278 | anchors.top: scriptSpecsHeader.bottom
279 | anchors.topMargin: settingsPanel.textMargin
280 | anchors.left: parent.left
281 | anchors.right: parent.right
282 | anchors.bottom: parent.bottom
283 | visible: manager.selectedScriptDefinitionId != ""
284 | style: UM.Theme.styles.scrollview;
285 |
286 | ListView
287 | {
288 | id: listview
289 | spacing: UM.Theme.getSize("default_lining").height
290 | model: UM.SettingDefinitionsModel
291 | {
292 | id: definitionsModel;
293 | containerId: manager.selectedScriptDefinitionId
294 | showAll: true
295 | }
296 | delegate:Loader
297 | {
298 | id: settingLoader
299 |
300 | width: parent.width
301 | height:
302 | {
303 | if(provider.properties.enabled == "True")
304 | {
305 | if(model.type != undefined)
306 | {
307 | return UM.Theme.getSize("section").height;
308 | }
309 | else
310 | {
311 | return 0;
312 | }
313 | }
314 | else
315 | {
316 | return 0;
317 | }
318 |
319 | }
320 | Behavior on height { NumberAnimation { duration: 100 } }
321 | opacity: provider.properties.enabled == "True" ? 1 : 0
322 | Behavior on opacity { NumberAnimation { duration: 100 } }
323 | enabled: opacity > 0
324 | property var definition: model
325 | property var settingDefinitionsModel: definitionsModel
326 | property var propertyProvider: provider
327 | property var globalPropertyProvider: inheritStackProvider
328 |
329 | //Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
330 | //In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
331 | //causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
332 | asynchronous: model.type != "enum" && model.type != "extruder"
333 |
334 | onLoaded: {
335 | settingLoader.item.showRevertButton = false
336 | settingLoader.item.showInheritButton = false
337 | settingLoader.item.showLinkedSettingIcon = false
338 | settingLoader.item.doDepthIndentation = true
339 | settingLoader.item.doQualityUserSettingEmphasis = false
340 | }
341 |
342 | sourceComponent:
343 | {
344 | switch(model.type)
345 | {
346 | case "int":
347 | return settingTextField
348 | case "float":
349 | return settingTextField
350 | case "enum":
351 | return settingComboBox
352 | case "extruder":
353 | return settingExtruder
354 | case "bool":
355 | return settingCheckBox
356 | case "str":
357 | return settingTextField
358 | case "category":
359 | return settingCategory
360 | default:
361 | return settingUnknown
362 | }
363 | }
364 |
365 | UM.SettingPropertyProvider
366 | {
367 | id: provider
368 | containerStackId: manager.selectedScriptStackId
369 | key: model.key ? model.key : "None"
370 | watchedProperties: [ "value", "enabled", "state", "validationState" ]
371 | storeIndex: 0
372 | }
373 |
374 | // Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
375 | // so we bypass that to make a dedicated provider).
376 | UM.SettingPropertyProvider
377 | {
378 | id: inheritStackProvider
379 | containerStackId: Cura.MachineManager.activeMachineId
380 | key: model.key ? model.key : "None"
381 | watchedProperties: [ "limit_to_extruder" ]
382 | }
383 |
384 | Connections
385 | {
386 | target: item
387 |
388 | onShowTooltip:
389 | {
390 | tooltip.text = text;
391 | var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
392 | tooltip.show(position);
393 | tooltip.target.x = position.x + 1
394 | }
395 |
396 | onHideTooltip:
397 | {
398 | tooltip.hide();
399 | }
400 | }
401 |
402 | }
403 | }
404 | }
405 | }
406 |
407 | Cura.SidebarTooltip
408 | {
409 | id: tooltip
410 | }
411 |
412 | Component
413 | {
414 | id: settingTextField;
415 |
416 | Cura.SettingTextField { }
417 | }
418 |
419 | Component
420 | {
421 | id: settingComboBox;
422 |
423 | Cura.SettingComboBox { }
424 | }
425 |
426 | Component
427 | {
428 | id: settingExtruder;
429 |
430 | Cura.SettingExtruder { }
431 | }
432 |
433 | Component
434 | {
435 | id: settingCheckBox;
436 |
437 | Cura.SettingCheckBox { }
438 | }
439 |
440 | Component
441 | {
442 | id: settingCategory;
443 |
444 | Cura.SettingCategory { }
445 | }
446 |
447 | Component
448 | {
449 | id: settingUnknown;
450 |
451 | Cura.SettingUnknown { }
452 | }
453 | }
454 | rightButtons: Button
455 | {
456 | text: catalog.i18nc("@action:button", "Close")
457 | iconName: "dialog-close"
458 | onClicked: dialog.accept()
459 | }
460 |
461 | Button {
462 | objectName: "postProcessingSaveAreaButton"
463 | visible: activeScriptsList.count > 0
464 | height: UM.Theme.getSize("save_button_save_to_button").height
465 | width: height
466 | tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
467 | onClicked: dialog.show()
468 |
469 | style: ButtonStyle {
470 | background: Rectangle {
471 | id: deviceSelectionIcon
472 | border.width: UM.Theme.getSize("default_lining").width
473 | border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
474 | control.pressed ? UM.Theme.getColor("action_button_active_border") :
475 | control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
476 | color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
477 | control.pressed ? UM.Theme.getColor("action_button_active") :
478 | control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
479 | Behavior on color { ColorAnimation { duration: 50; } }
480 | anchors.left: parent.left
481 | anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2);
482 | width: parent.height
483 | height: parent.height
484 |
485 | UM.RecolorImage {
486 | anchors.verticalCenter: parent.verticalCenter
487 | anchors.horizontalCenter: parent.horizontalCenter
488 | width: Math.floor(parent.width / 2)
489 | height: Math.floor(parent.height / 2)
490 | sourceSize.width: width
491 | sourceSize.height: height
492 | color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
493 | control.pressed ? UM.Theme.getColor("action_button_active_text") :
494 | control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
495 | source: "postprocessing.svg"
496 | }
497 | }
498 | label: Label{ }
499 | }
500 | }
501 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PostProcessingPlugin
2 | A post processing plugin for Cura
3 |
--------------------------------------------------------------------------------
/Script.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015 Jaime van Kessel
2 | # Copyright (c) 2017 Ultimaker B.V.
3 | # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
4 | from UM.Logger import Logger
5 | from UM.Signal import Signal, signalemitter
6 | from UM.i18n import i18nCatalog
7 |
8 | # Setting stuff import
9 | from UM.Application import Application
10 | from UM.Settings.ContainerStack import ContainerStack
11 | from UM.Settings.InstanceContainer import InstanceContainer
12 | from UM.Settings.DefinitionContainer import DefinitionContainer
13 | from UM.Settings.ContainerRegistry import ContainerRegistry
14 |
15 | import re
16 | import json
17 | import collections
18 | i18n_catalog = i18nCatalog("cura")
19 |
20 |
21 | ## Base class for scripts. All scripts should inherit the script class.
22 | @signalemitter
23 | class Script:
24 | def __init__(self):
25 | super().__init__()
26 | self._settings = None
27 | self._stack = None
28 |
29 | setting_data = self.getSettingData()
30 | self._stack = ContainerStack(stack_id = str(id(self)))
31 | self._stack.setDirty(False) # This stack does not need to be saved.
32 |
33 |
34 | ## Check if the definition of this script already exists. If not, add it to the registry.
35 | if "key" in setting_data:
36 | definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"])
37 | if definitions:
38 | # Definition was found
39 | self._definition = definitions[0]
40 | else:
41 | self._definition = DefinitionContainer(setting_data["key"])
42 | self._definition.deserialize(json.dumps(setting_data))
43 | ContainerRegistry.getInstance().addContainer(self._definition)
44 | self._stack.addContainer(self._definition)
45 | self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
46 | self._instance.setDefinition(self._definition.getId())
47 | self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0))
48 | self._stack.addContainer(self._instance)
49 | self._stack.propertyChanged.connect(self._onPropertyChanged)
50 |
51 | ContainerRegistry.getInstance().addContainer(self._stack)
52 |
53 | settingsLoaded = Signal()
54 | valueChanged = Signal() # Signal emitted whenever a value of a setting is changed
55 |
56 | def _onPropertyChanged(self, key, property_name):
57 | if property_name == "value":
58 | self.valueChanged.emit()
59 |
60 | # Property changed: trigger reslice
61 | # To do this we use the global container stack propertyChanged.
62 | # Reslicing is necessary for setting changes in this plugin, because the changes
63 | # are applied only once per "fresh" gcode
64 | global_container_stack = Application.getInstance().getGlobalContainerStack()
65 | global_container_stack.propertyChanged.emit(key, property_name)
66 |
67 | ## Needs to return a dict that can be used to construct a settingcategory file.
68 | # See the example script for an example.
69 | # It follows the same style / guides as the Uranium settings.
70 | # Scripts can either override getSettingData directly, or use getSettingDataString
71 | # to return a string that will be parsed as json. The latter has the benefit over
72 | # returning a dict in that the order of settings is maintained.
73 | def getSettingData(self):
74 | setting_data = self.getSettingDataString()
75 | if type(setting_data) == str:
76 | setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
77 | return setting_data
78 |
79 | def getSettingDataString(self):
80 | raise NotImplementedError()
81 |
82 | def getDefinitionId(self):
83 | if self._stack:
84 | return self._stack.getBottom().getId()
85 |
86 | def getStackId(self):
87 | if self._stack:
88 | return self._stack.getId()
89 |
90 | ## Convenience function that retrieves value of a setting from the stack.
91 | def getSettingValueByKey(self, key):
92 | return self._stack.getProperty(key, "value")
93 |
94 | ## Convenience function that finds the value in a line of g-code.
95 | # When requesting key = x from line "G1 X100" the value 100 is returned.
96 | def getValue(self, line, key, default = None):
97 | if not key in line or (';' in line and line.find(key) > line.find(';')):
98 | return default
99 | sub_part = line[line.find(key) + 1:]
100 | m = re.search('^-?[0-9]+\.?[0-9]*', sub_part)
101 | if m is None:
102 | return default
103 | try:
104 | return float(m.group(0))
105 | except:
106 | return default
107 |
108 | ## This is called when the script is executed.
109 | # It gets a list of g-code strings and needs to return a (modified) list.
110 | def execute(self, data):
111 | raise NotImplementedError()
112 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
2 | # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
3 |
4 | from . import PostProcessingPlugin
5 | from UM.i18n import i18nCatalog
6 | catalog = i18nCatalog("cura")
7 | def getMetaData():
8 | return {}
9 |
10 | def register(app):
11 | return {"extension": PostProcessingPlugin.PostProcessingPlugin()}
--------------------------------------------------------------------------------
/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Post Processing",
3 | "author": "Ultimaker",
4 | "version": "2.2",
5 | "api": 4,
6 | "description": "Extension that allows for user created scripts for post processing",
7 | "catalog": "cura"
8 | }
--------------------------------------------------------------------------------
/postprocessing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/scripts/BQ_PauseAtHeight.py:
--------------------------------------------------------------------------------
1 | from ..Script import Script
2 | class BQ_PauseAtHeight(Script):
3 | def __init__(self):
4 | super().__init__()
5 |
6 | def getSettingDataString(self):
7 | return """{
8 | "name":"Pause at height (BQ Printers)",
9 | "key": "BQ_PauseAtHeight",
10 | "metadata":{},
11 | "version": 2,
12 | "settings":
13 | {
14 | "pause_height":
15 | {
16 | "label": "Pause height",
17 | "description": "At what height should the pause occur",
18 | "unit": "mm",
19 | "type": "float",
20 | "default_value": 5.0
21 | }
22 | }
23 | }"""
24 |
25 | def execute(self, data):
26 | x = 0.
27 | y = 0.
28 | current_z = 0.
29 | pause_z = self.getSettingValueByKey("pause_height")
30 | for layer in data:
31 | lines = layer.split("\n")
32 | for line in lines:
33 | if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
34 | current_z = self.getValue(line, 'Z')
35 | if current_z != None:
36 | if current_z >= pause_z:
37 | prepend_gcode = ";TYPE:CUSTOM\n"
38 | prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
39 |
40 | # Insert Pause gcode
41 | prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n"
42 |
43 | index = data.index(layer)
44 | layer = prepend_gcode + layer
45 | data[index] = layer # Override the data of this layer with the modified data
46 | return data
47 | break
48 | return data
49 |
--------------------------------------------------------------------------------
/scripts/ColorChange.py:
--------------------------------------------------------------------------------
1 | # This PostProcessing Plugin script is released
2 | # under the terms of the AGPLv3 or higher
3 |
4 | from ..Script import Script
5 | #from UM.Logger import Logger
6 | # from cura.Settings.ExtruderManager import ExtruderManager
7 |
8 | class ColorChange(Script):
9 | def __init__(self):
10 | super().__init__()
11 |
12 | def getSettingDataString(self):
13 | return """{
14 | "name":"Color Change",
15 | "key": "ColorChange",
16 | "metadata": {},
17 | "version": 2,
18 | "settings":
19 | {
20 | "layer_number":
21 | {
22 | "label": "Layer",
23 | "description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.",
24 | "unit": "",
25 | "type": "str",
26 | "default_value": "1"
27 | },
28 |
29 | "initial_retract":
30 | {
31 | "label": "Initial Retraction",
32 | "description": "Initial filament retraction distance",
33 | "unit": "mm",
34 | "type": "float",
35 | "default_value": 300.0
36 | },
37 | "later_retract":
38 | {
39 | "label": "Later Retraction Distance",
40 | "description": "Later filament retraction distance for removal",
41 | "unit": "mm",
42 | "type": "float",
43 | "default_value": 30.0
44 | }
45 | }
46 | }"""
47 |
48 | def execute(self, data: list):
49 |
50 | """data is a list. Each index contains a layer"""
51 | layer_nums = self.getSettingValueByKey("layer_number")
52 | initial_retract = self.getSettingValueByKey("initial_retract")
53 | later_retract = self.getSettingValueByKey("later_retract")
54 |
55 | color_change = "M600"
56 |
57 | if initial_retract is not None and initial_retract > 0.:
58 | color_change = color_change + (" E%.2f" % initial_retract)
59 |
60 | if later_retract is not None and later_retract > 0.:
61 | color_change = color_change + (" L%.2f" % later_retract)
62 |
63 | color_change = color_change + " ; Generated by ColorChange plugin"
64 |
65 | layer_targets = layer_nums.split(',')
66 | if len(layer_targets) > 0:
67 | for layer_num in layer_targets:
68 | layer_num = int( layer_num.strip() )
69 | if layer_num < len(data):
70 | layer = data[ layer_num - 1 ]
71 | lines = layer.split("\n")
72 | lines.insert(2, color_change )
73 | final_line = "\n".join( lines )
74 | data[ layer_num - 1 ] = final_line
75 |
76 | return data
77 |
--------------------------------------------------------------------------------
/scripts/ExampleScript.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
2 | # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
3 | from ..Script import Script
4 |
5 | class ExampleScript(Script):
6 | def __init__(self):
7 | super().__init__()
8 |
9 | def getSettingDataString(self):
10 | return """{
11 | "name":"Example script",
12 | "key": "ExampleScript",
13 | "metadata": {},
14 | "version": 2,
15 | "settings":
16 | {
17 | "test":
18 | {
19 | "label": "Test",
20 | "description": "None",
21 | "unit": "mm",
22 | "type": "float",
23 | "default_value": 0.5,
24 | "minimum_value": "0",
25 | "minimum_value_warning": "0.1",
26 | "maximum_value_warning": "1"
27 | },
28 | "derp":
29 | {
30 | "label": "zomg",
31 | "description": "afgasgfgasfgasf",
32 | "unit": "mm",
33 | "type": "float",
34 | "default_value": 0.5,
35 | "minimum_value": "0",
36 | "minimum_value_warning": "0.1",
37 | "maximum_value_warning": "1"
38 | }
39 | }
40 | }"""
41 |
42 | def execute(self, data):
43 | return data
--------------------------------------------------------------------------------
/scripts/PauseAtHeight.py:
--------------------------------------------------------------------------------
1 | from ..Script import Script
2 | # from cura.Settings.ExtruderManager import ExtruderManager
3 |
4 | class PauseAtHeight(Script):
5 | def __init__(self):
6 | super().__init__()
7 |
8 | def getSettingDataString(self):
9 | return """{
10 | "name":"Pause at height",
11 | "key": "PauseAtHeight",
12 | "metadata": {},
13 | "version": 2,
14 | "settings":
15 | {
16 | "pause_height":
17 | {
18 | "label": "Pause Height",
19 | "description": "At what height should the pause occur",
20 | "unit": "mm",
21 | "type": "float",
22 | "default_value": 5.0
23 | },
24 | "head_park_x":
25 | {
26 | "label": "Park Print Head X",
27 | "description": "What X location does the head move to when pausing.",
28 | "unit": "mm",
29 | "type": "float",
30 | "default_value": 190
31 | },
32 | "head_park_y":
33 | {
34 | "label": "Park Print Head Y",
35 | "description": "What Y location does the head move to when pausing.",
36 | "unit": "mm",
37 | "type": "float",
38 | "default_value": 190
39 | },
40 | "retraction_amount":
41 | {
42 | "label": "Retraction",
43 | "description": "How much filament must be retracted at pause.",
44 | "unit": "mm",
45 | "type": "float",
46 | "default_value": 0
47 | },
48 | "retraction_speed":
49 | {
50 | "label": "Retraction Speed",
51 | "description": "How fast to retract the filament.",
52 | "unit": "mm/s",
53 | "type": "float",
54 | "default_value": 25
55 | },
56 | "extrude_amount":
57 | {
58 | "label": "Extrude Amount",
59 | "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
60 | "unit": "mm",
61 | "type": "float",
62 | "default_value": 0
63 | },
64 | "extrude_speed":
65 | {
66 | "label": "Extrude Speed",
67 | "description": "How fast to extrude the material after pause.",
68 | "unit": "mm/s",
69 | "type": "float",
70 | "default_value": 3.3333
71 | },
72 | "redo_layers":
73 | {
74 | "label": "Redo Layers",
75 | "description": "Redo a number of previous layers after a pause to increases adhesion.",
76 | "unit": "layers",
77 | "type": "int",
78 | "default_value": 0
79 | },
80 | "standby_temperature":
81 | {
82 | "label": "Standby Temperature",
83 | "description": "Change the temperature during the pause",
84 | "unit": "°C",
85 | "type": "int",
86 | "default_value": 0
87 | },
88 | "resume_temperature":
89 | {
90 | "label": "Resume Temperature",
91 | "description": "Change the temperature after the pause",
92 | "unit": "°C",
93 | "type": "int",
94 | "default_value": 0
95 | }
96 | }
97 | }"""
98 |
99 | def execute(self, data: list):
100 |
101 | """data is a list. Each index contains a layer"""
102 |
103 | x = 0.
104 | y = 0.
105 | current_z = 0.
106 | pause_height = self.getSettingValueByKey("pause_height")
107 | retraction_amount = self.getSettingValueByKey("retraction_amount")
108 | retraction_speed = self.getSettingValueByKey("retraction_speed")
109 | extrude_amount = self.getSettingValueByKey("extrude_amount")
110 | extrude_speed = self.getSettingValueByKey("extrude_speed")
111 | park_x = self.getSettingValueByKey("head_park_x")
112 | park_y = self.getSettingValueByKey("head_park_y")
113 | layers_started = False
114 | redo_layers = self.getSettingValueByKey("redo_layers")
115 | standby_temperature = self.getSettingValueByKey("standby_temperature")
116 | resume_temperature = self.getSettingValueByKey("resume_temperature")
117 |
118 | # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
119 | # with open("out.txt", "w") as f:
120 | # f.write(T)
121 |
122 | # use offset to calculate the current height: = -
123 | layer_0_z = 0.
124 | got_first_g_cmd_on_layer_0 = False
125 | for layer in data:
126 | lines = layer.split("\n")
127 | for line in lines:
128 | if ";LAYER:0" in line:
129 | layers_started = True
130 | continue
131 |
132 | if not layers_started:
133 | continue
134 |
135 | if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
136 | current_z = self.getValue(line, 'Z')
137 | if not got_first_g_cmd_on_layer_0:
138 | layer_0_z = current_z
139 | got_first_g_cmd_on_layer_0 = True
140 |
141 | x = self.getValue(line, 'X', x)
142 | y = self.getValue(line, 'Y', y)
143 | if current_z is not None:
144 | current_height = current_z - layer_0_z
145 | if current_height >= pause_height:
146 | index = data.index(layer)
147 | prevLayer = data[index - 1]
148 | prevLines = prevLayer.split("\n")
149 | current_e = 0.
150 | for prevLine in reversed(prevLines):
151 | current_e = self.getValue(prevLine, 'E', -1)
152 | if current_e >= 0:
153 | break
154 |
155 | # include a number of previous layers
156 | for i in range(1, redo_layers + 1):
157 | prevLayer = data[index - i]
158 | layer = prevLayer + layer
159 |
160 | prepend_gcode = ";TYPE:CUSTOM\n"
161 | prepend_gcode += ";added code by post processing\n"
162 | prepend_gcode += ";script: PauseAtHeight.py\n"
163 | prepend_gcode += ";current z: %f \n" % current_z
164 | prepend_gcode += ";current height: %f \n" % current_height
165 |
166 | # Retraction
167 | prepend_gcode += "M83\n"
168 | if retraction_amount != 0:
169 | prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
170 |
171 | # Move the head away
172 | prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
173 | prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
174 | if current_z < 15:
175 | prepend_gcode += "G1 Z15 F300\n"
176 |
177 | # Disable the E steppers
178 | prepend_gcode += "M84 E0\n"
179 |
180 | # Set extruder standby temperature
181 | prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature)
182 |
183 | # Wait till the user continues printing
184 | prepend_gcode += "M0 ;Do the actual pause\n"
185 |
186 | # Set extruder resume temperature
187 | prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature)
188 |
189 | # Push the filament back,
190 | if retraction_amount != 0:
191 | prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
192 |
193 | # Optionally extrude material
194 | if extrude_amount != 0:
195 | prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60)
196 |
197 | # and retract again, the properly primes the nozzle
198 | # when changing filament.
199 | if retraction_amount != 0:
200 | prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
201 |
202 | # Move the head back
203 | prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
204 | prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y)
205 | if retraction_amount != 0:
206 | prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
207 | prepend_gcode += "G1 F9000\n"
208 | prepend_gcode += "M82\n"
209 |
210 | # reset extrude value to pre pause value
211 | prepend_gcode += "G92 E%f\n" % (current_e)
212 |
213 | layer = prepend_gcode + layer
214 |
215 |
216 | # Override the data of this layer with the
217 | # modified data
218 | data[index] = layer
219 | return data
220 | break
221 | return data
222 |
--------------------------------------------------------------------------------
/scripts/PauseAtHeightforRepetier.py:
--------------------------------------------------------------------------------
1 | from ..Script import Script
2 | class PauseAtHeightforRepetier(Script):
3 | def __init__(self):
4 | super().__init__()
5 |
6 | def getSettingDataString(self):
7 | return """{
8 | "name":"Pause at height for repetier",
9 | "key": "PauseAtHeightforRepetier",
10 | "metadata": {},
11 | "version": 2,
12 | "settings":
13 | {
14 | "pause_height":
15 | {
16 | "label": "Pause height",
17 | "description": "At what height should the pause occur",
18 | "unit": "mm",
19 | "type": "float",
20 | "default_value": 5.0
21 | },
22 | "head_park_x":
23 | {
24 | "label": "Park print head X",
25 | "description": "What x location does the head move to when pausing.",
26 | "unit": "mm",
27 | "type": "float",
28 | "default_value": 5.0
29 | },
30 | "head_park_y":
31 | {
32 | "label": "Park print head Y",
33 | "description": "What y location does the head move to when pausing.",
34 | "unit": "mm",
35 | "type": "float",
36 | "default_value": 5.0
37 | },
38 | "head_move_Z":
39 | {
40 | "label": "Head move Z",
41 | "description": "The Hieght of Z-axis retraction before parking.",
42 | "unit": "mm",
43 | "type": "float",
44 | "default_value": 15.0
45 | },
46 | "retraction_amount":
47 | {
48 | "label": "Retraction",
49 | "description": "How much fillament must be retracted at pause.",
50 | "unit": "mm",
51 | "type": "float",
52 | "default_value": 5.0
53 | },
54 | "extrude_amount":
55 | {
56 | "label": "Extrude amount",
57 | "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
58 | "unit": "mm",
59 | "type": "float",
60 | "default_value": 90.0
61 | },
62 | "redo_layers":
63 | {
64 | "label": "Redo layers",
65 | "description": "Redo a number of previous layers after a pause to increases adhesion.",
66 | "unit": "layers",
67 | "type": "int",
68 | "default_value": 0
69 | }
70 | }
71 | }"""
72 |
73 | def execute(self, data):
74 | x = 0.
75 | y = 0.
76 | current_z = 0.
77 | pause_z = self.getSettingValueByKey("pause_height")
78 | retraction_amount = self.getSettingValueByKey("retraction_amount")
79 | extrude_amount = self.getSettingValueByKey("extrude_amount")
80 | park_x = self.getSettingValueByKey("head_park_x")
81 | park_y = self.getSettingValueByKey("head_park_y")
82 | move_Z = self.getSettingValueByKey("head_move_Z")
83 | layers_started = False
84 | redo_layers = self.getSettingValueByKey("redo_layers")
85 | for layer in data:
86 | lines = layer.split("\n")
87 | for line in lines:
88 | if ";LAYER:0" in line:
89 | layers_started = True
90 | continue
91 |
92 | if not layers_started:
93 | continue
94 |
95 | if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
96 | current_z = self.getValue(line, 'Z')
97 | x = self.getValue(line, 'X', x)
98 | y = self.getValue(line, 'Y', y)
99 | if current_z != None:
100 | if current_z >= pause_z:
101 |
102 | index = data.index(layer)
103 | prevLayer = data[index-1]
104 | prevLines = prevLayer.split("\n")
105 | current_e = 0.
106 | for prevLine in reversed(prevLines):
107 | current_e = self.getValue(prevLine, 'E', -1)
108 | if current_e >= 0:
109 | break
110 |
111 | prepend_gcode = ";TYPE:CUSTOM\n"
112 | prepend_gcode += ";added code by post processing\n"
113 | prepend_gcode += ";script: PauseAtHeightforRepetier.py\n"
114 | prepend_gcode += ";current z: %f \n" % (current_z)
115 | prepend_gcode += ";current X: %f \n" % (x)
116 | prepend_gcode += ";current Y: %f \n" % (y)
117 |
118 | #Retraction
119 | prepend_gcode += "M83\n"
120 | if retraction_amount != 0:
121 | prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
122 |
123 | #Move the head away
124 | prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
125 | prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
126 | if current_z < move_Z:
127 | prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z)
128 |
129 | #Disable the E steppers
130 | prepend_gcode += "M84 E0\n"
131 | #Wait till the user continues printing
132 | prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n"
133 |
134 | #Push the filament back,
135 | if retraction_amount != 0:
136 | prepend_gcode += "G1 E%f F6000\n" % (retraction_amount)
137 |
138 | # Optionally extrude material
139 | if extrude_amount != 0:
140 | prepend_gcode += "G1 E%f F200\n" % (extrude_amount)
141 | prepend_gcode += "@info wait for cleaning nozzle from previous filament\n"
142 | prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n"
143 |
144 | # and retract again, the properly primes the nozzle when changing filament.
145 | if retraction_amount != 0:
146 | prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
147 |
148 | #Move the head back
149 | prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
150 | prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y)
151 | if retraction_amount != 0:
152 | prepend_gcode +="G1 E%f F6000\n" % (retraction_amount)
153 | prepend_gcode +="G1 F9000\n"
154 | prepend_gcode +="M82\n"
155 |
156 | # reset extrude value to pre pause value
157 | prepend_gcode +="G92 E%f\n" % (current_e)
158 |
159 | layer = prepend_gcode + layer
160 |
161 | # include a number of previous layers
162 | for i in range(1, redo_layers + 1):
163 | prevLayer = data[index-i]
164 | layer = prevLayer + layer
165 |
166 | data[index] = layer #Override the data of this layer with the modified data
167 | return data
168 | break
169 | return data
170 |
--------------------------------------------------------------------------------
/scripts/SearchAndReplace.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2017 Ruben Dulek
2 | # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
3 |
4 | import re #To perform the search and replace.
5 |
6 | from ..Script import Script
7 |
8 | ## Performs a search-and-replace on all g-code.
9 | #
10 | # Due to technical limitations, the search can't cross the border between
11 | # layers.
12 | class SearchAndReplace(Script):
13 | def getSettingDataString(self):
14 | return """{
15 | "name": "Search and Replace",
16 | "key": "SearchAndReplace",
17 | "metadata": {},
18 | "version": 2,
19 | "settings":
20 | {
21 | "search":
22 | {
23 | "label": "Search",
24 | "description": "All occurrences of this text will get replaced by the replacement text.",
25 | "type": "str",
26 | "default_value": ""
27 | },
28 | "replace":
29 | {
30 | "label": "Replace",
31 | "description": "The search text will get replaced by this text.",
32 | "type": "str",
33 | "default_value": ""
34 | },
35 | "is_regex":
36 | {
37 | "label": "Use Regular Expressions",
38 | "description": "When enabled, the search text will be interpreted as a regular expression.",
39 | "type": "bool",
40 | "default_value": false
41 | }
42 | }
43 | }"""
44 |
45 | def execute(self, data):
46 | search_string = self.getSettingValueByKey("search")
47 | if not self.getSettingValueByKey("is_regex"):
48 | search_string = re.escape(search_string) #Need to search for the actual string, not as a regex.
49 | search_regex = re.compile(search_string)
50 |
51 | replace_string = self.getSettingValueByKey("replace")
52 |
53 | for layer_number, layer in enumerate(data):
54 | data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all.
55 |
56 | return data
--------------------------------------------------------------------------------
/scripts/Stretch.py:
--------------------------------------------------------------------------------
1 | # This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
2 | """
3 | Copyright (c) 2017 Christophe Baribaud 2017
4 | Python implementation of https://github.com/electrocbd/post_stretch
5 | Correction of hole sizes, cylinder diameters and curves
6 | See the original description in https://github.com/electrocbd/post_stretch
7 |
8 | WARNING This script has never been tested with several extruders
9 | """
10 | from ..Script import Script
11 | import numpy as np
12 | from UM.Logger import Logger
13 | from UM.Application import Application
14 | import re
15 |
16 | def _getValue(line, key, default=None):
17 | """
18 | Convenience function that finds the value in a line of g-code.
19 | When requesting key = x from line "G1 X100" the value 100 is returned.
20 | It is a copy of Stript's method, so it is no DontRepeatYourself, but
21 | I split the class into setup part (Stretch) and execution part (Strecher)
22 | and only the setup part inherits from Script
23 | """
24 | if not key in line or (";" in line and line.find(key) > line.find(";")):
25 | return default
26 | sub_part = line[line.find(key) + 1:]
27 | number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part)
28 | if number is None:
29 | return default
30 | return float(number.group(0))
31 |
32 | class GCodeStep():
33 | """
34 | Class to store the current value of each G_Code parameter
35 | for any G-Code step
36 | """
37 | def __init__(self, step):
38 | self.step = step
39 | self.step_x = 0
40 | self.step_y = 0
41 | self.step_z = 0
42 | self.step_e = 0
43 | self.step_f = 0
44 | self.comment = ""
45 |
46 | def readStep(self, line):
47 | """
48 | Reads gcode from line into self
49 | """
50 | self.step_x = _getValue(line, "X", self.step_x)
51 | self.step_y = _getValue(line, "Y", self.step_y)
52 | self.step_z = _getValue(line, "Z", self.step_z)
53 | self.step_e = _getValue(line, "E", self.step_e)
54 | self.step_f = _getValue(line, "F", self.step_f)
55 | return
56 |
57 | def copyPosFrom(self, step):
58 | """
59 | Copies positions of step into self
60 | """
61 | self.step_x = step.step_x
62 | self.step_y = step.step_y
63 | self.step_z = step.step_z
64 | self.step_e = step.step_e
65 | self.step_f = step.step_f
66 | self.comment = step.comment
67 | return
68 |
69 |
70 | # Execution part of the stretch plugin
71 | class Stretcher():
72 | """
73 | Execution part of the stretch algorithm
74 | """
75 | def __init__(self, line_width, wc_stretch, pw_stretch):
76 | self.line_width = line_width
77 | self.wc_stretch = wc_stretch
78 | self.pw_stretch = pw_stretch
79 | if self.pw_stretch > line_width / 4:
80 | self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance
81 | self.outpos = GCodeStep(0)
82 | self.vd1 = np.empty((0, 2)) # Start points of segments
83 | # of already deposited material for current layer
84 | self.vd2 = np.empty((0, 2)) # End points of segments
85 | # of already deposited material for current layer
86 | self.layer_z = 0 # Z position of the extrusion moves of the current layer
87 | self.layergcode = ""
88 |
89 | def execute(self, data):
90 | """
91 | Computes the new X and Y coordinates of all g-code steps
92 | """
93 | Logger.log("d", "Post stretch with line width = " + str(self.line_width)
94 | + "mm wide circle stretch = " + str(self.wc_stretch)+ "mm"
95 | + "and push wall stretch = " + str(self.pw_stretch) + "mm")
96 | retdata = []
97 | layer_steps = []
98 | current = GCodeStep(0)
99 | self.layer_z = 0.
100 | current_e = 0.
101 | for layer in data:
102 | lines = layer.rstrip("\n").split("\n")
103 | for line in lines:
104 | current.comment = ""
105 | if line.find(";") >= 0:
106 | current.comment = line[line.find(";"):]
107 | if _getValue(line, "G") == 0:
108 | current.readStep(line)
109 | onestep = GCodeStep(0)
110 | onestep.copyPosFrom(current)
111 | elif _getValue(line, "G") == 1:
112 | current.readStep(line)
113 | onestep = GCodeStep(1)
114 | onestep.copyPosFrom(current)
115 | elif _getValue(line, "G") == 92:
116 | current.readStep(line)
117 | onestep = GCodeStep(-1)
118 | onestep.copyPosFrom(current)
119 | else:
120 | onestep = GCodeStep(-1)
121 | onestep.copyPosFrom(current)
122 | onestep.comment = line
123 | if line.find(";LAYER:") >= 0 and len(layer_steps):
124 | # Previous plugin "forgot" to separate two layers...
125 | Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
126 | + " " + str(len(layer_steps)) + " steps")
127 | retdata.append(self.processLayer(layer_steps))
128 | layer_steps = []
129 | layer_steps.append(onestep)
130 | # self.layer_z is the z position of the last extrusion move (not travel move)
131 | if current.step_z != self.layer_z and current.step_e != current_e:
132 | self.layer_z = current.step_z
133 | current_e = current.step_e
134 | if len(layer_steps): # Force a new item in the array
135 | Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
136 | + " " + str(len(layer_steps)) + " steps")
137 | retdata.append(self.processLayer(layer_steps))
138 | layer_steps = []
139 | retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n")
140 | retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n")
141 | return retdata
142 |
143 | def extrusionBreak(self, layer_steps, i_pos):
144 | """
145 | Returns true if the command layer_steps[i_pos] breaks the extruded filament
146 | i.e. it is a travel move
147 | """
148 | if i_pos == 0:
149 | return True # Begining a layer always breaks filament (for simplicity)
150 | step = layer_steps[i_pos]
151 | prev_step = layer_steps[i_pos - 1]
152 | if step.step_e != prev_step.step_e:
153 | return False
154 | delta_x = step.step_x - prev_step.step_x
155 | delta_y = step.step_y - prev_step.step_y
156 | if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4:
157 | # This is a very short movement, less than 0.5 * line_width
158 | # It does not break filament, we should stay in the same extrusion sequence
159 | return False
160 | return True # New sequence
161 |
162 |
163 | def processLayer(self, layer_steps):
164 | """
165 | Computes the new coordinates of g-code steps
166 | for one layer (all the steps at the same Z coordinate)
167 | """
168 | self.outpos.step_x = -1000 # Force output of X and Y coordinates
169 | self.outpos.step_y = -1000 # at each start of layer
170 | self.layergcode = ""
171 | self.vd1 = np.empty((0, 2))
172 | self.vd2 = np.empty((0, 2))
173 | orig_seq = np.empty((0, 2))
174 | modif_seq = np.empty((0, 2))
175 | iflush = 0
176 | for i, step in enumerate(layer_steps):
177 | if step.step == 0 or step.step == 1:
178 | if self.extrusionBreak(layer_steps, i):
179 | # No extrusion since the previous step, so it is a travel move
180 | # Let process steps accumulated into orig_seq,
181 | # which are a sequence of continuous extrusion
182 | modif_seq = np.copy(orig_seq)
183 | if len(orig_seq) >= 2:
184 | self.workOnSequence(orig_seq, modif_seq)
185 | self.generate(layer_steps, iflush, i, modif_seq)
186 | iflush = i
187 | orig_seq = np.empty((0, 2))
188 | orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])])
189 | if len(orig_seq):
190 | modif_seq = np.copy(orig_seq)
191 | if len(orig_seq) >= 2:
192 | self.workOnSequence(orig_seq, modif_seq)
193 | self.generate(layer_steps, iflush, len(layer_steps), modif_seq)
194 | return self.layergcode
195 |
196 | def stepToGcode(self, onestep):
197 | """
198 | Converts a step into G-Code
199 | For each of the X, Y, Z, E and F parameter,
200 | the parameter is written only if its value changed since the
201 | previous g-code step.
202 | """
203 | sout = ""
204 | if onestep.step_f != self.outpos.step_f:
205 | self.outpos.step_f = onestep.step_f
206 | sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".")
207 | if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y:
208 | assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails,
209 | # something went really wrong !
210 | self.outpos.step_x = onestep.step_x
211 | sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".")
212 | assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails,
213 | # something went really wrong !
214 | self.outpos.step_y = onestep.step_y
215 | sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".")
216 | if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z:
217 | self.outpos.step_z = onestep.step_z
218 | sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".")
219 | if onestep.step_e != self.outpos.step_e:
220 | self.outpos.step_e = onestep.step_e
221 | sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".")
222 | return sout
223 |
224 | def generate(self, layer_steps, ibeg, iend, orig_seq):
225 | """
226 | Appends g-code lines to the plugin's returned string
227 | starting from step ibeg included and until step iend excluded
228 | """
229 | ipos = 0
230 | for i in range(ibeg, iend):
231 | if layer_steps[i].step == 0:
232 | layer_steps[i].step_x = orig_seq[ipos][0]
233 | layer_steps[i].step_y = orig_seq[ipos][1]
234 | sout = "G0" + self.stepToGcode(layer_steps[i])
235 | self.layergcode = self.layergcode + sout + "\n"
236 | ipos = ipos + 1
237 | elif layer_steps[i].step == 1:
238 | layer_steps[i].step_x = orig_seq[ipos][0]
239 | layer_steps[i].step_y = orig_seq[ipos][1]
240 | sout = "G1" + self.stepToGcode(layer_steps[i])
241 | self.layergcode = self.layergcode + sout + "\n"
242 | ipos = ipos + 1
243 | else:
244 | self.layergcode = self.layergcode + layer_steps[i].comment + "\n"
245 |
246 |
247 | def workOnSequence(self, orig_seq, modif_seq):
248 | """
249 | Computes new coordinates for a sequence
250 | A sequence is a list of consecutive g-code steps
251 | of continuous material extrusion
252 | """
253 | d_contact = self.line_width / 2.0
254 | if (len(orig_seq) > 2 and
255 | ((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact):
256 | # Starting and ending point of the sequence are nearby
257 | # It is a closed loop
258 | #self.layergcode = self.layergcode + ";wideCircle\n"
259 | self.wideCircle(orig_seq, modif_seq)
260 | else:
261 | #self.layergcode = self.layergcode + ";wideTurn\n"
262 | self.wideTurn(orig_seq, modif_seq) # It is an open curve
263 | if len(orig_seq) > 6: # Don't try push wall on a short sequence
264 | self.pushWall(orig_seq, modif_seq)
265 | if len(orig_seq):
266 | self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])])
267 | self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])])
268 |
269 | def wideCircle(self, orig_seq, modif_seq):
270 | """
271 | Similar to wideTurn
272 | The first and last point of the sequence are the same,
273 | so it is possible to extend the end of the sequence
274 | with its beginning when seeking for triangles
275 |
276 | It is necessary to find the direction of the curve, knowing three points (a triangle)
277 | If the triangle is not wide enough, there is a huge risk of finding
278 | an incorrect orientation, due to insufficient accuracy.
279 | So, when the consecutive points are too close, the method
280 | use following and preceding points to form a wider triangle around
281 | the current point
282 | dmin_tri is the minimum distance between two consecutive points
283 | of an acceptable triangle
284 | """
285 | dmin_tri = self.line_width / 2.0
286 | iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
287 | ibeg = 0 # Index of first point of the triangle
288 | iend = 0 # Index of the third point of the triangle
289 | for i, step in enumerate(orig_seq):
290 | if i == 0 or i == len(orig_seq) - 1:
291 | # First and last point of the sequence are the same,
292 | # so it is necessary to skip one of these two points
293 | # when creating a triangle containing the first or the last point
294 | iextra = iextra_base + 1
295 | else:
296 | iextra = iextra_base
297 | # i is the index of the second point of the triangle
298 | # pos_after is the array of positions of the original sequence
299 | # after the current point
300 | pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2))
301 | # Vector of distances between the current point and each following point
302 | dist_from_point = ((step - pos_after) ** 2).sum(1)
303 | if np.amax(dist_from_point) < dmin_tri * dmin_tri:
304 | continue
305 | iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
306 | # pos_before is the array of positions of the original sequence
307 | # before the current point
308 | pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2))
309 | # This time, vector of distances between the current point and each preceding point
310 | dist_from_point = ((step - pos_before) ** 2).sum(1)
311 | if np.amax(dist_from_point) < dmin_tri * dmin_tri:
312 | continue
313 | ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
314 | # See https://github.com/electrocbd/post_stretch for explanations
315 | # relpos is the relative position of the projection of the second point
316 | # of the triangle on the segment from the first to the third point
317 | # 0 means the position of the first point, 1 means the position of the third,
318 | # intermediate values are positions between
319 | length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0)
320 | relpos = ((step - pos_before[ibeg])
321 | * (pos_after[iend] - pos_before[ibeg])).sum(0)
322 | if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
323 | relpos /= length_base
324 | else:
325 | relpos = 0.5 # To avoid division by zero or precision loss
326 | projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
327 | dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
328 | if dist_from_proj > 0.001: # Move central point only if points are not aligned
329 | modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
330 | * (projection - step))
331 | return
332 |
333 | def wideTurn(self, orig_seq, modif_seq):
334 | '''
335 | We have to select three points in order to form a triangle
336 | These three points should be far enough from each other to have
337 | a reliable estimation of the orientation of the current turn
338 | '''
339 | dmin_tri = self.line_width / 2.0
340 | ibeg = 0
341 | iend = 2
342 | for i in range(1, len(orig_seq) - 1):
343 | dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1)
344 | if np.amax(dist_from_point) < dmin_tri * dmin_tri:
345 | continue
346 | iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri)
347 | dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1)
348 | if np.amax(dist_from_point) < dmin_tri * dmin_tri:
349 | continue
350 | ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri)
351 | length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0)
352 | relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0)
353 | if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
354 | relpos /= length_base
355 | else:
356 | relpos = 0.5
357 | projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg])
358 | dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0))
359 | if dist_from_proj > 0.001:
360 | modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj)
361 | * (projection - orig_seq[i]))
362 | return
363 |
364 | def pushWall(self, orig_seq, modif_seq):
365 | """
366 | The algorithm tests for each segment if material was
367 | already deposited at one or the other side of this segment.
368 | If material was deposited at one side but not both,
369 | the segment is moved into the direction of the deposited material,
370 | to "push the wall"
371 |
372 | Already deposited material is stored as segments.
373 | vd1 is the array of the starting points of the segments
374 | vd2 is the array of the ending points of the segments
375 | For example, segment nr 8 starts at position self.vd1[8]
376 | and ends at position self.vd2[8]
377 | """
378 | dist_palp = self.line_width # Palpation distance to seek for a wall
379 | mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn
380 | for i in range(len(orig_seq)):
381 | ibeg = i # Index of the first point of the segment
382 | iend = i + 1 # Index of the last point of the segment
383 | if iend == len(orig_seq):
384 | iend = i - 1
385 | xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg])
386 | xperp = xperp / np.sqrt((xperp ** 2).sum(-1))
387 | testleft = orig_seq[ibeg] + xperp * dist_palp
388 | materialleft = False # Is there already extruded material at the left of the segment
389 | testright = orig_seq[ibeg] - xperp * dist_palp
390 | materialright = False # Is there already extruded material at the right of the segment
391 | if self.vd1.shape[0]:
392 | relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1)
393 | / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
394 | nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
395 | # nearpoints is the array of the nearest points of each segment
396 | # from the point testleft
397 | dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1)
398 | # dist is the array of the squares of the distances between testleft
399 | # and each segment
400 | if np.amin(dist) <= dist_palp * dist_palp:
401 | materialleft = True
402 | # Now the same computation with the point testright at the other side of the
403 | # current segment
404 | relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1)
405 | / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
406 | nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
407 | dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1)
408 | if np.amin(dist) <= dist_palp * dist_palp:
409 | materialright = True
410 | if materialleft and not materialright:
411 | modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
412 | elif not materialleft and materialright:
413 | modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
414 | if materialleft and materialright:
415 | modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move
416 |
417 | # Setup part of the stretch plugin
418 | class Stretch(Script):
419 | """
420 | Setup part of the stretch algorithm
421 | The only parameter is the stretch distance
422 | """
423 | def __init__(self):
424 | super().__init__()
425 |
426 | def getSettingDataString(self):
427 | return """{
428 | "name":"Post stretch script",
429 | "key": "Stretch",
430 | "metadata": {},
431 | "version": 2,
432 | "settings":
433 | {
434 | "wc_stretch":
435 | {
436 | "label": "Wide circle stretch distance",
437 | "description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
438 | "unit": "mm",
439 | "type": "float",
440 | "default_value": 0.08,
441 | "minimum_value": 0,
442 | "minimum_value_warning": 0,
443 | "maximum_value_warning": 0.2
444 | },
445 | "pw_stretch":
446 | {
447 | "label": "Push Wall stretch distance",
448 | "description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect",
449 | "unit": "mm",
450 | "type": "float",
451 | "default_value": 0.08,
452 | "minimum_value": 0,
453 | "minimum_value_warning": 0,
454 | "maximum_value_warning": 0.2
455 | }
456 | }
457 | }"""
458 |
459 | def execute(self, data):
460 | """
461 | Entry point of the plugin.
462 | data is the list of original g-code instructions,
463 | the returned string is the list of modified g-code instructions
464 | """
465 | stretcher = Stretcher(
466 | Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value")
467 | , self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
468 | return stretcher.execute(data)
469 |
470 |
--------------------------------------------------------------------------------
/scripts/TweakAtZ.py:
--------------------------------------------------------------------------------
1 | # TweakAtZ script - Change printing parameters at a given height
2 | # This script is the successor of the TweakAtZ plugin for legacy Cura.
3 | # It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
4 | # It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
5 | # This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
6 |
7 | #Authors of the TweakAtZ plugin / script:
8 | # Written by Steven Morlock, smorloc@gmail.com
9 | # Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
10 | # Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
11 | # Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
12 | # Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug.
13 |
14 | ##history / changelog:
15 | ##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
16 | ##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
17 | ## extruder three temperature disabled by "#Ex3"
18 | ##V3.1.1: Bugfix reset flow rate
19 | ##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift
20 | ##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
21 | ## added speed reset at the end of the print
22 | ##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
23 | ## extruder three code removed, tweaking print speed, save call of Publisher class,
24 | ## uses previous value from other plugins also on UltiGCode
25 | ##V4.0.1: Bugfix for doubled G1 commands
26 | ##V4.0.2: uses Cura progress bar instead of its own
27 | ##V4.0.3: Bugfix for cool head lift (contributed by luisonoff)
28 | ##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin
29 | ##V4.9.92: Modifications for Cura 15.10
30 | ##V4.9.93: Minor bugfixes (input settings) / documentation
31 | ##V4.9.94: Bugfix Combobox-selection; remove logger
32 | ##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
33 | ##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
34 | ##V5.1: API Changes included for use with Cura 2.2
35 |
36 | ## Uses -
37 | ## M220 S - set speed factor override percentage
38 | ## M221 S - set flow factor override percentage
39 | ## M221 S T<0-#toolheads> - set flow factor override percentage for single extruder
40 | ## M104 S T<0-#toolheads> - set extruder to target temperature
41 | ## M140 S - set bed target temperature
42 | ## M106 S - set fan speed to target speed
43 | ## M605/606 to save and recall material settings on the UM2
44 |
45 | from ..Script import Script
46 | #from UM.Logger import Logger
47 | import re
48 |
49 | class TweakAtZ(Script):
50 | version = "5.1.1"
51 | def __init__(self):
52 | super().__init__()
53 |
54 | def getSettingDataString(self):
55 | return """{
56 | "name":"TweakAtZ """ + self.version + """ (Experimental)",
57 | "key":"TweakAtZ",
58 | "metadata": {},
59 | "version": 2,
60 | "settings":
61 | {
62 | "a_trigger":
63 | {
64 | "label": "Trigger",
65 | "description": "Trigger at height or at layer no.",
66 | "type": "enum",
67 | "options": {"height":"Height","layer_no":"Layer No."},
68 | "default_value": "height"
69 | },
70 | "b_targetZ":
71 | {
72 | "label": "Tweak Height",
73 | "description": "Z height to tweak at",
74 | "unit": "mm",
75 | "type": "float",
76 | "default_value": 5.0,
77 | "minimum_value": "0",
78 | "minimum_value_warning": "0.1",
79 | "maximum_value_warning": "230",
80 | "enabled": "a_trigger == 'height'"
81 | },
82 | "b_targetL":
83 | {
84 | "label": "Tweak Layer",
85 | "description": "Layer no. to tweak at",
86 | "unit": "",
87 | "type": "int",
88 | "default_value": 1,
89 | "minimum_value": "-100",
90 | "minimum_value_warning": "-1",
91 | "enabled": "a_trigger == 'layer_no'"
92 | },
93 | "c_behavior":
94 | {
95 | "label": "Behavior",
96 | "description": "Select behavior: Tweak value and keep it for the rest, Tweak value for single layer only",
97 | "type": "enum",
98 | "options": {"keep_value":"Keep value","single_layer":"Single Layer"},
99 | "default_value": "keep_value"
100 | },
101 | "d_twLayers":
102 | {
103 | "label": "No. Layers",
104 | "description": "No. of layers used to tweak",
105 | "unit": "",
106 | "type": "int",
107 | "default_value": 1,
108 | "minimum_value": "1",
109 | "maximum_value_warning": "50",
110 | "enabled": "c_behavior == 'keep_value'"
111 | },
112 | "e1_Tweak_speed":
113 | {
114 | "label": "Tweak Speed",
115 | "description": "Select if total speed (print and travel) has to be tweaked",
116 | "type": "bool",
117 | "default_value": false
118 | },
119 | "e2_speed":
120 | {
121 | "label": "Speed",
122 | "description": "New total speed (print and travel)",
123 | "unit": "%",
124 | "type": "int",
125 | "default_value": 100,
126 | "minimum_value": "1",
127 | "minimum_value_warning": "10",
128 | "maximum_value_warning": "200",
129 | "enabled": "e1_Tweak_speed"
130 | },
131 | "f1_Tweak_printspeed":
132 | {
133 | "label": "Tweak Print Speed",
134 | "description": "Select if print speed has to be tweaked",
135 | "type": "bool",
136 | "default_value": false
137 | },
138 | "f2_printspeed":
139 | {
140 | "label": "Print Speed",
141 | "description": "New print speed",
142 | "unit": "%",
143 | "type": "int",
144 | "default_value": 100,
145 | "minimum_value": "1",
146 | "minimum_value_warning": "10",
147 | "maximum_value_warning": "200",
148 | "enabled": "f1_Tweak_printspeed"
149 | },
150 | "g1_Tweak_flowrate":
151 | {
152 | "label": "Tweak Flow Rate",
153 | "description": "Select if flow rate has to be tweaked",
154 | "type": "bool",
155 | "default_value": false
156 | },
157 | "g2_flowrate":
158 | {
159 | "label": "Flow Rate",
160 | "description": "New Flow rate",
161 | "unit": "%",
162 | "type": "int",
163 | "default_value": 100,
164 | "minimum_value": "1",
165 | "minimum_value_warning": "10",
166 | "maximum_value_warning": "200",
167 | "enabled": "g1_Tweak_flowrate"
168 | },
169 | "g3_Tweak_flowrateOne":
170 | {
171 | "label": "Tweak Flow Rate 1",
172 | "description": "Select if first extruder flow rate has to be tweaked",
173 | "type": "bool",
174 | "default_value": false
175 | },
176 | "g4_flowrateOne":
177 | {
178 | "label": "Flow Rate One",
179 | "description": "New Flow rate Extruder 1",
180 | "unit": "%",
181 | "type": "int",
182 | "default_value": 100,
183 | "minimum_value": "1",
184 | "minimum_value_warning": "10",
185 | "maximum_value_warning": "200",
186 | "enabled": "g3_Tweak_flowrateOne"
187 | },
188 | "g5_Tweak_flowrateTwo":
189 | {
190 | "label": "Tweak Flow Rate 2",
191 | "description": "Select if second extruder flow rate has to be tweaked",
192 | "type": "bool",
193 | "default_value": false
194 | },
195 | "g6_flowrateTwo":
196 | {
197 | "label": "Flow Rate two",
198 | "description": "New Flow rate Extruder 2",
199 | "unit": "%",
200 | "type": "int",
201 | "default_value": 100,
202 | "minimum_value": "1",
203 | "minimum_value_warning": "10",
204 | "maximum_value_warning": "200",
205 | "enabled": "g5_Tweak_flowrateTwo"
206 | },
207 | "h1_Tweak_bedTemp":
208 | {
209 | "label": "Tweak Bed Temp",
210 | "description": "Select if Bed Temperature has to be tweaked",
211 | "type": "bool",
212 | "default_value": false
213 | },
214 | "h2_bedTemp":
215 | {
216 | "label": "Bed Temp",
217 | "description": "New Bed Temperature",
218 | "unit": "C",
219 | "type": "float",
220 | "default_value": 60,
221 | "minimum_value": "0",
222 | "minimum_value_warning": "30",
223 | "maximum_value_warning": "120",
224 | "enabled": "h1_Tweak_bedTemp"
225 | },
226 | "i1_Tweak_extruderOne":
227 | {
228 | "label": "Tweak Extruder 1 Temp",
229 | "description": "Select if First Extruder Temperature has to be tweaked",
230 | "type": "bool",
231 | "default_value": false
232 | },
233 | "i2_extruderOne":
234 | {
235 | "label": "Extruder 1 Temp",
236 | "description": "New First Extruder Temperature",
237 | "unit": "C",
238 | "type": "float",
239 | "default_value": 190,
240 | "minimum_value": "0",
241 | "minimum_value_warning": "160",
242 | "maximum_value_warning": "250",
243 | "enabled": "i1_Tweak_extruderOne"
244 | },
245 | "i3_Tweak_extruderTwo":
246 | {
247 | "label": "Tweak Extruder 2 Temp",
248 | "description": "Select if Second Extruder Temperature has to be tweaked",
249 | "type": "bool",
250 | "default_value": false
251 | },
252 | "i4_extruderTwo":
253 | {
254 | "label": "Extruder 2 Temp",
255 | "description": "New Second Extruder Temperature",
256 | "unit": "C",
257 | "type": "float",
258 | "default_value": 190,
259 | "minimum_value": "0",
260 | "minimum_value_warning": "160",
261 | "maximum_value_warning": "250",
262 | "enabled": "i3_Tweak_extruderTwo"
263 | },
264 | "j1_Tweak_fanSpeed":
265 | {
266 | "label": "Tweak Fan Speed",
267 | "description": "Select if Fan Speed has to be tweaked",
268 | "type": "bool",
269 | "default_value": false
270 | },
271 | "j2_fanSpeed":
272 | {
273 | "label": "Fan Speed",
274 | "description": "New Fan Speed (0-255)",
275 | "unit": "PWM",
276 | "type": "int",
277 | "default_value": 255,
278 | "minimum_value": "0",
279 | "minimum_value_warning": "15",
280 | "maximum_value_warning": "255",
281 | "enabled": "j1_Tweak_fanSpeed"
282 | }
283 | }
284 | }"""
285 |
286 | def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature
287 | if not key in line or (";" in line and line.find(key) > line.find(";") and
288 | not ";TweakAtZ" in key and not ";LAYER:" in key):
289 | return default
290 | subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
291 | if ";TweakAtZ" in key:
292 | m = re.search("^[0-4]", subPart)
293 | elif ";LAYER:" in key:
294 | m = re.search("^[+-]?[0-9]*", subPart)
295 | else:
296 | #the minus at the beginning allows for negative values, e.g. for delta printers
297 | m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart)
298 | if m == None:
299 | return default
300 | try:
301 | return float(m.group(0))
302 | except:
303 | return default
304 |
305 | def execute(self, data):
306 | #Check which tweaks should apply
307 | TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"),
308 | "flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"),
309 | "flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"),
310 | "flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"),
311 | "bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"),
312 | "extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"),
313 | "extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"),
314 | "fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")}
315 | TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed")
316 | TweakStrings = {"speed": "M220 S%f\n",
317 | "flowrate": "M221 S%f\n",
318 | "flowrateOne": "M221 T0 S%f\n",
319 | "flowrateTwo": "M221 T1 S%f\n",
320 | "bedTemp": "M140 S%f\n",
321 | "extruderOne": "M104 S%f T0\n",
322 | "extruderTwo": "M104 S%f T1\n",
323 | "fanSpeed": "M106 S%d\n"}
324 | target_values = {"speed": self.getSettingValueByKey("e2_speed"),
325 | "printspeed": self.getSettingValueByKey("f2_printspeed"),
326 | "flowrate": self.getSettingValueByKey("g2_flowrate"),
327 | "flowrateOne": self.getSettingValueByKey("g4_flowrateOne"),
328 | "flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"),
329 | "bedTemp": self.getSettingValueByKey("h2_bedTemp"),
330 | "extruderOne": self.getSettingValueByKey("i2_extruderOne"),
331 | "extruderTwo": self.getSettingValueByKey("i4_extruderTwo"),
332 | "fanSpeed": self.getSettingValueByKey("j2_fanSpeed")}
333 | old = {"speed": -1, "flowrate": -1, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1,
334 | "extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1}
335 | twLayers = self.getSettingValueByKey("d_twLayers")
336 | if self.getSettingValueByKey("c_behavior") == "single_layer":
337 | behavior = 1
338 | else:
339 | behavior = 0
340 | try:
341 | twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1
342 | except:
343 | twLayers = 1
344 | pres_ext = 0
345 | done_layers = 0
346 | z = 0
347 | x = None
348 | y = None
349 | layer = -100000 #layer no. may be negative (raft) but never that low
350 | # state 0: deactivated, state 1: activated, state 2: active, but below z,
351 | # state 3: active and partially executed (multi layer), state 4: active and passed z
352 | state = 1
353 | # IsUM2: Used for reset of values (ok for Marlin/Sprinter),
354 | # has to be set to 1 for UltiGCode (work-around for missing default values)
355 | IsUM2 = False
356 | oldValueUnknown = False
357 | TWinstances = 0
358 |
359 | if self.getSettingValueByKey("a_trigger") == "layer_no":
360 | targetL_i = int(self.getSettingValueByKey("b_targetL"))
361 | targetZ = 100000
362 | else:
363 | targetL_i = -100000
364 | targetZ = self.getSettingValueByKey("b_targetZ")
365 | index = 0
366 | for active_layer in data:
367 | modified_gcode = ""
368 | lines = active_layer.split("\n")
369 | for line in lines:
370 | if ";Generated with Cura_SteamEngine" in line:
371 | TWinstances += 1
372 | modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances
373 | if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or
374 | ";TweakAtZ instances:" in line):
375 | modified_gcode += line + "\n"
376 | IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
377 | if ";TweakAtZ-state" in line: #checks for state change comment
378 | state = self.getValue(line, ";TweakAtZ-state", state)
379 | if ";TweakAtZ instances:" in line:
380 | try:
381 | tempTWi = int(line[20:])
382 | except:
383 | tempTWi = TWinstances
384 | TWinstances = tempTWi
385 | if ";Small layer" in line: #checks for begin of Cool Head Lift
386 | old["state"] = state
387 | state = 0
388 | if ";LAYER:" in line: #new layer no. found
389 | if state == 0:
390 | state = old["state"]
391 | layer = self.getValue(line, ";LAYER:", layer)
392 | if targetL_i > -100000: #target selected by layer no.
393 | if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0
394 | state = 2
395 | targetZ = z + 0.001
396 | if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd
397 | pres_ext = self.getValue(line, "T", pres_ext)
398 | if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed
399 | old["bedTemp"] = self.getValue(line, "S", old["bedTemp"])
400 | if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed
401 | if self.getValue(line, "T", pres_ext) == 0:
402 | old["extruderOne"] = self.getValue(line, "S", old["extruderOne"])
403 | elif self.getValue(line, "T", pres_ext) == 1:
404 | old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"])
405 | if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object
406 | old["fanSpeed"] = 0
407 | if "M106" in line and state < 3: #looking for fan speed
408 | old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"])
409 | if "M221" in line and state < 3: #looking for flow rate
410 | tmp_extruder = self.getValue(line,"T",None)
411 | if tmp_extruder == None: #check if extruder is specified
412 | old["flowrate"] = self.getValue(line, "S", old["flowrate"])
413 | elif tmp_extruder == 0: #first extruder
414 | old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
415 | elif tmp_extruder == 1: #second extruder
416 | old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
417 | if ("M84" in line or "M25" in line):
418 | if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2
419 | modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
420 | modified_gcode += "M117 \n"
421 | modified_gcode += line + "\n"
422 | if "G1" in line or "G0" in line:
423 | newZ = self.getValue(line, "Z", z)
424 | x = self.getValue(line, "X", None)
425 | y = self.getValue(line, "Y", None)
426 | e = self.getValue(line, "E", None)
427 | f = self.getValue(line, "F", None)
428 | if 'G1' in line and TweakPrintSpeed and (state==3 or state==4):
429 | # check for pure print movement in target range:
430 | if x != None and y != None and f != None and e != None and newZ==z:
431 | modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"),
432 | self.getValue(line, "Y"), self.getValue(line, "E"))
433 | else: #G1 command but not a print movement
434 | modified_gcode += line + "\n"
435 | # no tweaking on retraction hops which have no x and y coordinate:
436 | if (newZ != z) and (x is not None) and (y is not None):
437 | z = newZ
438 | if z < targetZ and state == 1:
439 | state = 2
440 | if z >= targetZ and state == 2:
441 | state = 3
442 | done_layers = 0
443 | for key in TweakProp:
444 | if TweakProp[key] and old[key]==-1: #old value is not known
445 | oldValueUnknown = True
446 | if oldValueUnknown: #the tweaking has to happen within one layer
447 | twLayers = 1
448 | if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2)
449 | modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1)
450 | if behavior == 1: #single layer tweak only and then reset
451 | twLayers = 1
452 | if TweakPrintSpeed and behavior == 0:
453 | twLayers = done_layers + 1
454 | if state==3:
455 | if twLayers-done_layers>0: #still layers to go?
456 | if targetL_i > -100000:
457 | modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer)
458 | modified_gcode += "M117 Printing... tw@L%4d\n" % layer
459 | else:
460 | modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
461 | modified_gcode += "M117 Printing... tw@%5.1f\n" % z
462 | for key in TweakProp:
463 | if TweakProp[key]:
464 | modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
465 | done_layers += 1
466 | else:
467 | state = 4
468 | if behavior == 1: #reset values after one layer
469 | if targetL_i > -100000:
470 | modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer)
471 | else:
472 | modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
473 | if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
474 | modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
475 | else: #executes on RepRap, UM2 with Ultigcode and Cura setting
476 | for key in TweakProp:
477 | if TweakProp[key]:
478 | modified_gcode += TweakStrings[key] % float(old[key])
479 | # re-activates the plugin if executed by pre-print G-command, resets settings:
480 | if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0
481 | state = 2
482 | done_layers = 0
483 | if targetL_i > -100000:
484 | modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
485 | else:
486 | modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ)
487 | if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
488 | modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
489 | else: #executes on RepRap, UM2 with Ultigcode and Cura setting
490 | for key in TweakProp:
491 | if TweakProp[key]:
492 | modified_gcode += TweakStrings[key] % float(old[key])
493 | data[index] = modified_gcode
494 | index += 1
495 | return data
496 |
--------------------------------------------------------------------------------