├── .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 | image/svg+xml -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------