├── .github └── workflows │ └── build-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── auto_load.py └── generator.py /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | # Build a release containing .zip of the (filtered) contents of the repository 2 | # when a new tag is pushed with a semantic versioning format. 3 | name: Build Release 4 | 5 | on: 6 | push: 7 | tags: ["v[0-9]+.[0-9]+.[0-9]+"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Checkout the repository under a subdirectory (repository-name/) to 14 | # make zipping easier. Note: 'gh' or 'git' commands must be executed 15 | # *after* changing into the repository's directory. 16 | - uses: actions/checkout@v3 17 | with: 18 | path: ${{ github.event.repository.name }} 19 | 20 | # Create a filtered zip of the repository. 21 | - name: Zip Repository (excludes .git*) 22 | run: | 23 | zip -r ${{ github.event.repository.name }}.zip \ 24 | ${{ github.event.repository.name }} \ 25 | -x "${{ github.event.repository.name }}/.git*" 26 | 27 | # Create a new GitHub release using the tag name or commit id. 28 | - name: Create versioned build with filtered zip file. 29 | run: | 30 | cd ${{ github.event.repository.name }} 31 | gh release create ${{github.ref_name}} --generate-notes \ 32 | ../${{ github.event.repository.name }}.zip 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 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 General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Floorboard-Generator 2 | 3 | Download the latest release: 4 | 5 | https://github.com/importbpy/Floorboard-Generator/releases/latest/download/Floorboard-Generator.zip 6 | 7 | and install the zip file (do not unpack it). 8 | 9 | 10 | If you need another version just download the source code and pack it to zip file. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Floor Generator, a Blender addon 4 | # (c) 2013,2015,2016 Michel J. Anders (varkenvarken) 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software Foundation, 18 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | # 20 | # ##### END GPL LICENSE BLOCK ##### 21 | 22 | bl_info = { 23 | "name": "Floor Generator", 24 | "author": "Michel Anders (varkenvarken) with contributions from Alain, Floric ,Lell and Marek Moravec. The idea to add patterns is based on Cedric Brandin's (clarkx) parquet addon", 25 | "version": (1, 0, 0), 26 | "blender": (2, 80, 0), 27 | "location": "View3D > Add > Mesh", 28 | "description": "Adds a mesh representing floor boards (planks)", 29 | "warning": "", 30 | "wiki_url": "", 31 | "tracker_url": "", 32 | "category": "Add Mesh" 33 | } 34 | 35 | from . import auto_load 36 | 37 | auto_load.init() 38 | 39 | def register(): 40 | auto_load.register() 41 | 42 | def unregister(): 43 | auto_load.unregister() 44 | -------------------------------------------------------------------------------- /auto_load.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import typing 3 | import inspect 4 | import pkgutil 5 | import importlib 6 | from pathlib import Path 7 | 8 | __all__ = ( 9 | "init", 10 | "register", 11 | "unregister", 12 | ) 13 | 14 | blender_version = bpy.app.version 15 | 16 | modules = None 17 | ordered_classes = None 18 | 19 | def init(): 20 | global modules 21 | global ordered_classes 22 | 23 | modules = get_all_submodules(Path(__file__).parent) 24 | ordered_classes = get_ordered_classes_to_register(modules) 25 | 26 | def register(): 27 | for cls in ordered_classes: 28 | bpy.utils.register_class(cls) 29 | 30 | for module in modules: 31 | if module.__name__ == __name__: 32 | continue 33 | if hasattr(module, "register"): 34 | module.register() 35 | 36 | def unregister(): 37 | for cls in reversed(ordered_classes): 38 | bpy.utils.unregister_class(cls) 39 | 40 | for module in modules: 41 | if module.__name__ == __name__: 42 | continue 43 | if hasattr(module, "unregister"): 44 | module.unregister() 45 | 46 | 47 | # Import modules 48 | ################################################# 49 | 50 | def get_all_submodules(directory): 51 | return list(iter_submodules(directory, directory.name)) 52 | 53 | def iter_submodules(path, package_name): 54 | for name in sorted(iter_submodule_names(path)): 55 | yield importlib.import_module("." + name, package_name) 56 | 57 | def iter_submodule_names(path, root=""): 58 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]): 59 | if is_package: 60 | sub_path = path / module_name 61 | sub_root = root + module_name + "." 62 | yield from iter_submodule_names(sub_path, sub_root) 63 | else: 64 | yield root + module_name 65 | 66 | 67 | # Find classes to register 68 | ################################################# 69 | 70 | def get_ordered_classes_to_register(modules): 71 | return toposort(get_register_deps_dict(modules)) 72 | 73 | def get_register_deps_dict(modules): 74 | my_classes = set(iter_my_classes(modules)) 75 | my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")} 76 | 77 | deps_dict = {} 78 | for cls in my_classes: 79 | deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) 80 | return deps_dict 81 | 82 | def iter_my_register_deps(cls, my_classes, my_classes_by_idname): 83 | yield from iter_my_deps_from_annotations(cls, my_classes) 84 | yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) 85 | 86 | def iter_my_deps_from_annotations(cls, my_classes): 87 | for value in typing.get_type_hints(cls, {}, {}).values(): 88 | dependency = get_dependency_from_annotation(value) 89 | if dependency is not None: 90 | if dependency in my_classes: 91 | yield dependency 92 | 93 | def get_dependency_from_annotation(value): 94 | if blender_version >= (2, 93): 95 | if isinstance(value, bpy.props._PropertyDeferred): 96 | return value.keywords.get("type") 97 | else: 98 | if isinstance(value, tuple) and len(value) == 2: 99 | if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): 100 | return value[1]["type"] 101 | return None 102 | 103 | def iter_my_deps_from_parent_id(cls, my_classes_by_idname): 104 | if bpy.types.Panel in cls.__bases__: 105 | parent_idname = getattr(cls, "bl_parent_id", None) 106 | if parent_idname is not None: 107 | parent_cls = my_classes_by_idname.get(parent_idname) 108 | if parent_cls is not None: 109 | yield parent_cls 110 | 111 | def iter_my_classes(modules): 112 | base_types = get_register_base_types() 113 | for cls in get_classes_in_modules(modules): 114 | if any(base in base_types for base in cls.__bases__): 115 | if not getattr(cls, "is_registered", False): 116 | yield cls 117 | 118 | def get_classes_in_modules(modules): 119 | classes = set() 120 | for module in modules: 121 | for cls in iter_classes_in_module(module): 122 | classes.add(cls) 123 | return classes 124 | 125 | def iter_classes_in_module(module): 126 | for value in module.__dict__.values(): 127 | if inspect.isclass(value): 128 | yield value 129 | 130 | def get_register_base_types(): 131 | return set(getattr(bpy.types, name) for name in [ 132 | "Panel", "Operator", "PropertyGroup", 133 | "AddonPreferences", "Header", "Menu", 134 | "Node", "NodeSocket", "NodeTree", 135 | "UIList", "RenderEngine", 136 | "Gizmo", "GizmoGroup", 137 | ]) 138 | 139 | 140 | # Find order to register to solve dependencies 141 | ################################################# 142 | 143 | def toposort(deps_dict): 144 | sorted_list = [] 145 | sorted_values = set() 146 | while len(deps_dict) > 0: 147 | unsorted = [] 148 | for value, deps in deps_dict.items(): 149 | if len(deps) == 0: 150 | sorted_list.append(value) 151 | sorted_values.add(value) 152 | else: 153 | unsorted.append(value) 154 | deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} 155 | return sorted_list 156 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | from random import random as rand, seed, uniform as randuni, randrange 2 | from math import pi as PI, sqrt, radians 3 | from copy import deepcopy 4 | from itertools import zip_longest 5 | import bpy 6 | import bmesh 7 | from bpy.props import FloatProperty, IntProperty, BoolProperty, EnumProperty, StringProperty 8 | from mathutils import Vector, Euler 9 | 10 | D180 = radians(180) 11 | D90 = radians(90) 12 | D45 = radians(45) 13 | W2 = sqrt(2) 14 | 15 | # Vector.rotate() does NOT return anything, contrary to what the docs say 16 | # docs are now fixed (https://projects.blender.org/tracker/index.php?func=detail&aid=36518&group_id=9&atid=498) 17 | # but unfortunately no rotated() function was added 18 | def rotate(v, r): 19 | v2 = deepcopy(v) 20 | v2.rotate(r) 21 | return v2 22 | 23 | def rotatep(v, r, p): 24 | v2 = v - p 25 | v2.rotate(r) 26 | return v2 + p 27 | 28 | def vcenter(verts): 29 | return sum(verts,Vector())/len(verts) 30 | 31 | available_meshes = [(' None ','None',"")] 32 | 33 | def availableMeshes(self, context): 34 | available_meshes.clear() 35 | for ob in bpy.data.objects: 36 | if ob.type == 'MESH' and ob.name != context.active_object.name: 37 | name = ob.name[:] 38 | available_meshes.append((name, name, "")) 39 | if(len(available_meshes)==0): 40 | available_meshes.append((' None ','None',"There appear to be no mesh objects in this scene")) 41 | return available_meshes 42 | 43 | def swap(c, i, j): 44 | p1 = deepcopy(c[i]) 45 | p2 = deepcopy(c[j]) 46 | c[i] = p2 47 | c[j] = p1 48 | 49 | def swapx(c, i): 50 | p1 = c[i] 51 | p2 = c[i+1] 52 | x1min = min(v.x for v in p1) 53 | x1max = max(v.x for v in p1) 54 | x2min = min(v.x for v in p2) 55 | x2max = max(v.x for v in p2) 56 | dx1 = Vector((x2min - x1min,0,0)) 57 | dx2 = Vector((x2max - x1max,0,0)) 58 | c[i+1] = [v - dx1 for v in p2] 59 | c[i] = [v + dx2 for v in p1] 60 | 61 | 62 | def getMaterialList(obj): 63 | materials = [] 64 | for slot in obj.material_slots: 65 | materials.append((slot.link, slot.name, slot.material.name, slot.material.use_fake_user)) 66 | slot.material.use_fake_user = True # we remove the mesh so any linked data is invalidated. setting a fake user will keep our material 'live' 67 | return materials 68 | 69 | def rebuildMaterialList(obj, lst): 70 | for slot, (link, slotname, materialname, use_fake_user) in enumerate(lst): 71 | bpy.ops.object.material_slot_add() 72 | obj.material_slots[slot].link = link 73 | obj.material_slots[slot].material = bpy.data.materials[materialname] 74 | obj.material_slots[slot].material.use_fake_user = use_fake_user 75 | 76 | def assignRandomMaterial(slot_n): 77 | bpy.ops.object.mode_set(mode = 'EDIT') # Go to edit mode to create bmesh 78 | obj = bpy.context.object 79 | 80 | bm = bmesh.from_edit_mesh(obj.data) # Create bmesh object from object mesh 81 | 82 | for face in bm.faces: # Iterate over all of the object's faces 83 | face.material_index = randrange(slot_n) # Assign random material to face 84 | obj.data.update() # Update the mesh from the bmesh data 85 | bpy.ops.object.mode_set(mode = 'OBJECT') # Return to object mode 86 | 87 | def plank(start, end, left, right, longgap, shortgap, rot=None): 88 | ll = Vector((start, left, 0)) 89 | lr = Vector((start, right - longgap, 0)) 90 | ul = Vector((end - shortgap, right - longgap, 0)) 91 | ur = Vector((end - shortgap, left, 0)) 92 | if rot: 93 | midpoint = Vector(((start + end)/2.0, (left + right)/ 2.0, 0)) 94 | ll = rotate((ll - midpoint), rot) + midpoint 95 | lr = rotate((lr - midpoint), rot) + midpoint 96 | ul = rotate((ul - midpoint), rot) + midpoint 97 | ur = rotate((ur - midpoint), rot) + midpoint 98 | verts = (ll, lr, ul, ur) 99 | return verts 100 | 101 | def planklw(length, width, rot=None): 102 | ll = Vector((0, 0, 0)) 103 | lr = Vector((length, 0, 0)) 104 | ul = Vector((length, width, 0)) 105 | ur = Vector((0, width, 0)) 106 | if rot: 107 | ll = rotate(ll, rot) 108 | lr = rotate(lr, rot) 109 | ul = rotate(ul, rot) 110 | ur = rotate(ur, rot) 111 | verts = (ll, lr, ul, ur) 112 | return verts 113 | 114 | 115 | def planks(n, m, 116 | length, lengthvar, 117 | width, widthvar, 118 | longgap, shortgap, 119 | offset, randomoffset, minoffset, 120 | nseed, 121 | randrotx, randroty, randrotz, 122 | originx, originy): 123 | 124 | #n=Number of planks, m=Floor Length, length = Planklength 125 | 126 | verts = [] 127 | faces = [] 128 | uvs = [] 129 | 130 | seed(nseed) 131 | widthoffset = 0 132 | s = 0 133 | e = offset 134 | c = offset # Offset per row 135 | ws = 0 136 | p = 0 137 | 138 | while p < n: 139 | p += 1 140 | 141 | uvs.append([]) 142 | 143 | w = width + randuni(0, widthvar) 144 | we = ws + w 145 | if randomoffset: 146 | e = randuni(4 * shortgap + (offset if minoffset else 0.0), length) # we don't like negative plank lengths 147 | while (m - e) > (4 * shortgap + (offset if minoffset else 0.0)): 148 | ll = len(verts) 149 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 150 | pverts = plank(s - originx, e - originx, ws - originy, we - originy, longgap, shortgap, rot) 151 | verts.extend(pverts) 152 | uvs[-1].append(deepcopy(pverts)) 153 | faces.append((ll, ll + 3, ll + 2, ll + 1)) 154 | s = e 155 | e += length + randuni(0, lengthvar) 156 | ll = len(verts) 157 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 158 | pverts = plank(s - originx, m - originx, ws - originy, we - originy, longgap, shortgap, rot) 159 | verts.extend(pverts) 160 | uvs[-1].append(deepcopy(pverts)) 161 | faces.append((ll, ll + 3, ll + 2, ll + 1)) 162 | s = 0 163 | #e = e - m 164 | if c <= (length): 165 | c = c + offset 166 | if c > (length): 167 | c = c - length 168 | e = c 169 | ws = we 170 | # randomly swap uvs of planks. Note: we only swap within one set of planks because different sets can have different widths. 171 | nplanks = len(uvs[-1]) 172 | if nplanks < 2 : continue 173 | for pp in range(nplanks//2): # // to make sure it stays an int 174 | i = randrange(nplanks-1) 175 | swapx(uvs[-1],i) 176 | 177 | fuvs = [uv for col in uvs for plank in col for uv in plank] 178 | return verts, faces, fuvs 179 | 180 | def herringbone(rows, cols, planklength, plankwidth, longgap, shortgap, nseed, randrotx, randroty, randrotz, originx, originy): 181 | verts = [] 182 | faces = [] 183 | uvs = [] 184 | 185 | seed(nseed) 186 | 187 | ll=0 188 | longside = (planklength-shortgap)/sqrt(2.0) 189 | shortside = (plankwidth-longgap)/sqrt(2.0) 190 | vstep = Vector((0,plankwidth * sqrt(2.0),0)) 191 | hstepl = Vector((planklength * sqrt(2.0),0,0)) 192 | hstep = Vector((planklength/sqrt(2.0)-(plankwidth-longgap)/sqrt(2.0),planklength/sqrt(2.0)+(plankwidth-longgap)/sqrt(2.0),0)) 193 | 194 | dy = Vector((0,-planklength/sqrt(2.0),0)) 195 | 196 | pu = [Vector((0,0,0)),Vector((longside,0,0)),Vector((longside,shortside,0)),Vector((0,shortside,0))] 197 | 198 | pv = [Vector((0,0,0)),Vector((longside,longside,0)),Vector((longside-shortside,longside+shortside,0)),Vector((-shortside,shortside,0))] 199 | rot = Euler((0,0,-PI/2),"XYZ") 200 | pvm = [rotate(v, rot)+hstep for v in pv] 201 | 202 | midpointpv = sum(pv,Vector())/4.0 203 | midpointpvm = sum(pvm,Vector())/4.0 204 | 205 | o = Vector((-originx, -originy, 0)) 206 | midpointpvo = midpointpv - o 207 | midpointpvmo = midpointpvm - o 208 | 209 | for col in range(cols): 210 | for row in range(rows): 211 | # CLEANUP: this could be shorter: for P in pv,pvm 212 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 213 | pvo = [ v + o for v in pv] 214 | pverts = [rotate(v - midpointpvo, rot) + midpointpvo + row * vstep + col * hstepl + dy for v in pvo] 215 | verts.extend(deepcopy(pverts)) 216 | uvs.append([v + Vector((col*2*longside,row*shortside,0)) for v in pu]) 217 | faces.append((ll, ll + 1, ll + 2, ll + 3)) 218 | ll = len(verts) 219 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 220 | pvmo = [ v + o for v in pvm] 221 | pverts = [rotate(v - midpointpvmo, rot) + midpointpvmo + row * vstep + col * hstepl + dy for v in pvmo] 222 | verts.extend(deepcopy(pverts)) 223 | uvs.append([v + Vector(((1+col*2)*longside,row*shortside,0)) for v in pu]) 224 | faces.append((ll, ll + 1, ll + 2, ll + 3)) 225 | ll = len(verts) 226 | 227 | for i in range(len(uvs)): 228 | pp1 = randrange(len(uvs)) 229 | pp2 = randrange(len(uvs)) 230 | swap(uvs,pp1,pp2) 231 | 232 | fuvs = [v for p in uvs for v in p] 233 | return verts, faces, fuvs 234 | 235 | def square(rows, cols, planklength, n, border, longgap, shortgap, nseed, randrotx, randroty, randrotz, originx, originy): 236 | verts = [] 237 | verts2 = [] 238 | faces = [] 239 | faces2 = [] 240 | uvs = [] 241 | uvs2 = [] 242 | seed(nseed) 243 | 244 | ll=0 245 | ll2=0 246 | net_planklength = planklength - 2.0 * border 247 | 248 | plankwidth = net_planklength/n 249 | longside = (net_planklength-shortgap) 250 | shortside = (plankwidth-longgap) 251 | stepv = Vector((0,planklength ,0)) 252 | steph = Vector((planklength,0 ,0)) 253 | nstepv = Vector((0,plankwidth ,0)) 254 | nsteph = Vector((plankwidth,0 ,0)) 255 | 256 | pv = [Vector((0,0,0)),Vector((longside,0,0)),Vector((longside,shortside,0)),Vector((0,shortside,0))] 257 | rot = Euler((0,0,-PI/2),"XYZ") 258 | pvm = [rotate(v, rot) + Vector((0,planklength - border,0)) for v in pv] 259 | 260 | midpointpv = sum(pv,Vector())/4.0 261 | midpointpvm = sum(pvm,Vector())/4.0 262 | 263 | offseth = Vector((border, border, 0)) 264 | offsetv = Vector((border, 0, 0)) 265 | 266 | bw = border - shortgap 267 | b1 = [(0,longgap/2.0,0),(0,planklength - longgap/2.0,0),(bw,planklength - longgap/2.0 - border,0),(bw,longgap/2.0 + border,0)] 268 | b1 = [Vector(v) for v in b1] 269 | d = Vector((planklength/2.0, planklength/2.0, 0)) 270 | rot = Euler((0,0,- PI/2),"XYZ") 271 | b2 = [rotate(v-d,rot)+d for v in b1] 272 | rot = Euler((0,0,- PI ),"XYZ") 273 | b3 = [rotate(v-d,rot)+d for v in b1] 274 | rot = Euler((0,0,-3*PI/2),"XYZ") 275 | b4 = [rotate(v-d,rot)+d for v in b1] 276 | 277 | o = Vector((-originx, -originy, 0)) 278 | 279 | # CLEANUP: duplicate code, suboptimal loop nesting and a lot of repeated calculations 280 | # note that the uv map we create here is always aligned in the same direction even though planks alternate. This matches the saw direction in real life 281 | for col in range(cols): 282 | for row in range(rows): 283 | # add the regular planks 284 | for p in range(n): 285 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 286 | if (col ^ row) %2 == 1: 287 | pverts = [rotate(v - midpointpv, rot) + midpointpv + row * stepv + col * steph + nstepv * p + offseth + o for v in pv] 288 | uverts = [v + row * stepv + col * steph + nstepv * p for v in pv] 289 | else: 290 | pverts = [rotate(v - midpointpv, rot) + midpointpv + row * stepv + col * steph + nsteph * p + offsetv + o for v in pvm] 291 | uverts = [v + row * stepv + col * steph + nstepv * p for v in pv] 292 | verts.extend(deepcopy(pverts)) 293 | uvs.append(deepcopy(uverts)) 294 | faces.append((ll, ll + 1, ll + 2, ll + 3)) 295 | ll = len(verts) 296 | # add the border planks 297 | if bw > 0.001: 298 | for vl in b1,b2,b3,b4: 299 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 300 | midpointvl = sum(vl,Vector())/4.0 301 | verts2.extend([rotate(v - midpointvl, rot) + midpointvl + row * stepv + col * steph + o for v in vl]) 302 | uvs2.append(deepcopy([v + row * stepv + col * steph for v in b1])) # again, always the unrotated uvs to match the saw direction 303 | faces2.append((ll2, ll2 + 3, ll2 + 2, ll2 + 1)) 304 | ll2 = len(verts2) 305 | 306 | for i in range(len(uvs)): 307 | pp1 = randrange(len(uvs)) 308 | pp2 = randrange(len(uvs)) 309 | swap(uvs,pp1,pp2) 310 | for i in range(len(uvs2)): 311 | pp1 = randrange(len(uvs2)) 312 | pp2 = randrange(len(uvs2)) 313 | swap(uvs2,pp1,pp2) 314 | 315 | fuvs = [v for p in uvs for v in p] 316 | fuvs2 = [v for p in uvs2 for v in p] 317 | 318 | return verts + verts2, faces + [(f[0]+ll,f[1]+ll,f[2]+ll,f[3]+ll) for f in faces2], fuvs + fuvs2 319 | 320 | def shortside(vert): 321 | """return true if length of 2 out of 3 connected vertices is equal to the min length of the connected edges""" 322 | n = 0 323 | el = [e.calc_length() for e in vert.link_edges] 324 | mel = min(el) 325 | for e in el: 326 | if abs(e - mel) < 1e-4 : 327 | n += 1 328 | return n == 2 329 | 330 | def versaille(rows, cols, planklength, plankwidth,longgap=0, shortgap=0, randrotx=0, randroty=0, randrotz=0, originx=0, originy=0, switch=False): 331 | 332 | o = Vector((-originx, -originy, 0)) * planklength 333 | 334 | # (8*w+w/W2)*W2 + w = 8*w*W2+w = (8*W2+1)*w = 1 335 | w = 1.0 / (8*W2+2) 336 | #w1 = 1 - w 337 | q = w/W2 338 | #k = w*4*W2-w 339 | #s = (k - w)/2 340 | #d = ((s+2*w)/W2)/2 341 | #S = s/W2 342 | sg = shortgap 343 | s2 = sg/W2 344 | lg = longgap 345 | 346 | dd=-q if switch else 0 347 | 348 | planks1 = ( 349 | # rectangles 350 | (0,[(0+sg,0,0), (w*5-sg,0,0), (w*5-sg,w,0), (0+sg,w,0)]), 351 | (0,[(6*w+sg,0,0), (w*11-sg,0,0), (w*11-sg,w,0), (6*w+sg,w,0)]), 352 | (90,[(5*w,-2*w+sg,0), (w*6,-2*w+sg,0), (w*6,3*w-sg,0), (5*w,3*w-sg,0)]), 353 | (0,[(3*w+sg,3*w,0), (w*8-sg,3*w,0), (w*8-sg,w*4,0), (3*w+sg,w*4,0)]), 354 | (0,[(3*w+sg,-3*w,0), (w*8-sg,-3*w,0), (w*8-sg,w*-2,0), (3*w+sg,w*-2,0)]), 355 | (90,[(5*w,4*w+sg,0),(6*w,4*w+sg,0),(6*w,6*w-sg,0),(5*w,6*w-sg,0)]), 356 | (90,[(5*w,-3*w-sg,0),(5*w,-5*w+sg,0),(6*w,-5*w+sg,0),(6*w,-3*w-sg,0)]), 357 | # squares 358 | (0,[(0+sg,w+sg,0), (w*2-sg,w+sg,0), (w*2-sg,w*3-sg,0), (0+sg,w*3-sg,0)]), 359 | (0,[(3*w+sg,w+sg,0), (w*5-sg,w+sg,0), (w*5-sg,w*3-sg,0), (3*w+sg,w*3-sg,0)]), 360 | (0,[(6*w+sg,w+sg,0), (w*8-sg,w+sg,0), (w*8-sg,w*3-sg,0), (6*w+sg,w*3-sg,0)]), 361 | (0,[(9*w+sg,w+sg,0), (w*11-sg,w+sg,0), (w*11-sg,w*3-sg,0), (9*w+sg,w*3-sg,0)]), 362 | (0,[(0+sg,-2*w+sg,0), (w*2-sg,-2*w+sg,0), (w*2-sg,0-sg,0), (0+sg,0-sg,0)]), 363 | (0,[(3*w+sg,-2*w+sg,0), (w*5-sg,-2*w+sg,0), (w*5-sg,0-sg,0), (3*w+sg,0-sg,0)]), 364 | (0,[(6*w+sg,-2*w+sg,0), (w*8-sg,-2*w+sg,0), (w*8-sg,0-sg,0), (6*w+sg,0-sg,0)]), 365 | (0,[(9*w+sg,-2*w+sg,0), (w*11-sg,-2*w+sg,0), (w*11-sg,0-sg,0), (9*w+sg,0-sg,0)]), 366 | (0,[(3*w+sg,4*w+sg,0),(5*w-sg,4*w+sg,0),(5*w-sg,6*w-sg,0),(3*w+sg,6*w-sg,0)]), 367 | (0,[(6*w+sg,4*w+sg,0),(8*w-sg,4*w+sg,0),(8*w-sg,6*w-sg,0),(6*w+sg,6*w-sg,0)]), 368 | (0,[(3*w+sg,-5*w+sg,0),(5*w-sg,-5*w+sg,0),(5*w-sg,-3*w-sg,0),(3*w+sg,-3*w-sg,0)]), 369 | (0,[(6*w+sg,-5*w+sg,0),(8*w-sg,-5*w+sg,0),(8*w-sg,-3*w-sg,0),(6*w+sg,-3*w-sg,0)]), 370 | 371 | # pointed 372 | (0,[(0+sg,3*w,0),(2*w-sg,3*w,0),(2*w-sg,4*w,0),(w+sg,4*w,0)]), 373 | #left 374 | (0,[(w+sg,4*w,0),(2*w-sg,4*w,0),(2*w-sg,5*w-sg*2,0)]), 375 | 376 | (0,[(9*w+sg,3*w,0),(11*w-sg,3*w,0),(10*w-sg,4*w,0),(9*w+sg,4*w,0)]), 377 | #top 378 | (0,[(9*w+sg,4*w,0),(10*w-sg,4*w,0),(9*w+sg,5*w-sg*2,0)]), 379 | 380 | (0,[(0+sg,-2*w,0),(w+sg,-3*w,0),(2*w-sg,-3*w,0),(2*w-sg,-2*w,0)]), 381 | #bottom 382 | (0,[(1*w+sg,-3*w,0),(2*w-sg,-4*w+sg+sg,0),(2*w-sg,-3*w,0)]), 383 | 384 | (0,[(9*w+sg,-3*w,0),(10*w-sg,-3*w,0),(11*w-sg,-2*w,0),(9*w+sg,-2*w,0)]), 385 | #right 386 | (0,[(9*w+sg,-3*w,0),(9*w+sg,-4*w+sg*2,0),(10*w-sg,-3*w,0)]), 387 | 388 | # long pointed 389 | (90,[(2*w,0-sg,0),(2*w,-4*w+sg,0),(3*w,-5*w+sg,0),(3*w,0-sg,0)]), 390 | (90,[(8*w,0-sg,0),(8*w,-5*w+sg,0),(9*w,-4*w+sg,0),(9*w,0-sg,0)]), 391 | (90,[(2*w,w+sg,0),(3*w,w+sg,0),(3*w,6*w-sg,0),(2*w,5*w-sg,0)]), 392 | (90,[(8*w,w+sg,0),(9*w,w+sg,0),(9*w,5*w-sg,0),(8*w,6*w-sg,0)]), 393 | # corner planks 394 | (90,[(0,-2*w+sg,0),(0,3*w-sg,0),(-1*w,2*w-sg,0),(-1*w,-1*w+sg,0)]), 395 | (90,[(11*w,-2*w+sg,0),(12*w,-1*w+sg,0),(12*w,2*w-sg,0),(11*w,3*w-sg,0)]), 396 | (0,[(3*w+sg,-5*w,0),(4*w+sg,-6*w,0),(7*w-sg,-6*w,0),(8*w-sg,-5*w,0)]), 397 | (0,[(3*w+sg,6*w,0),(8*w-sg,6*w,0),(7*w-sg,7*w,0),(4*w+sg,7*w,0)]), 398 | # corner triangles 399 | (90,[(-w-s2,-w+s2*2,0),(-w-s2,2*w-s2*2,0),(-2.5*w+s2,0.5*w,0)]), 400 | (90,[(12*w+s2,2*w-s2*2,0),(12*w+s2,-w+s2*2,0),(13.5*w-s2,0.5*w,0)]), 401 | (0,[(4*w+s2*2,7*w+s2,0),(7*w-s2*2,7*w+s2,0),(5.5*w,8.5*w-s2,0)]), 402 | (0,[(4*w+s2*2,-6*w-s2,0),(5.5*w,-7.5*w+s2,0),(7*w-s2*2,-6*w-s2,0)]), 403 | 404 | # border planks 405 | # bottom 406 | (45,[(-2.5*w-q+q+dd+lg,0.5*w+q-q-dd-lg,0),(-2.5*w-2*q+q+dd+lg+lg,0.5*w-q-dd+lg-lg,0),(5.5*w-q+lg-lg,-7.5*w-q+lg+lg,0),(5.5*w-lg,-7.5*w+lg,0)]), 407 | # right 408 | (135,[(5.5*w-q+lg,-7.5*w-q+lg,0),(5.5*w+lg-lg,-7.5*w-2*q+lg+lg,0),(13.5*w+2*q+dd-lg-lg,0.5*w+dd-lg+lg,0),(13.5*w+q+dd-lg,0.5*w+q+dd-lg,0)]), 409 | #top 410 | (45,[(13.5*w-dd-lg,0.5*w+dd+lg,0),(13.5*w+q-dd-lg-lg,0.5*w+q+dd-lg+lg,0),(5.5*w+q-lg+lg,8.5*w+q-lg-lg,0),(5.5*w+lg,8.5*w-lg,0)]), 411 | #left 412 | (135,[(-2.5*w-q-dd+lg,0.5*w-q-dd+lg,0),(5.5*w+q-lg,8.5*w+q-lg,0),(5.5*w-lg+lg,8.5*w+2*q-lg-lg,0),(-2.5*w-q-q-dd+lg+lg,0.5*w+q-q-dd+lg-lg,0)]) 413 | ) 414 | 415 | verts = [] 416 | faces = [] 417 | uvs = [] 418 | left = 0 419 | center = Vector((5.5*w,0.5*w,0))*planklength 420 | delta = Vector((w, -10*q, 0)) * planklength 421 | for col in range(cols): 422 | start = 0 423 | for row in range(rows): 424 | origin = Vector((start, left, 0)) 425 | for uvrot,p in planks1: 426 | ll = len(verts) 427 | rot = Euler((randrotx * randuni(-1, 1), randroty * randuni(-1, 1), randrotz * randuni(-1, 1)), 'XYZ') 428 | # randomly rotate the plank a little bit around its own center 429 | pverts = [rotate(Vector(v)*planklength, rot) for v in p] 430 | pverts = [origin + delta + o + rotatep(v, Euler((0,0,radians(45)),'XYZ'), center) for v in pverts] 431 | 432 | verts.extend(pverts) 433 | midpoint = vcenter(pverts) 434 | #if uvrot > 0: 435 | # print(uvrot) 436 | # print([v - midpoint for v in pverts]) 437 | # print([rotatep(v, Euler((0,0,radians(uvrot)),'XYZ'), midpoint) - midpoint for v in pverts]) 438 | # print() 439 | uvs.append([rotatep(v, Euler((0,0,radians(uvrot)),'XYZ'), midpoint) for v in pverts]) 440 | faces.append((ll, ll + 3, ll + 2, ll + 1) if len(pverts)==4 else (ll, ll + 2, ll + 1)) 441 | 442 | 443 | start += planklength 444 | left += planklength 445 | 446 | fuvs = [v for p in uvs for v in p] 447 | 448 | return verts, faces, fuvs 449 | 450 | def updateMesh(self, context): 451 | o = context.object 452 | 453 | material_list = getMaterialList(o) 454 | 455 | if o.pattern == 'Regular': 456 | nplanks = (o.width + o.originy) / o.plankwidth 457 | verts, faces, uvs = planks(nplanks, o.length + o.originx, 458 | o.planklength, o.planklengthvar, 459 | o.plankwidth, o.plankwidthvar, 460 | o.longgap, o.shortgap, 461 | o.offset, o.randomoffset, o.minoffset, 462 | o.randomseed, 463 | o.randrotx, o.randroty, o.randrotz, 464 | o.originx, o.originy) 465 | elif o.pattern == 'Herringbone': 466 | # note that there is a lot of extra length and width here to make sure that we create a pattern w.o. gaps at the edges 467 | v = o.plankwidth * sqrt(2.0) 468 | w = o.planklength * sqrt(2.0) 469 | nplanks = int((o.width+o.planklength + o.originy*2) / v)+1 470 | nplanksc = int((o.length + o.originx*2) / w)+1 471 | verts, faces, uvs = herringbone(nplanks, nplanksc, 472 | o.planklength, o.plankwidth, 473 | o.longgap, o.shortgap, 474 | o.randomseed, 475 | o.randrotx, o.randroty, o.randrotz, 476 | o.originx, o.originy) 477 | elif o.pattern == 'Square': 478 | rows = int((o.width + o.originy)/ o.planklength)+1 479 | cols = int((o.length + o.originx)/ o.planklength)+1 480 | verts, faces, uvs = square(rows, cols, o.planklength, o.nsquare, o.border, o.longgap, o.shortgap, o.randomseed, 481 | o.randrotx, o.randroty, o.randrotz, 482 | o.originx, o.originy) 483 | elif o.pattern == 'Versaille': 484 | rows = int((o.width + o.originy)/ o.planklength)+2 485 | cols = int((o.length + o.originx)/ o.planklength)+2 486 | verts, faces, uvs = versaille(rows, cols, 487 | o.planklength, o.plankwidth, 488 | o.longgap, o.shortgap, 489 | o.randrotx, o.randroty, o.randrotz, 490 | o.originx, o.originy, 491 | o.borderswitch) 492 | 493 | # create mesh &link object to scene 494 | emesh = o.data 495 | 496 | mesh = bpy.data.meshes.new(name='Planks') 497 | mesh.from_pydata(verts, [], faces) 498 | 499 | mesh.update(calc_edges=True) 500 | 501 | # more than one object can refer to the same emesh 502 | for i in bpy.data.objects: 503 | if i.data == emesh: 504 | i.data = mesh 505 | 506 | name = emesh.name 507 | emesh.user_clear() # this way the old mesh is marked as used by noone and not saved on exit 508 | bpy.data.meshes.remove(emesh) 509 | mesh.name = name 510 | 511 | #switch to edit mode 512 | if bpy.context.mode != 'EDIT_MESH': 513 | bpy.ops.object.editmode_toggle() 514 | bpy.ops.object.editmode_toggle() 515 | 516 | #set shading to smooth and autosmooth 517 | bpy.ops.object.shade_smooth() 518 | bpy.context.object.data.use_auto_smooth = 1 519 | bpy.context.object.data.auto_smooth_angle = 1 520 | 521 | # add uv-coords and per face random vertex colors 522 | rot = Euler((0,0,o.uvrotation)) 523 | mesh.uv_layers.new() 524 | # WARNING: need to create vertex colors layer before accessing uv_layer 525 | vertex_colors = mesh.vertex_colors.new().data 526 | uv_layer = mesh.uv_layers.active.data 527 | offset = Vector() 528 | # note that the uvs that are returned are shuffled 529 | for poly in mesh.polygons: 530 | color = [rand(), rand(), rand(), 1] 531 | if o.randomuv == 'Random': 532 | offset = Vector((rand(), rand(), 0)) 533 | if o.randomuv == 'Restricted': 534 | offset = Vector((rand()*2-1, rand()*2-1, 0)) 535 | for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total): 536 | co = offset + mesh.vertices[mesh.loops[loop_index].vertex_index].co 537 | if co.x > o.length or co.x < 0: 538 | offset[0] = 0 539 | if co.y > o.width or co.y < 0: 540 | offset[1] = 0 541 | elif o.randomuv == 'Packed': 542 | x = [] 543 | y = [] 544 | for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total): 545 | x.append(uvs[mesh.loops[loop_index].vertex_index].x) 546 | y.append(uvs[mesh.loops[loop_index].vertex_index].y) 547 | offset = Vector((-min(x), -min(y), 0)) 548 | for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total): 549 | if o.randomuv == 'Shuffle': 550 | coords = uvs[mesh.loops[loop_index].vertex_index] 551 | elif o.randomuv in ('Random', 'Restricted'): 552 | coords = mesh.vertices[mesh.loops[loop_index].vertex_index].co + offset 553 | elif o.randomuv == 'Packed': 554 | coords = uvs[mesh.loops[loop_index].vertex_index] + offset 555 | else: 556 | coords = mesh.vertices[mesh.loops[loop_index].vertex_index].co 557 | coords = Vector(coords) # copy 558 | coords.x *= o.uvscalex 559 | coords.y *= o.uvscaley 560 | coords.rotate(rot) 561 | uv_layer[loop_index].uv = coords.xy 562 | vertex_colors[loop_index].color = color 563 | 564 | # subdivide mesh and warp it 565 | warped = o.hollowlong > 0 or o.hollowshort > 0 or o.twist > 0 566 | if warped: 567 | bm = bmesh.new() 568 | bm.from_mesh(mesh) 569 | 570 | # calculate hollowness for each face 571 | dshortmap = {} 572 | dlongmap = {} 573 | for face in bm.faces: 574 | dshort = o.hollowshort * rand() 575 | dlong = o.hollowlong * rand() 576 | for v in face.verts: 577 | dshortmap[v.index] = dshort 578 | dlongmap[v.index] = dlong 579 | 580 | bm.to_mesh(mesh) 581 | bm.free() 582 | 583 | # at this point all new geometry is selected and subdivide works in all selection modes 584 | bpy.ops.object.editmode_toggle() 585 | bpy.ops.mesh.subdivide() # bmesh subdivide doesn't work for me ... 586 | bpy.ops.object.editmode_toggle() 587 | 588 | bm = bmesh.new() 589 | bm.from_mesh(mesh) 590 | 591 | for v in bm.verts: 592 | if o.twist and len(v.link_edges) == 4: # vertex in the middle of the plank 593 | dtwist = o.twist * randuni(-1, 1) 594 | for e in v.link_edges: 595 | v2 = e.other_vert(v) # the vertices on the side of the plank 596 | if shortside(v2): 597 | for e2 in v2.link_edges: 598 | v3 = e2.other_vert(v2) 599 | if len(v3.link_edges) == 2: 600 | v3.co.z += dtwist 601 | dtwist = -dtwist # one corner up, the other corner down 602 | elif len(v.link_edges) == 3: # vertex in the middle of a side of the plank 603 | for e in v.link_edges: 604 | v2 = e.other_vert(v) 605 | if len(v2.link_edges) == 2: # hollowness values are stored with the all original corner vertices 606 | dshort = dshortmap[v2.index] 607 | dlong = dlongmap[v2.index] 608 | break 609 | if shortside(v): 610 | v.co.z -= dlong 611 | else: 612 | v.co.z -= dshort 613 | 614 | if bpy.app.version >= (4,0,0): 615 | creases = bm.edges.layers.float.get('crease_edge', bm.edges.layers.float.new('crease_edge')) 616 | else: 617 | creases = bm.edges.layers.crease.verify() 618 | for edge in bm.edges: 619 | edge[creases] = 1 620 | for vert in edge.verts: 621 | if len(vert.link_edges) == 4: 622 | edge[creases] = 0 623 | break 624 | 625 | bm.to_mesh(mesh) 626 | bm.free() 627 | 628 | 629 | # remove all modifiers to make sure the boolean will be last & only modifier 630 | n = len(o.modifiers) 631 | while n > 0: 632 | n -= 1 633 | bpy.ops.object.modifier_remove(modifier=o.modifiers[-1].name) 634 | 635 | # add thickness 636 | bpy.ops.object.mode_set(mode='EDIT') 637 | bm = bmesh.from_edit_mesh(o.data) 638 | 639 | # extrude to given thickness 640 | ret=bmesh.ops.extrude_face_region(bm,geom=bm.faces[:]) # all planks are separate faces, except when subdivided by random twist or hollowness 641 | if warped: # we have a extra subdivision 642 | Z = Vector((0,0,1)) 643 | for el in ret['geom']: 644 | if isinstance(el, bmesh.types.BMVert) and len(el.link_edges) == 4 and el.normal.dot(Z) > 0.99 : # we look start at the vertex in the middle of the 4 faces but only on the top 645 | d = Vector((0,0,o.thickness + rand() * o.randomthickness)) 646 | verts = set(v for f in el.link_faces for v in f.verts) # some vertices are shared, this way we make them unique 647 | bmesh.ops.translate(bm, vec=d, verts=list(verts)) 648 | else: 649 | for el in ret['geom']: 650 | if isinstance(el, bmesh.types.BMFace): 651 | d = Vector((0,0,o.thickness + rand() * o.randomthickness)) 652 | bmesh.ops.translate(bm, vec=d, verts=el.verts) 653 | 654 | # trim excess flooring 655 | ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(o.length,0,0), plane_no=(1,0,0), clear_outer=True) 656 | ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(0,0,0), plane_no=(-1,0,0), clear_outer=True) 657 | ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(0,o.width,0), plane_no=(0,1,0), clear_outer=True) 658 | ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(0,0,0), plane_no=(0,-1,0), clear_outer=True) 659 | 660 | # fill in holes caused by the trimming 661 | open_edges = [e for e in bm.edges if len(e.link_faces)==1] 662 | bmesh.ops.edgeloop_fill(bm, edges=open_edges, mat_nr=0, use_smooth=False) 663 | 664 | creases = bm.edges.layers.float.get('crease_edge', None) if bpy.app.version >= (4,0,0) else bm.edges.layers.crease.active 665 | if creases is not None: 666 | for edge in open_edges: 667 | edge[creases] = 1 668 | 669 | bmesh.update_edit_mesh(o.data) 670 | bpy.ops.object.mode_set(mode='OBJECT') 671 | 672 | # intersect with a floorplan. Note the floorplan must be 2D (all z-coords must be identical) and a closed polygon. 673 | if self.usefloorplan and self.floorplan != ' None ': 674 | # make the floorplan the only active an selected object 675 | bpy.ops.object.select_all(action='DESELECT') 676 | context.scene.objects.active = bpy.data.objects[self.floorplan] 677 | bpy.data.objects[self.floorplan].select = True 678 | 679 | # duplicate the selected geometry into a separate object 680 | me = context.scene.objects.active.data 681 | selected_faces = [p.index for p in me.polygons if p.select] 682 | bpy.ops.object.editmode_toggle() 683 | bpy.ops.mesh.duplicate() 684 | bpy.ops.mesh.separate() 685 | bpy.ops.object.editmode_toggle() 686 | me = context.scene.objects.active.data 687 | for i in selected_faces: 688 | me.polygons[i].select = True 689 | 690 | # now there will be two selected objects 691 | # the one with the new name will be the copy 692 | for ob in context.selected_objects: 693 | if ob.name != self.floorplan: 694 | fpob = ob 695 | 696 | # make that copy active and selected 697 | for ob in context.selected_objects: 698 | ob.select = False 699 | fpob.select = True 700 | context.scene.objects.active = fpob 701 | # add thickness 702 | # let normals of select faces point in same direction 703 | bpy.ops.object.editmode_toggle() 704 | bpy.ops.mesh.select_all(action='SELECT') 705 | bpy.ops.mesh.normals_make_consistent(inside=False) 706 | bpy.ops.object.editmode_toggle() 707 | # add solidify modifier 708 | # NOTE: for some reason bpy.ops.object.modifier_add doesn't work here 709 | # even though fpob at this point is verifyable the active and selected object ... 710 | mod = fpob.modifiers.new(name='Solidify', type='SOLIDIFY') 711 | mod.offset = 1.0 # in the direction of the normals 712 | mod.thickness = 2000 # very thick 713 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Solidify") 714 | bpy.ops.object.editmode_toggle() 715 | bpy.ops.mesh.select_all(action='SELECT') 716 | bpy.ops.mesh.normals_make_consistent(inside=False) 717 | bpy.ops.object.editmode_toggle() 718 | fpob.location -= Vector((0,0,1000)) # actually this should be in the negative direction of the normals not just plain downward... 719 | 720 | # at this point the floorplan object is the active and selected object 721 | if True: 722 | # make the floorboards active and selected 723 | for ob in context.selected_objects: 724 | ob.select = False 725 | context.scene.objects.active = o 726 | o.select = True 727 | 728 | # add-and-apply a boolean modifier to get the intersection with the floorplan copy 729 | bpy.ops.object.modifier_add(type='BOOLEAN') # default is intersect 730 | o.modifiers[-1].object = fpob 731 | if True: 732 | bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean") 733 | # delete the copy 734 | bpy.ops.object.select_all(action='DESELECT') 735 | context.scene.objects.active = fpob 736 | fpob.select = True 737 | bpy.ops.object.delete() 738 | # make the floorboards active and selected 739 | context.scene.objects.active = o 740 | o.select = True 741 | 742 | if self.modify: 743 | mods = o.modifiers 744 | if len(mods) == 0: # always true 745 | bpy.ops.object.modifier_add(type='BEVEL') 746 | mods[0].use_clamp_overlap = False # clamp overlap causes some issues 747 | #bpy.ops.object.modifier_add(type='EDGE_SPLIT') 748 | mods = o.modifiers 749 | mods[0].show_expanded = False 750 | #mods[1].show_expanded = False 751 | mods[0].width = self.bevel 752 | mods[0].segments = 2 753 | mods[0].limit_method = 'ANGLE' 754 | mods[0].angle_limit = (85/90.0)*PI/2 755 | if warped and not ('SUBSURF' in [m.type for m in mods]): 756 | bpy.ops.object.modifier_add(type='SUBSURF') 757 | mods[-1].show_expanded = False 758 | mods[-1].show_viewport = False # subsurf in viewport is not necessary and slow things down 759 | mods[-1].levels = 2 760 | if not warped and ('SUBSURF' in [m.type for m in mods]): 761 | bpy.ops.object.modifier_remove(modifier='Subsurf') 762 | 763 | if self.preservemats and len(material_list)>0: 764 | rebuildMaterialList(o, material_list) 765 | assignRandomMaterial(len(material_list)) 766 | 767 | # correct the shading artefacts 768 | bpy.ops.object.editmode_toggle() 769 | bpy.ops.mesh.select_all(action='SELECT') 770 | bpy.ops.mesh.remove_doubles() 771 | bpy.ops.mesh.normals_make_consistent() 772 | bpy.ops.mesh.select_all(action='DESELECT') 773 | bpy.ops.mesh.edges_select_sharp(sharpness=1.4) 774 | bpy.ops.mesh.mark_sharp() 775 | bpy.ops.mesh.select_face_by_sides(number=5, type='GREATER', extend=False) 776 | bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') 777 | bpy.ops.mesh.tris_convert_to_quads() 778 | bpy.ops.object.editmode_toggle() 779 | 780 | bpy.types.Object.reg = StringProperty(default='FloorBoards') 781 | 782 | bpy.types.Object.length = FloatProperty(name="Floor Size X", 783 | description="Length (X) of the floor in Blender units", 784 | default=4, 785 | soft_min=0.5, 786 | soft_max=40.0, 787 | subtype='DISTANCE', 788 | unit='LENGTH', 789 | update=updateMesh) 790 | 791 | bpy.types.Object.width = FloatProperty(name="Y", 792 | description="Width (Y) of the floor in Blender units", 793 | default=4, 794 | soft_min=0.5, 795 | soft_max=40.0, 796 | subtype='DISTANCE', 797 | unit='LENGTH', 798 | update=updateMesh) 799 | 800 | bpy.types.Object.planklength = FloatProperty(name="Plank Size X", 801 | description="Length of a single plank", 802 | default=2, 803 | soft_min=0.5, 804 | soft_max=40.0, 805 | subtype='DISTANCE', 806 | unit='LENGTH', 807 | update=updateMesh) 808 | 809 | bpy.types.Object.planklengthvar = FloatProperty(name="Variation X", 810 | description="Max Length variation of single planks", 811 | default=0.2, 812 | min=0, 813 | soft_max=40.0, 814 | subtype='DISTANCE', 815 | unit='LENGTH', 816 | update=updateMesh) 817 | 818 | bpy.types.Object.plankwidth = FloatProperty(name="Y", 819 | description="Width of a single plank", 820 | default=0.18, 821 | soft_min=0.05, 822 | soft_max=40.0, 823 | subtype='DISTANCE', 824 | unit='LENGTH', 825 | update=updateMesh) 826 | 827 | bpy.types.Object.plankwidthvar = FloatProperty(name="Y", 828 | description="Max Width variation of single planks", 829 | default=0, 830 | min=0, 831 | soft_max=4.0, 832 | subtype='DISTANCE', 833 | unit='LENGTH', 834 | update=updateMesh) 835 | 836 | bpy.types.Object.longgap = FloatProperty(name="Gap Size X", 837 | description="Gap between the long edges of the planks", 838 | default=0.002, 839 | min=0, 840 | soft_max=0.01, 841 | step=0.01, 842 | precision=4, 843 | subtype='DISTANCE', 844 | unit='LENGTH', 845 | update=updateMesh) 846 | 847 | bpy.types.Object.shortgap = FloatProperty(name="Y", 848 | description="Gap between the short edges of the planks", 849 | default=0.0005, 850 | min=0, 851 | soft_max=0.01, 852 | step=0.01, 853 | precision=4, 854 | subtype='DISTANCE', 855 | unit='LENGTH', 856 | update=updateMesh) 857 | 858 | bpy.types.Object.thickness = FloatProperty(name="Plank Thickness", 859 | description="Thickness of the planks", 860 | default=0.018, 861 | soft_max=0.1, 862 | soft_min=0.008, 863 | step=0.1, 864 | precision=3, 865 | subtype='DISTANCE', 866 | unit='LENGTH', 867 | update=updateMesh) 868 | 869 | bpy.types.Object.bevel = FloatProperty(name="Bevel", 870 | description="Bevel width planks", 871 | default=0.001, 872 | min=0, 873 | soft_max=0.01, 874 | step=0.01, 875 | precision=4, 876 | subtype='DISTANCE', 877 | unit='LENGTH', 878 | update=updateMesh) 879 | 880 | bpy.types.Object.offset = FloatProperty(name="Offset", 881 | description="Offset per row in Blender Units", 882 | default=0.4, 883 | min=0, 884 | soft_max=2, 885 | subtype='DISTANCE', 886 | unit='LENGTH', 887 | update=updateMesh) 888 | 889 | bpy.types.Object.randomoffset = BoolProperty(name="Offset random", 890 | description="Uses random values for offset", 891 | default=True, 892 | update=updateMesh) 893 | 894 | bpy.types.Object.minoffset = BoolProperty(name="Minimum offset", 895 | description="Use Offset value as a minimum when using random values for offset", 896 | default=False, 897 | update=updateMesh) 898 | 899 | bpy.types.Object.randomseed = IntProperty(name="Random Seed", 900 | description="The seed governing random generation", 901 | default=0, 902 | min=0, 903 | update=updateMesh) 904 | 905 | bpy.types.Object.nsquare = IntProperty(name="Planks per Square", 906 | description="Number of planks in each square tile", 907 | default=4, 908 | min=1, 909 | update=updateMesh) 910 | 911 | bpy.types.Object.border = FloatProperty(name="Border", 912 | description="Width of border", 913 | default=0, 914 | soft_max=0.1, 915 | min=0, 916 | subtype='DISTANCE', 917 | unit='LENGTH', 918 | update=updateMesh) 919 | 920 | bpy.types.Object.originx = FloatProperty(name="Origin Offset X", 921 | description="X offset of the whole pattern", 922 | default=0, 923 | soft_max=1, 924 | min=0, 925 | subtype='DISTANCE', 926 | unit='LENGTH', 927 | update=updateMesh) 928 | 929 | bpy.types.Object.originy = FloatProperty(name="Y", 930 | description="Y offset of the whole pattern", 931 | default=0, 932 | soft_max=1, 933 | min=0, 934 | subtype='DISTANCE', 935 | unit='LENGTH', 936 | update=updateMesh) 937 | 938 | 939 | bpy.types.Object.randomuv = EnumProperty(name="UV randomization", 940 | description="Randomization mode for the uv-offset of individual planks", 941 | items = [('None','None','Plain mapping from top view'), 942 | ('Random','Random','Add a random offset to the plain uv map of individual planks'), 943 | ('Restricted','Restricted','Add a random offset to the plain map but keep individual planks withing the orginal border'), 944 | ('Shuffle','Shuffle','Exchange uvmaps of simlilar planks'), 945 | ('Packed','Packed','Overlap all uvs of individual planks while maintaining their proportions')], 946 | update=updateMesh) 947 | 948 | bpy.types.Object.modify = BoolProperty(name="Add modifiers", 949 | description="Add bevel and solidify modifiers to the planks", 950 | default=True, 951 | update=updateMesh) 952 | 953 | bpy.types.Object.preservemats = BoolProperty(name="Keep materials", 954 | description="Keep any materials assigned to the mesh", 955 | default=True, 956 | update=updateMesh) 957 | 958 | bpy.types.Object.randrotx = FloatProperty(name="Rotation X", 959 | description="Random rotation of individual planks around x-axis", 960 | default=0, 961 | min=0, 962 | soft_max=0.01, 963 | step=(0.02 / 180) * PI, 964 | precision=4, 965 | subtype='ANGLE', 966 | unit='ROTATION', 967 | update=updateMesh) 968 | 969 | bpy.types.Object.randroty = FloatProperty(name="Rotation Y", 970 | description="Random rotation of individual planks around y-axis", 971 | default=0, 972 | min=0, 973 | soft_max=0.01, 974 | step=(0.02 / 180) * PI, 975 | precision=4, 976 | subtype='ANGLE', 977 | unit='ROTATION', 978 | update=updateMesh) 979 | 980 | bpy.types.Object.randrotz = FloatProperty(name="Rotation Z", 981 | description="Random rotation of individual planks around z-axis", 982 | default=0, 983 | min=0, 984 | soft_max=0.01, 985 | step=(0.02 / 180) * PI, 986 | precision=4, 987 | subtype='ANGLE', 988 | unit='ROTATION', 989 | update=updateMesh) 990 | 991 | bpy.types.Object.randomthickness = FloatProperty(name="Thickness", 992 | description="Random thickness added to a plank", 993 | default=0, 994 | min=0, 995 | soft_max=0.05, 996 | step=0.01, 997 | precision=4, 998 | update=updateMesh) 999 | 1000 | bpy.types.Object.hollowlong = FloatProperty(name="Hollowness along plank", 1001 | description="Amount of curvature along a plank", 1002 | default=0, 1003 | min=0, 1004 | soft_max=0.01, 1005 | step=0.01, 1006 | precision=4, 1007 | update=updateMesh) 1008 | 1009 | bpy.types.Object.hollowshort = FloatProperty(name="Hollowness across plank", 1010 | description="Amount of curvature across a plank", 1011 | default=0, 1012 | min=0, 1013 | soft_max=0.01, 1014 | step=0.01, 1015 | precision=4, 1016 | update=updateMesh) 1017 | 1018 | bpy.types.Object.twist = FloatProperty(name="Twist along plank", 1019 | description="Amount of twist along a plank", 1020 | default=0, 1021 | min=0, 1022 | soft_max=0.01, 1023 | step=0.01, 1024 | precision=4, 1025 | update=updateMesh) 1026 | 1027 | bpy.types.Object.uvrotation = FloatProperty(name="UV Rotation", 1028 | description="Rotation of the generated UV-map", 1029 | default=0, 1030 | step=(1.0 / 180) * PI, 1031 | precision=2, 1032 | subtype='ANGLE', 1033 | unit='ROTATION', 1034 | update=updateMesh) 1035 | 1036 | bpy.types.Object.uvscalex = FloatProperty(name="Scale UV X", 1037 | description="Scale UV coordinates in the X direction", 1038 | default=1.0, 1039 | step=0.01, 1040 | precision=4, 1041 | update=updateMesh) 1042 | 1043 | bpy.types.Object.uvscaley = FloatProperty(name="Y", 1044 | description="Scale UV coordinates in the Y direction", 1045 | default=1.0, 1046 | step=0.01, 1047 | precision=4, 1048 | update=updateMesh) 1049 | 1050 | bpy.types.Object.floorplan = EnumProperty(name="Floorplan", 1051 | description="Mesh to use as a floorplan", 1052 | items = availableMeshes, 1053 | update=updateMesh) 1054 | 1055 | bpy.types.Object.usefloorplan = BoolProperty(name="Use Floorplan", 1056 | description="use a mesh object as a floorplan", 1057 | default=False, 1058 | update=updateMesh) 1059 | 1060 | bpy.types.Object.pattern = EnumProperty(name="Pattern", 1061 | description="Pattern of the planks", 1062 | items = [('Regular','Regular','Parallel planks'), 1063 | ('Herringbone','Herringbone','Herringbone pattern'), 1064 | ('Square','Square','Alternating square pattern'), 1065 | ('Versaille','Versaille','Diagonal weave like pattern')], 1066 | update=updateMesh) 1067 | 1068 | bpy.types.Object.borderswitch = BoolProperty(name="Switch border", 1069 | description="Order border plank in a spiral fashion", 1070 | default=False, 1071 | update=updateMesh) 1072 | 1073 | 1074 | 1075 | class FBGPanelMain(bpy.types.Panel): 1076 | bl_idname = "FLOOR_BOARD_GEN_PT_main" 1077 | bl_label = "Main Settings" 1078 | bl_space_type = "VIEW_3D" 1079 | bl_region_type = "UI" 1080 | bl_category = "Floorgenerator" 1081 | 1082 | #def poll(cls, context): 1083 | #return context.mode == 'OBJECT' 1084 | 1085 | @classmethod 1086 | def poll(cls, context): 1087 | o = context.object 1088 | return ((context.active_object != None) and (context.mode == 'OBJECT')) 1089 | 1090 | def draw(self, context): 1091 | layout = self.layout 1092 | o = context.object 1093 | layout.use_property_split = True # Active single-column layout 1094 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 1095 | 1096 | if 'reg' in o: 1097 | if o['reg'] == 'FloorBoards': 1098 | 1099 | col = flow.column(align=True) 1100 | col.prop(o, 'pattern') 1101 | col = flow.column(align=True) 1102 | col.prop(o, 'length') 1103 | col.prop(o, 'width') 1104 | 1105 | 1106 | else: 1107 | layout.operator('mesh.floor_boards_convert') 1108 | else: 1109 | layout.operator('mesh.floor_boards_convert') 1110 | 1111 | class FBGPanelMainOffset(bpy.types.Panel): 1112 | bl_idname = "FLOOR_BOARD_GEN_PT_main_offset" 1113 | bl_parent_id = "FLOOR_BOARD_GEN_PT_main" 1114 | bl_label = "Origin Offset" 1115 | bl_space_type = "VIEW_3D" 1116 | bl_region_type = "UI" 1117 | bl_category = "Floorgenerator" 1118 | 1119 | 1120 | @classmethod 1121 | def poll(cls, context): 1122 | o = context.object 1123 | return ((context.mode == 'OBJECT') and ('reg' in o) and (o['reg'] == 'FloorBoards')) 1124 | 1125 | def draw(self, context): 1126 | o = context.object 1127 | layout = self.layout 1128 | layout.use_property_split = True # Active single-column layout 1129 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 1130 | 1131 | col = flow.column(align=True) 1132 | col.prop(o, 'originx') 1133 | col.prop(o, 'originy') 1134 | 1135 | class FBGPanelPlanksDim(bpy.types.Panel): 1136 | bl_idname = "FLOOR_BOARD_GEN_PT_planks_dimensions" 1137 | bl_label = "Plank Dimensions" 1138 | bl_space_type = "VIEW_3D" 1139 | bl_region_type = "UI" 1140 | bl_category = "Floorgenerator" 1141 | 1142 | 1143 | @classmethod 1144 | def poll(cls, context): 1145 | o = context.object 1146 | return ((context.active_object != None) and (context.mode == 'OBJECT') and ('reg' in o) and (o['reg'] == 'FloorBoards')) 1147 | 1148 | def draw(self, context): 1149 | o = context.object 1150 | layout = self.layout 1151 | layout.use_property_split = True # Active single-column layout 1152 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 1153 | 1154 | if o.pattern == 'Regular': 1155 | col = flow.column(align=True) 1156 | col.prop(o, 'planklength') 1157 | col.prop(o, 'plankwidth') 1158 | col = flow.column(align=True) 1159 | col.prop(o, 'planklengthvar') 1160 | col.prop(o, 'plankwidthvar') 1161 | 1162 | if o.pattern == 'Herringbone': 1163 | col = flow.column(align=True) 1164 | col.prop(o, 'planklength') 1165 | col.prop(o, 'plankwidth') 1166 | 1167 | if o.pattern == 'Square': 1168 | layout.prop(o, 'planklength') 1169 | layout.prop(o, 'nsquare') 1170 | layout.prop(o, 'border') 1171 | 1172 | if o.pattern == 'Versaille': 1173 | layout.prop(o, 'planklength') 1174 | 1175 | layout.prop(o, 'thickness') 1176 | 1177 | col = flow.column(align=True) 1178 | col.prop(o, 'longgap') 1179 | col.prop(o, 'shortgap') 1180 | 1181 | if o.pattern == 'Regular': 1182 | layout.prop(o, 'offset') 1183 | layout.prop(o, 'randomoffset') 1184 | layout.prop(o, 'minoffset') 1185 | 1186 | if o.pattern == 'Versaille': 1187 | layout.prop(o, 'borderswitch') 1188 | 1189 | class FBGPanelRandomness(bpy.types.Panel): 1190 | bl_idname = "FLOOR_BOARD_GEN_PT_randomness" 1191 | bl_parent_id = "FLOOR_BOARD_GEN_PT_planks_dimensions" 1192 | bl_label = "Randomness" 1193 | bl_space_type = "VIEW_3D" 1194 | bl_region_type = "UI" 1195 | bl_category = "Floorgenerator" 1196 | 1197 | 1198 | @classmethod 1199 | def poll(cls, context): 1200 | o = context.object 1201 | return ((context.mode == 'OBJECT') and ('reg' in o) and (o['reg'] == 'FloorBoards')) 1202 | 1203 | def draw(self, context): 1204 | o = context.object 1205 | layout = self.layout 1206 | layout.use_property_split = True # Active single-column layout 1207 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 1208 | 1209 | col = flow.column(align=True) 1210 | col.prop(o, 'randrotx') 1211 | col.prop(o, 'randroty') 1212 | col.prop(o, 'randrotz') 1213 | 1214 | col = flow.column(align=True) 1215 | col.prop(o, 'hollowlong') 1216 | col.prop(o, 'hollowshort') 1217 | 1218 | layout.prop(o, 'twist') 1219 | layout.prop(o, 'randomthickness') 1220 | layout.prop(o, 'randomseed') 1221 | 1222 | class FBGPanelMiscellaneous(bpy.types.Panel): 1223 | bl_idname = "FLOOR_BOARD_GEN_PT_miscellaneous" 1224 | bl_label = "Miscellaneous" 1225 | bl_space_type = "VIEW_3D" 1226 | bl_region_type = "UI" 1227 | bl_category = "Floorgenerator" 1228 | 1229 | 1230 | @classmethod 1231 | def poll(cls, context): 1232 | o = context.object 1233 | return ((context.active_object != None) and (context.mode == 'OBJECT') and ('reg' in o) and (o['reg'] == 'FloorBoards')) 1234 | 1235 | def draw(self, context): 1236 | o = context.object 1237 | layout = self.layout 1238 | layout.use_property_split = True # Active single-column layout 1239 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 1240 | 1241 | layout.prop(o, 'usefloorplan') 1242 | if o.usefloorplan: 1243 | layout.prop(o, 'floorplan') 1244 | layout.prop(o, 'randomuv') 1245 | layout.prop(o, 'uvrotation') 1246 | col = flow.column(align=True) 1247 | col.prop(o, 'uvscalex') 1248 | col.prop(o, 'uvscaley') 1249 | layout.prop(o, 'modify') 1250 | if o.modify: 1251 | layout.prop(o, 'bevel') 1252 | layout.prop(o,'preservemats') 1253 | 1254 | 1255 | 1256 | class FloorBoardsAdd(bpy.types.Operator): 1257 | bl_idname = "mesh.floor_boards_add" 1258 | bl_label = "FloorBoards" 1259 | bl_options = {'REGISTER', 'UNDO'} 1260 | 1261 | @classmethod 1262 | def poll(self, context): 1263 | return context.mode == 'OBJECT' 1264 | 1265 | def execute(self, context): 1266 | bpy.ops.mesh.primitive_cube_add() 1267 | context.active_object.name = "FloorBoard" 1268 | bpy.ops.mesh.floor_boards_convert('INVOKE_DEFAULT') 1269 | return {'FINISHED'} 1270 | 1271 | class FloorBoardsConvert(bpy.types.Operator): 1272 | bl_idname = 'mesh.floor_boards_convert' 1273 | bl_label = 'Convert to Floorobject' 1274 | #bl_options = {"UNDO"} 1275 | 1276 | def invoke(self, context, event): 1277 | o = context.object 1278 | o.reg = 'FloorBoards' 1279 | o.length = 4 1280 | return {"FINISHED"} 1281 | 1282 | def menu_func(self, context): 1283 | self.layout.operator( 1284 | FloorBoardsAdd.bl_idname, 1285 | text="Add floor board mesh", 1286 | icon='PLUGIN' 1287 | ) 1288 | 1289 | def register(): 1290 | bpy.types.VIEW3D_MT_mesh_add.append(menu_func) 1291 | 1292 | def unregister(): 1293 | bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) 1294 | 1295 | --------------------------------------------------------------------------------