├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── __init__.py ├── blender_utils.py ├── blockBuilder.py ├── blockMeshBodyFit.py ├── blockMeshMG.py ├── cycleFinderNumba.py ├── docs ├── Makefile ├── conf.py ├── images │ ├── block_method_settings.png │ ├── boundary_patches.png │ ├── complex_block_with_elongations.png │ ├── edge_settings.png │ ├── flow_around_sphere.png │ ├── naca_airfoil_mesh_preview.png │ ├── projections.png │ └── ui.png ├── index.rst ├── requirements.txt └── swift.rst ├── example ├── complex_block_example.blend ├── flow_around_sphere.blend └── naca_airfoil_example.blend └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Emacs 107 | *~ 108 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | version: "2" 4 | 5 | build: 6 | os: "ubuntu-22.04" 7 | tools: 8 | python: "3.10" 9 | 10 | python: 11 | install: 12 | - requirements: docs/requirements.txt 13 | 14 | sphinx: 15 | configuration: docs/conf.py 16 | -------------------------------------------------------------------------------- /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 | # SwiftBlock 2 | 3 | SwiftBlock is a [Blender](https://www.blender.org/) GUI add-on for 4 | the OpenFOAM® *blockMesh* utility, which creates hexahedral block 5 | structured volume meshes for OpenFOAM simulations. 6 | The target of SwiftBlock is to ease the creation of structured 7 | block meshes for controlled grading (e.g. boundary layers) or streched 8 | cells composed of hexahedral cell blocks. 9 | 10 | Block structure is first modelled as a mesh object in Blender. A graph 11 | theory based method implemented in the addon identifies the discrete 12 | hexahedral blocks in the mesh object and generates blockMeshDict. Main 13 | features include 14 | 15 | * user specified divisions and optional grading of block edges 16 | * specification of patches (boundary surfaces) 17 | * specification of blocks to create cell zones/sets 18 | * easy block manipulations including selection, visualisation and disabling of blocks 19 | * visualization of edge directions 20 | * projection of block edges to surfaces on another object to 21 | create curved shapes 22 | 23 |

24 | 25 |

26 | 27 | The add-on is aimed to work with latest 28 | [Blender LTS version](https://www.blender.org/download/lts/) and 29 | latest [OpenFOAM Foundation](https://openfoam.org/) and 30 | latest [OpenFOAM.com](https://openfoam.com) versions. 31 | Tested with Blender 4.2 and OpenFOAM.org v12. 32 | 33 | ## Documentation 34 | 35 | Documentation (made using [Sphinx](https://www.sphinx-doc.org/en/master/)) 36 | is located in docs directory of the sources and is viewable online at 37 | https://swiftblock.readthedocs.io. 38 | 39 | ## Feedback 40 | 41 | If you use this add-on, please star the project in GitHub! 42 | 43 | ### OpenFOAM Trade Mark Notice 44 | 45 | This offering is not approved or endorsed by OpenCFD Limited, producer 46 | and distributor of the OpenFOAM software via www.openfoam.com, and 47 | owner of the OPENFOAM® and OpenCFD® trade marks. 48 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "SwiftBlock", 3 | "author": "Karl-Johan Nogenmyr, Mikko Folkersma, Turo Valikangas, Tuomo Keskitalo", 4 | "version": (0, 5), 5 | "blender": (2, 93, 0), 6 | "location": "View_3D > Object > SwiftBlock", 7 | "description": "Creates OpenFOAM blockMeshDict block geometry definition file", 8 | "warning": "", 9 | "wiki_url": "https://github.com/tkeskita/swiftBlock", 10 | "tracker_url": "https://github.com/tkeskita/swiftBlock/issues", 11 | "category": "OpenFOAM"} 12 | 13 | import bpy 14 | import bmesh 15 | import time 16 | import importlib 17 | from . import blockBuilder 18 | importlib.reload(blockBuilder) 19 | #from . import blender_utils 20 | #importlib.reload(blender_utils) 21 | from .utils import * 22 | importlib.reload(utils) 23 | from mathutils import Vector 24 | 25 | 26 | # Property groups 27 | # --------------- 28 | # Note: Property groups must be registered immediately to avoid later error 29 | # about "missing bl_rna attribute from 'RNAMetaPropGroup' instance 30 | # (may not be registered)". This will generate warnings about 31 | # registration failed for these classes when all classes are registered. 32 | # TODO: Find elegant solution. 33 | 34 | class SWIFTBLOCK_PG_BlockProperty(bpy.types.PropertyGroup): 35 | id: bpy.props.IntProperty() 36 | name: bpy.props.StringProperty() 37 | verts: bpy.props.IntVectorProperty(size = 8) 38 | enabled: bpy.props.BoolProperty(default=True) 39 | namedRegion: bpy.props.BoolProperty(default=False) 40 | bpy.utils.register_class(SWIFTBLOCK_PG_BlockProperty) 41 | 42 | class SWIFTBLOCK_PG_ProjectionProperty(bpy.types.PropertyGroup): 43 | type: bpy.props.StringProperty() #vert2surf,edge2surf,face2sur,edge2polyline 44 | id: bpy.props.IntProperty() #bmesh id 45 | ob: bpy.props.StringProperty() 46 | bpy.utils.register_class(SWIFTBLOCK_PG_ProjectionProperty) 47 | 48 | class SWIFTBLOCK_PG_EdgeGroupProperty(bpy.types.PropertyGroup): 49 | group_name: bpy.props.StringProperty() 50 | group_edges: bpy.props.StringProperty() 51 | bpy.utils.register_class(SWIFTBLOCK_PG_EdgeGroupProperty) 52 | 53 | 54 | 55 | # Object properties 56 | # ----------------- 57 | 58 | bpy.types.Object.swiftBlock_isblockingObject = bpy.props.BoolProperty(default=False) 59 | bpy.types.Object.swiftBlock_blocking_object = bpy.props.StringProperty(default="") 60 | bpy.types.Object.swiftBlock_ispreviewObject = bpy.props.BoolProperty(default=False) 61 | bpy.types.Object.swiftBlock_preview_object = bpy.props.StringProperty(default='') 62 | bpy.types.Object.swiftBlock_direction_object = bpy.props.StringProperty(default="") 63 | bpy.types.Object.swiftBlock_isdirectionObject = bpy.props.BoolProperty(default=False) 64 | 65 | bpy.types.Object.swiftBlock_Mesher = bpy.props.EnumProperty( 66 | name="Blocking Method Selection", 67 | items = ( 68 | ("blockMeshMG","blockMeshMG","Block Mesh (with Multigrading and Projections)", 1), 69 | # blockMeshBodyFit has not been upgraded/tested with Blender 2.8, disable for now 70 | # ("blockMeshBodyFit","blockMeshBodyFit","Body Fit Method (Requires blockMeshBodyFit)", 2), 71 | ), 72 | update=changeMesher) 73 | 74 | # Blocking properties 75 | bpy.types.Object.swiftBlock_blocks = bpy.props.CollectionProperty(type=SWIFTBLOCK_PG_BlockProperty) 76 | bpy.types.Object.swiftBlock_block_index = bpy.props.IntProperty() 77 | bpy.types.Object.swiftBlock_useNumba = bpy.props.BoolProperty( 78 | name="Use Numba", 79 | description="Option to Use Python Numba Performance Library (Must be Installed Separately)", 80 | default=False, 81 | ) 82 | 83 | # Projection/snapping properties 84 | bpy.types.Object.swiftBlock_projections = \ 85 | bpy.props.CollectionProperty(type=SWIFTBLOCK_PG_ProjectionProperty) 86 | bpy.types.Object.swiftBlock_projection_index = bpy.props.IntProperty() 87 | bpy.types.Object.swiftBlock_Autosnap = bpy.props.BoolProperty( 88 | name="Automatic Edge Projection", 89 | description = "Option to Snap Lines Automatically from Geometry" 90 | ) 91 | bpy.types.Object.swiftBlock_ShowInternalFaces = bpy.props.BoolProperty( 92 | name="Show Internal Faces", 93 | description = "Show Internal Faces", 94 | default=False, update=showInternalFaces 95 | ) 96 | bpy.types.Object.swiftBlock_ProjectionObject = bpy.props.EnumProperty( 97 | name="Projection Object", 98 | items=getProjectionObjects, description = "Projection Object" 99 | ) 100 | bpy.types.Object.swiftBlock_EdgeSnapObject = bpy.props.EnumProperty( 101 | name="Object", 102 | items=getProjectionObjects, description = "Projection Object" 103 | ) 104 | 105 | # Mapping properties 106 | bpy.types.Object.swiftBlock_MappingType = bpy.props.EnumProperty( 107 | name="", 108 | items = (("Geometric MG","Geometric MG","",1), 109 | ("Geometric","Geometric","",2),) 110 | ) 111 | 112 | # bpy.types.Object.swiftBlock_Dx = bpy.props.FloatProperty( 113 | # name="dx", default=1, update=setCellSize, min=0) 114 | bpy.types.Object.swiftBlock_Cells = bpy.props.IntProperty( 115 | name="Cells", default=10, min=1, description="Number of Cell Divisions for Edge") 116 | bpy.types.Object.swiftBlock_x1 = bpy.props.FloatProperty( 117 | name="x1", default=0.1, description="First Cell Edge Length", min=0) 118 | bpy.types.Object.swiftBlock_x2 = bpy.props.FloatProperty( 119 | name="x2", default=0.1, description="Last Cell Edge Length", min=0) 120 | bpy.types.Object.swiftBlock_r1 = bpy.props.FloatProperty( 121 | name="r1", default=1.0, description="First Boundary Layer Geometric Ratio", min=1.0) 122 | bpy.types.Object.swiftBlock_r2 = bpy.props.FloatProperty( 123 | name="r2", default=1.0, description="Last Boundary Layer Geometric Ratio", min=1.0) 124 | bpy.types.Object.swiftBlock_Ratio = bpy.props.FloatProperty( 125 | name="Ratio", default=1.0, description="Ratio of First Cell Length to Last Cell Length", min=0) 126 | bpy.types.Object.swiftBlock_SearchLength = bpy.props.FloatProperty( 127 | name="Search Length", default=1.0, description="Search Length", min=0) 128 | # bpy.types.Object.swiftBlock_ShowEdgeDirections = bpy.props.BoolProperty( 129 | # name="Show directions", default=True, update = updateEdgeDirections, description="Show edge directions?") 130 | 131 | # Boundary condition properties 132 | bpy.types.Object.swiftBlock_bcTypeEnum = bpy.props.EnumProperty( 133 | items = [('wall', 'wall', 'Defines the patch as wall'), 134 | ('patch', 'patch', 'Defines the patch as generic patch'), 135 | ('empty', 'empty', 'Defines the patch as empty'), 136 | ('symmetry', 'symmetry', 'Defines the patch as symmetry'), 137 | ], 138 | name = "Patch Type" 139 | ) 140 | bpy.types.Object.swiftBlock_patchName = bpy.props.StringProperty( 141 | name = "Patch Name", 142 | description = "Specify Name of Patch", 143 | default = "default" 144 | ) 145 | bpy.types.Object.swiftBlock_boundary_index = bpy.props.IntProperty( 146 | description = "Boundary Patch Index", 147 | update = selectActiveBoundary 148 | ) 149 | bpy.types.Material.boundary_type = bpy.props.EnumProperty( 150 | items = [('wall', 'wall', '', 1), 151 | ('patch', 'patch', '', 2), 152 | ('empty', 'empty', '', 3), 153 | ('symmetry', 'symmetry', '', 4), 154 | ], 155 | name = "Patch Type", 156 | description = "Boundary Patch Type" 157 | ) 158 | 159 | # Edge group properties 160 | bpy.types.Object.swiftBlock_edgegroups = \ 161 | bpy.props.CollectionProperty(type=SWIFTBLOCK_PG_EdgeGroupProperty) 162 | 163 | bpy.types.Object.swiftBlock_edgegroup_index = bpy.props.IntProperty() 164 | 165 | # Main class definitions 166 | # ---------------------- 167 | 168 | # Create the swiftBlock panel 169 | class VIEW3D_PT_SwiftBlockPanel(bpy.types.Panel): 170 | bl_space_type = "VIEW_3D" 171 | bl_region_type = "UI" 172 | bl_category = "SwiftBlock" 173 | bl_label = "SwiftBlock" 174 | 175 | def draw(self, context): 176 | ob = context.active_object 177 | if not ob: 178 | return 179 | box = self.layout.column(align=True) 180 | 181 | if ob.swiftBlock_ispreviewObject: 182 | box = self.layout.box() 183 | box.operator("swift_block.activate_blocking").hide = True 184 | elif ob.swiftBlock_blocking_object and ob.name != ob.swiftBlock_blocking_object: 185 | box = self.layout.box() 186 | box.operator("swift_block.activate_blocking").hide = False 187 | elif not ob.swiftBlock_isblockingObject and ob.type == 'MESH': 188 | box.operator("swift_block.init_blocking") 189 | 190 | elif context.active_object and bpy.context.active_object.mode == "EDIT": 191 | 192 | box = self.layout.box() 193 | box.label(text="Block Method Settings") 194 | box.alignment = 'RIGHT' 195 | box.prop(ob, "swiftBlock_Mesher", text="Method") 196 | split = box.split(factor=0.5) 197 | split.operator("swift_block.build_blocking") 198 | split.prop(ob, "swiftBlock_useNumba") 199 | 200 | split = box.split() 201 | split.operator("swift_block.preview_mesh") 202 | split = split.split() 203 | split.operator("swift_block.write_mesh") 204 | box.template_list("SWIFTBLOCK_UL_block_items", "", ob, "swiftBlock_blocks", ob, "swiftBlock_block_index", rows=2) 205 | box.operator("swift_block.get_block") 206 | row = box.row() 207 | row.operator("swift_block.extrude_blocks") 208 | 209 | box = self.layout.box() 210 | box.label(text="Edge Settings") 211 | # box.prop(ob, "swiftBlock_MappingType") 212 | split = box.split() 213 | split.prop(ob, "swiftBlock_Cells") 214 | # split.operator("swift_block.set_cellsize") 215 | if ob.swiftBlock_Mesher == "blockMeshMG": 216 | split = box.split() 217 | col = split.column() 218 | col.label(text="Start") 219 | col.prop(ob, "swiftBlock_x1") 220 | col.prop(ob, "swiftBlock_r1") 221 | col = split.column() 222 | col.label(text="End") 223 | col.prop(ob, "swiftBlock_x2") 224 | col.prop(ob, "swiftBlock_r2") 225 | elif ob.swiftBlock_Mesher == "blockMeshBodyFit": 226 | split.prop(ob, "swiftBlock_Ratio") 227 | split = box.split() 228 | split.operator("swift_block.set_edge") 229 | split.operator("swift_block.get_edge") 230 | split = box.split() 231 | split.operator("swift_block.edge_select_parallel") 232 | split.operator("swift_block.flip_edges") 233 | if 'Edge_directions' in bpy.data.objects: 234 | box.operator("swift_block.draw_edge_directions",text='Show edge directions',emboss=False,icon="CHECKBOX_HLT").show=False 235 | else: 236 | box.operator("swift_block.draw_edge_directions",text='Show edge directions',emboss=False,icon="CHECKBOX_DEHLT").show=True 237 | #Edge grouping control panel 238 | box = self.layout.box() 239 | box.label(text="Edge Groups") 240 | split = box.split(factor=0.85) 241 | row = split.column() 242 | row.template_list("SWIFTBLOCK_UL_edgegroup_items", "", ob, "swiftBlock_edgegroups", ob, "swiftBlock_edgegroup_index", rows=max(len(ob.swiftBlock_edgegroups),2)) 243 | row = split.column() 244 | row.operator("swift_block.edgegroups_action", text="", icon='ADD').action = 'ADD' 245 | row.operator("swift_block.edgegroups_action", text="", icon='REMOVE').action = 'DELETE' 246 | row = box.split(align=True) 247 | row.operator("swift_block.edgegroups_action", text="Assign").action = 'ASSIGN' 248 | row.operator("swift_block.edgegroups_action", text="Remove").action = 'REMOVE' 249 | row.operator("swift_block.edgegroups_action", text="Select").action = 'SELECT' 250 | row.operator("swift_block.edgegroups_action", text="Deselect").action = 'DESELECT' 251 | 252 | box = self.layout.box() 253 | box.label(text="Projections") 254 | split = box.split() 255 | split.prop(ob, "swiftBlock_ProjectionObject", text="", icon="OUTLINER_OB_SURFACE") 256 | split.operator("swift_block.add_projections", text="Add") 257 | split.operator("swift_block.remove_projections", text="Remove") 258 | if ob.swiftBlock_Mesher == "blockMeshBodyFit": 259 | box.prop(ob, 'swiftBlock_SearchLength') 260 | box.template_list("SWIFTBLOCK_UL_projection_items", "", ob, "swiftBlock_projections", ob, "swiftBlock_projection_index", rows=2) 261 | if ob.swiftBlock_Autosnap: 262 | split = box.split(factor=0.1) 263 | split.prop(ob, "swiftBlock_Autosnap", text="") 264 | split = split.split(factor=0.9) 265 | split.prop(ob, "swiftBlock_EdgeSnapObject", text="") 266 | if ob.swiftBlock_EdgeSnapObject != "": 267 | o = split.operator("swift_block.activate_snap",text="",emboss=False,icon="OBJECT_DATA") 268 | o.ob = ob.swiftBlock_EdgeSnapObject 269 | else: 270 | box.prop(ob, "swiftBlock_Autosnap") 271 | box.prop(ob,"swiftBlock_ShowInternalFaces") 272 | 273 | 274 | box = self.layout.box() 275 | box.label(text="Boundary Patches") 276 | row = box.row() 277 | row.template_list("SWIFTBLOCK_UL_boundary_items", "", ob.data, "materials", ob, "swiftBlock_boundary_index", rows=2) 278 | col = row.column(align=True) 279 | col.operator("swift_block.boundaries_action", icon='ZOOM_IN', text="").action = 'ADD' 280 | col.operator("swift_block.boundaries_action", icon='ZOOM_OUT', text="").action = 'REMOVE' 281 | row = self.layout.row() 282 | row.operator("swift_block.boundaries_action", text="Assign").action = 'ASSIGN' 283 | 284 | # For the lists in GUI 285 | class SWIFTBLOCK_UL_block_items(bpy.types.UIList): 286 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 287 | split = layout.split(factor=0.9) 288 | block = context.active_object.swiftBlock_blocks[index] 289 | name = block.name + ' %d'%index 290 | c = split.operator("swift_block.edit_block", text=name, emboss=False, icon="UV_FACESEL") 291 | c.blockid = index 292 | c.name = block.name 293 | 294 | if block.enabled: 295 | c = split.operator("swift_block.enable_block", text='',emboss=False,icon="CHECKBOX_HLT").blockid = index 296 | else: 297 | c = split.operator("swift_block.enable_block", text='', emboss=False,icon="CHECKBOX_DEHLT").blockid = index 298 | 299 | class SWIFTBLOCK_UL_boundary_items(bpy.types.UIList): 300 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 301 | mat = data.materials[index] 302 | split = layout.split(factor=0.2) 303 | split.prop(item, "diffuse_color", text='') 304 | split.prop(item, "name", text='', emboss = False) 305 | split.prop(item, "boundary_type", text='', emboss = False) 306 | 307 | class SWIFTBLOCK_OT_BoundariesAction(bpy.types.Operator): 308 | bl_idname = "swift_block.boundaries_action" 309 | bl_label = "Boundary Action" 310 | bl_description = "Runs Action on Selected Boundary" 311 | 312 | action: bpy.props.EnumProperty( 313 | items=( 314 | ('REMOVE', "Remove", ""), 315 | ('ADD', "Add", ""), 316 | ('ASSIGN', "Assign", ""), 317 | ) 318 | ) 319 | 320 | def invoke(self, context, event): 321 | 322 | ob = context.active_object 323 | bm = bmesh.from_edit_mesh(ob.data) 324 | 325 | if self.action == 'REMOVE': 326 | mat_name = ob.active_material.name 327 | ob.data.materials.pop(index=ob.active_material_index) 328 | if not bpy.data.materials[mat_name].users: 329 | bpy.data.materials.remove(bpy.data.materials[mat_name]) 330 | 331 | elif self.action == 'ASSIGN': 332 | for f in bm.faces: 333 | if f.select: 334 | f.material_index = ob.swiftBlock_boundary_index 335 | ob.data.update() 336 | 337 | if self.action == 'ADD': 338 | name = 'default' 339 | mat = bpy.data.materials.new(name) 340 | color = patchColor(len(ob.data.materials)) 341 | mat.diffuse_color = color 342 | ob.data.materials.append(mat) 343 | material_index = len(ob.data.materials) - 1 344 | 345 | for f in bm.faces: 346 | if f.select: 347 | f.material_index = material_index 348 | ob.swiftBlock_boundary_index = material_index 349 | ob.active_material_index = material_index 350 | ob.data.update() 351 | 352 | return {"FINISHED"} 353 | 354 | class SWIFTBLOCK_UL_projection_items(bpy.types.UIList): 355 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 356 | split = layout.split(factor=0.4) 357 | proj = context.active_object.swiftBlock_projections[index] 358 | if proj.type == 'vert2surf': 359 | icon = "VERTEXSEL" 360 | elif proj.type == 'edge2surf': 361 | icon = "EDGESEL" 362 | elif proj.type == 'face2surf': 363 | icon = "FACESEL" 364 | c = split.operator("swift_block.get_projection", text='{}{}'.format(proj.type[0],proj.id), emboss=False, icon=icon) 365 | c.type = proj.type 366 | c.id = proj.id 367 | split = split.split(factor=0.6) 368 | c = split.operator("swift_block.activate_snap", text=proj.ob, emboss=False, icon="OBJECT_DATA") 369 | c.ob = proj.ob 370 | c = split.operator("swift_block.remove_projection", text='', emboss = False, icon='X') 371 | c.proj_id = index 372 | 373 | 374 | class SWIFTBLOCK_UL_edgegroup_items(bpy.types.UIList): 375 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 376 | # eg = context.active_object.swiftBlock_edgegroups[index] 377 | layout.prop(item, "name", text='', emboss = False) 378 | 379 | 380 | #Edge grouping function for creating, removing and selecting edges in groups 381 | class SWIFTBLOCK_OT_EdgeGroupsAction(bpy.types.Operator): 382 | bl_idname = "swift_block.edgegroups_action" 383 | bl_label = "Edge group action" 384 | bl_description = "Runs Action on selected edges" 385 | 386 | action: bpy.props.EnumProperty( 387 | items=( 388 | ('ADD', "Add", ""), 389 | ('DELETE', "Delete", ""), 390 | ('ASSIGN', "Assign", ""), 391 | ('REMOVE', "Remove", ""), 392 | ('SELECT', "Select", ""), 393 | ('DESELECT', "Deselect", ""), 394 | ) 395 | ) 396 | 397 | def invoke(self, context, event): 398 | ob = context.active_object 399 | bm = bmesh.from_edit_mesh(ob.data) 400 | egLayer = bm.edges.layers.int.get("edgegroup") 401 | index = ob.swiftBlock_edgegroup_index 402 | 403 | if self.action == 'ADD': 404 | index = len(ob.swiftBlock_edgegroups) 405 | neweg = ob.swiftBlock_edgegroups.add() 406 | neweg.name = 'EdgeGroup_{}'.format(index) 407 | 408 | if self.action == 'DELETE': 409 | ob.swiftBlock_edgegroups.remove(index) 410 | for e in bm.edges: 411 | if e[egLayer] == index: 412 | e[egLayer] = -1 413 | elif e[egLayer] > index: 414 | e[egLayer] -= 1 415 | 416 | if self.action == 'ASSIGN': 417 | for e in bm.edges: 418 | if e.select: 419 | e[egLayer] = index 420 | 421 | if self.action == 'REMOVE': 422 | for e in bm.edges: 423 | if e.select: 424 | e[egLayer] = -1 425 | 426 | if self.action == 'SELECT': 427 | for e in bm.edges: 428 | if e[egLayer] == index: 429 | e.select = True 430 | 431 | if self.action == 'DESELECT': 432 | for e in bm.edges: 433 | if e.select and e[egLayer] == index: 434 | e.select = False 435 | 436 | ob.data.update() 437 | return {"FINISHED"} 438 | 439 | # Initialize all the bmesh layer properties for the blocking object 440 | class SWIFTBLOCK_OT_InitBlocking(bpy.types.Operator): 441 | bl_idname = "swift_block.init_blocking" 442 | bl_label = "Initialize Object" 443 | bl_description = "Initializes the Active Object for SwiftBlock" 444 | bl_options = {"UNDO"} 445 | 446 | def invoke(self, context, event): 447 | print("initialize BMesh") 448 | 449 | bpy.ops.object.mode_set(mode='EDIT') 450 | ob = bpy.context.active_object 451 | bm = bmesh.from_edit_mesh(ob.data) 452 | 453 | bm.edges.layers.string.new("type") 454 | bm.edges.layers.float.new("x1") 455 | bm.edges.layers.float.new("x2") 456 | bm.edges.layers.float.new("r1") 457 | bm.edges.layers.float.new("r2") 458 | bm.edges.layers.float.new("dx") 459 | bm.edges.layers.float.new("ratio") 460 | bm.edges.layers.int.new("cells") 461 | bm.edges.layers.int.new("groupid") 462 | bm.edges.layers.int.new("modtime") 463 | bm.edges.layers.int.new("edgegroup") 464 | 465 | bm.faces.layers.int.new('pos') # block number on positive side of the face, -1 boundary face 466 | bm.faces.layers.int.new('neg') # block number on negative side of the face, -1 boundary face 467 | bm.faces.layers.int.new('enabled') # 0 = disabled, 1 = boundary face, 2 = internal face 468 | 469 | ob.swiftBlock_blocks.clear() 470 | ob.swiftBlock_projections.clear() 471 | # ob.swiftBlock_edge_groups.clear() 472 | bpy.ops.swift_block.boundaries_action("INVOKE_DEFAULT",action='ADD') 473 | 474 | ob.swiftBlock_isblockingObject = True 475 | ob.data.update() 476 | ob.show_all_edges = True 477 | ob.show_wire = True 478 | return {"FINISHED"} 479 | 480 | # Blocking and previewing operators 481 | # Automatical block detection. 482 | class SWIFTBLOCK_OT_BuildBlocking(bpy.types.Operator): 483 | bl_idname = "swift_block.build_blocking" 484 | bl_label = "Build" 485 | bl_description = "Generates Blocks from Mesh (Main Routine)" 486 | bl_options = {"UNDO"} 487 | 488 | def invoke(self, context, event): 489 | ob = context.active_object 490 | bm = bmesh.from_edit_mesh(ob.data) 491 | 492 | verts = [] 493 | edges = [] 494 | 495 | for v in bm.verts: 496 | verts.append(v.co) 497 | for e in bm.edges: 498 | edges.append([e.verts[0].index,e.verts[1].index]) 499 | 500 | disabled = [] #not needed anymore 501 | 502 | print('Beginning automatic block detection') 503 | stime = time.time() 504 | log, block_verts, block_edges, face_info, all_edges, faces_as_list_of_nodes = blockBuilder.blockFinder(edges, verts, disabled = disabled, numba = ob.swiftBlock_useNumba) 505 | print('Found {} blocks in {:.1f} seconds, used Numba={}'.format(len(block_verts), time.time()-stime,ob.swiftBlock_useNumba)) 506 | 507 | 508 | ob.swiftBlock_blocks.clear() 509 | for i,bv in enumerate(block_verts): 510 | b = ob.swiftBlock_blocks.add() 511 | b.id = i 512 | b.name = 'block'#_{}'.format(i) 513 | b.verts = bv 514 | 515 | groupl = bm.edges.layers.int.get('groupid') 516 | bm.verts.ensure_lookup_table() 517 | 518 | for i, g in enumerate(block_edges): 519 | for e in g: 520 | bme = bm.edges.get((bm.verts[e[0]],bm.verts[e[1]])) 521 | bme[groupl] = i 522 | 523 | # A bug in face_info when there are o-grids. The block indices after o-grid block have to be decreased by one. 524 | replace_ids = dict() 525 | block_ids = [] 526 | for key in face_info.keys(): 527 | block_ids.extend(face_info[key]['pos']) 528 | block_ids.extend(face_info[key]['neg']) 529 | block_ids = sorted(set(block_ids)) 530 | nblocks = len(ob.swiftBlock_blocks)-1 531 | 532 | decrease = [] 533 | if nblocks < max(block_ids): 534 | for i in range(max(block_ids)): 535 | if i not in block_ids: 536 | decrease.append(i) 537 | 538 | negl = bm.faces.layers.int.get('neg') 539 | posl = bm.faces.layers.int.get('pos') 540 | enabledl = bm.faces.layers.int.get('enabled') 541 | 542 | block_faces = [] 543 | 544 | for key, value in face_info.items(): 545 | # probably a bug, some extra faces which do not belong to any block 546 | if not value['neg'] and not value['pos']: 547 | continue 548 | verts = [bm.verts[v] for v in faces_as_list_of_nodes[key]] 549 | f = bm.faces.get(verts) 550 | if not f: 551 | f = bm.faces.new(verts) 552 | f[enabledl] = -1 553 | block_faces.append(f) 554 | if value['pos']: 555 | f[posl] = value['pos'][0] 556 | dec = sum(x < f[posl] for x in decrease) 557 | f[posl] -= dec 558 | else: 559 | f[posl] = -1 560 | if value['neg']: 561 | f[negl] = value['neg'][0] 562 | dec = sum(x < f[negl] for x in decrease) 563 | f[negl] -= dec 564 | else: 565 | f[negl] = -1 566 | 567 | for f in bm.faces: 568 | if not f in block_faces: 569 | bm.faces.remove(f) 570 | 571 | 572 | bpy.ops.object.mode_set(mode='OBJECT') 573 | 574 | edgeDirections = utils.getEdgeDirections(block_verts, block_edges) 575 | 576 | ob = bpy.context.active_object 577 | edgelist = dict() 578 | for e in ob.data.edges: 579 | edgelist[(e.vertices[0],e.vertices[1])] = e.index 580 | for ed in edgeDirections: 581 | # consistentEdgeDirs(ed) 582 | for e in ed: 583 | if (e[0],e[1]) not in edgelist: 584 | ei = ob.data.edges[edgelist[(e[1],e[0])]] 585 | (e0, e1) = ei.vertices 586 | ei.vertices = (e1, e0) 587 | bpy.ops.object.mode_set(mode='EDIT') 588 | updateProjections(ob) 589 | hideFacesEdges(ob, ob.swiftBlock_ShowInternalFaces) 590 | bpy.ops.swift_block.draw_edge_directions('INVOKE_DEFAULT',show=False) 591 | self.report({'INFO'}, "Number of blocks: {}".format(len(block_verts))) 592 | return {"FINISHED"} 593 | 594 | 595 | class SWIFTBLOCK_OT_PreviewMesh(bpy.types.Operator): 596 | bl_idname = "swift_block.preview_mesh" 597 | bl_label = "Preview" 598 | bl_description = "Preview the Blocking Result" 599 | bl_options = {"UNDO"} 600 | 601 | filename: bpy.props.StringProperty(default='') 602 | 603 | def invoke(self, context, event): 604 | ob = context.active_object 605 | mesh, cells = writeMesh(ob) 606 | points, faces = mesh.runMesh() 607 | if points == []: 608 | self.report({'ERROR'}, "blockMesh command not found! Preview is unavailable. Source OpenFOAM in terminal and start Blender from that terminal to enable previewing.") 609 | else: 610 | self.report({'INFO'}, "Cells in mesh: " + str(cells)) 611 | blender_utils.previewMesh(ob, points, faces) 612 | return {"FINISHED"} 613 | 614 | class SWIFTBLOCK_OT_WriteMesh(bpy.types.Operator): 615 | bl_idname = "swift_block.write_mesh" 616 | bl_label = "Export" 617 | bl_description = "Generates OpenFOAM Files to a Case Folder" 618 | 619 | filepath: bpy.props.StringProperty(subtype='DIR_PATH') 620 | # filepath = bpy.props.StringProperty( 621 | # name="File Path", 622 | # description="Filepath used for exporting the file", 623 | # maxlen=1024, 624 | # subtype='FILE_PATH', 625 | # default='/opt', 626 | # ) 627 | check_existing: bpy.props.BoolProperty( 628 | name="Check Existing", 629 | description="Check and warn on overwriting existing files", 630 | default=True, 631 | options={'HIDDEN'}, 632 | ) 633 | 634 | # use_filter_folder = True 635 | 636 | def invoke(self, context, event): 637 | bpy.context.window_manager.fileselect_add(self) 638 | return {'RUNNING_MODAL'} 639 | 640 | def execute(self, context): 641 | ob = context.active_object 642 | mesh, cells = writeMesh(ob, self.filepath) 643 | bpy.ops.object.mode_set(mode='EDIT') 644 | self.report({'INFO'}, "Cells in mesh: " + str(cells)) 645 | return {"FINISHED"} 646 | 647 | class SWIFTBLOCK_OT_ActivateBlocking(bpy.types.Operator): 648 | bl_idname = "swift_block.activate_blocking" 649 | bl_label = "Return to SwiftBlock" 650 | bl_description = "Go Back to SwiftBlock Settings" 651 | bl_options = {"UNDO"} 652 | 653 | hide: bpy.props.BoolProperty() 654 | 655 | def invoke(self, context, event): 656 | ob = context.active_object 657 | bob = bpy.data.objects[ob.swiftBlock_blocking_object] 658 | blender_utils.activateObject(bob, self.hide) 659 | return {'FINISHED'} 660 | 661 | class SWIFTBLOCK_OT_GetBlock(bpy.types.Operator): 662 | """Get block from selection""" 663 | bl_idname = "swift_block.get_block" 664 | bl_label = "Get Block from Selection" 665 | bl_description = "Identifies the Block from Active Selection" 666 | bl_options = {'REGISTER', 'UNDO'} 667 | 668 | def invoke(self, context, event): 669 | ob = bpy.context.active_object 670 | bm = bmesh.from_edit_mesh(ob.data) 671 | selection = [] 672 | for v in bm.verts: 673 | if v.select: 674 | selection.append(v.index) 675 | block = False 676 | occs = [] 677 | for b in ob.swiftBlock_blocks: 678 | occ = [v in selection for v in b.verts].count(True) 679 | if occ == 8: 680 | block = b 681 | break 682 | else: 683 | occs.append(occ) 684 | if not block: 685 | max_occ = max(enumerate(occs), key=lambda x:x[1])[0] 686 | block = ob.swiftBlock_blocks[max_occ] 687 | if not block: 688 | self.report({'INFO'}, "No block found with selected vertices") 689 | return {'CANCELLED'} 690 | bpy.ops.swift_block.edit_block('INVOKE_DEFAULT', blockid=block.id, name = block.name ) 691 | return {'FINISHED'} 692 | 693 | class SWIFTBLOCK_OT_EditBlock(bpy.types.Operator): 694 | bl_idname = "swift_block.edit_block" 695 | bl_label = "Select Block" 696 | bl_description = "Selects Block for Editing" 697 | bl_options = {'REGISTER', 'UNDO'} 698 | 699 | 700 | blockid: bpy.props.IntProperty(name='id') 701 | namedRegion: bpy.props.BoolProperty(name='Named region', default = False) 702 | name: bpy.props.StringProperty(name='name') 703 | 704 | def draw(self, context): 705 | ob = context.active_object 706 | if not ob.swiftBlock_blocks[self.blockid].enabled: 707 | return 708 | col = self.layout.column(align = True) 709 | # col.prop(self, "enabled") 710 | # split = col.split(factor=0.1, align=True) 711 | # col = split.column() 712 | col.prop(self, "namedRegion") 713 | if self.namedRegion: 714 | # col = split.column() 715 | col.prop(self, "name") 716 | 717 | # this could be used to select multiple blocks 718 | def invoke(self, context, event): 719 | ob = context.active_object 720 | ob.swiftBlock_block_index = self.blockid 721 | if event.shift: 722 | self.shiftDown = True 723 | else: 724 | self.shiftDown = False 725 | self.execute(context) 726 | return {'FINISHED'} 727 | 728 | def execute(self, context): 729 | bpy.ops.mesh.select_all(action="DESELECT") 730 | ob = context.active_object 731 | ob.swiftBlock_blocks[self.blockid].name = self.name 732 | ob.swiftBlock_blocks[self.blockid].namedRegion = self.namedRegion 733 | # OK to remove? ob = context.active_object 734 | 735 | verts = ob.swiftBlock_blocks[self.blockid].verts 736 | 737 | bm = bmesh.from_edit_mesh(ob.data) 738 | bm.verts.ensure_lookup_table() 739 | for v in verts: 740 | bm.verts[v].select = True 741 | for e in bm.edges: 742 | if e.verts[0].select and e.verts[1].select: 743 | e.select = True 744 | for f in bm.faces: 745 | if len(f.verts) == 4 and sum([v.select for v in f.verts]) == 4: 746 | f.select = True 747 | ob.data.update() 748 | return {'FINISHED'} 749 | 750 | class SWIFTBLOCK_OT_EnableBlock(bpy.types.Operator): 751 | bl_idname = "swift_block.enable_block" 752 | bl_label = "Include in Build Blocking" 753 | bl_description = "Option to Include This Block in Build Blocking" 754 | 755 | blockid: bpy.props.IntProperty() 756 | 757 | def execute(self, context): 758 | ob = context.active_object 759 | block = ob.swiftBlock_blocks[self.blockid] 760 | ob.swiftBlock_block_index = self.blockid 761 | 762 | if block.enabled: 763 | block.enabled = False 764 | else: 765 | block.enabled = True 766 | # repair_blockFacesEdges(ob) 767 | hideFacesEdges(ob) 768 | 769 | return {'FINISHED'} 770 | 771 | # Mapping operators 772 | 773 | # Change the layer properties of currently selected edges 774 | class SWIFTBLOCK_OT_SetEdge(bpy.types.Operator): 775 | """Set mapping for the edge""" 776 | bl_idname = "swift_block.set_edge" 777 | bl_label = "Set Params" 778 | bl_description = "Set Parameters for Currently Selected Edges" 779 | bl_options = {"UNDO"} 780 | 781 | def execute(self, context): 782 | ob = context.active_object 783 | if not ob.swiftBlock_blocks: 784 | bpy.ops.swift_block.build_blocking('INVOKE_DEFAULT') 785 | 786 | bm = bmesh.from_edit_mesh(ob.data) 787 | typel = bm.edges.layers.string.get('type') 788 | cellsl = bm.edges.layers.int.get('cells') 789 | x1l = bm.edges.layers.float.get('x1') 790 | x2l = bm.edges.layers.float.get('x2') 791 | r1l = bm.edges.layers.float.get('r1') 792 | r2l = bm.edges.layers.float.get('r2') 793 | rl = bm.edges.layers.float.get('ratio') 794 | groupl = bm.edges.layers.int.get('groupid') 795 | timel = bm.edges.layers.int.get('modtime') 796 | 797 | for e in bm.edges: 798 | if e.select: 799 | e[typel] = str.encode(ob.swiftBlock_MappingType) 800 | e[cellsl] = ob.swiftBlock_Cells 801 | e[timel] = int(time.time()) 802 | if ob.swiftBlock_MappingType == "Geometric MG": 803 | e[x1l] = ob.swiftBlock_x1 804 | e[x2l] = ob.swiftBlock_x2 805 | e[r1l] = ob.swiftBlock_r1 806 | e[r2l] = ob.swiftBlock_r2 807 | elif ob.swiftBlock_MappingType == "Geometric": 808 | e[rl] = ob.swiftBlock_Ratio 809 | return {'FINISHED'} 810 | 811 | class SWIFTBLOCK_OT_GetEdge(bpy.types.Operator): 812 | bl_idname = "swift_block.get_edge" 813 | bl_label = "Get Params" 814 | bl_description = "Get Parameter Values from Active Edge" 815 | bl_options = {"UNDO"} 816 | 817 | def execute(self, context): 818 | ob = context.active_object 819 | if not ob.swiftBlock_blocks: 820 | bpy.ops.swift_block.build_blocking('INVOKE_DEFAULT') 821 | 822 | bm = bmesh.from_edit_mesh(ob.data) 823 | typel = bm.edges.layers.string.get('type') 824 | x1l = bm.edges.layers.float.get('x1') 825 | x2l = bm.edges.layers.float.get('x2') 826 | r1l = bm.edges.layers.float.get('r1') 827 | r2l = bm.edges.layers.float.get('r2') 828 | cellsl = bm.edges.layers.int.get('cells') 829 | 830 | for e in bm.edges: 831 | if e.select: 832 | # e[typel] = str.encode(ob.swiftBlock_MappingType) 833 | ob.swiftBlock_Cells = e[cellsl] 834 | ob.swiftBlock_x1 = e[x1l] 835 | ob.swiftBlock_x2 = e[x2l] 836 | ob.swiftBlock_r1 = e[r1l] 837 | ob.swiftBlock_r2 = e[r2l] 838 | return {'FINISHED'} 839 | 840 | class SWIFTBLOCK_OT_SetCellSize(bpy.types.Operator): 841 | """Calculates the number of cells from maximum cell size""" 842 | bl_idname = "swift_block.set_cellsize" 843 | bl_label = "Set Cell Size" 844 | bl_description = "Set Cell Size" 845 | bl_options = {"UNDO"} 846 | 847 | def execute(self, context): 848 | ob = context.active_object 849 | bm = bmesh.from_edit_mesh(ob.data) 850 | typel = bm.edges.layers.string.get('type') 851 | x1l = bm.edges.layers.float.get('x1') 852 | x2l = bm.edges.layers.float.get('x2') 853 | r1l = bm.edges.layers.float.get('r1') 854 | r2l = bm.edges.layers.float.get('r2') 855 | cellsl = bm.edges.layers.int.get('cells') 856 | verts = [v.co for v in bm.verts] 857 | edges = [(e.verts[0].index, e.verts[1].index) for e in bm.edges] 858 | 859 | if ob.swiftBlock_Autosnap and ob.swiftBlock_EdgeSnapObject: 860 | polyLines, polyLinesPoints, lengths = getPolyLines(verts, edges, ob) 861 | else: 862 | polyLines = [] 863 | lengths = [[]] 864 | 865 | 866 | for e in bm.edges: 867 | if e.select: 868 | ev = list([e.verts[0].index,e.verts[1].index]) 869 | if ev in lengths[0]: 870 | ind = lengths[0].index(ev) 871 | L = lengths[1][ind] 872 | else: 873 | L = (e.verts[0].co-e.verts[1].co).length 874 | 875 | e[typel] = str.encode(ob.swiftBlock_MappingType) 876 | N=utils.getCells(ob.swiftBlock_x1,ob.swiftBlock_x2,ob.swiftBlock_r1,ob.swiftBlock_r2,L,ob.swiftBlock_Dx) 877 | e[cellsl] = N 878 | e[x1l] = ob.swiftBlock_x1 879 | e[x2l] = ob.swiftBlock_x2 880 | e[r1l] = ob.swiftBlock_r1 881 | e[r2l] = ob.swiftBlock_r2 882 | print(N) 883 | return {'FINISHED'} 884 | 885 | filename: bpy.props.StringProperty(default='') 886 | 887 | class SWIFTBLOCK_OT_EdgeSelectParallel(bpy.types.Operator): 888 | bl_idname = "swift_block.edge_select_parallel" 889 | bl_label = "Select Group" 890 | bl_description = "Selects All Edges in Active Edge Group" 891 | 892 | def execute(self, context): 893 | ob = context.active_object 894 | bm = bmesh.from_edit_mesh(ob.data) 895 | groupl = bm.edges.layers.int.get('groupid') 896 | for e in bm.edges: 897 | if e.select: 898 | groupid = e[groupl] 899 | for i in bm.edges: 900 | if i[groupl] == groupid: 901 | i.select = True 902 | ob.data.update() 903 | return {'FINISHED'} 904 | 905 | class SWIFTBLOCK_OT_FlipEdges(bpy.types.Operator): 906 | """Flips parallel edges, select only one edge per group""" 907 | bl_idname = "swift_block.flip_edges" 908 | bl_label = "Flip Dir" 909 | bl_description = "Flip Edge Direction" 910 | 911 | def execute(self, context): 912 | ob = context.active_object 913 | bm = bmesh.from_edit_mesh(ob.data) 914 | groupl = bm.edges.layers.int.get('groupid') 915 | flip_edges = [] 916 | for e in bm.edges: 917 | if e.select: 918 | groupid = e[groupl] 919 | for i in bm.edges: 920 | if i[groupl] == groupid: 921 | flip_edges.append(i.index) 922 | break 923 | bpy.ops.object.mode_set(mode='OBJECT') 924 | for fe in flip_edges: 925 | e = ob.data.edges[fe] 926 | (e0,e1) = e.vertices 927 | e.vertices = (e1,e0) 928 | bpy.ops.object.mode_set(mode='EDIT') 929 | bpy.ops.swift_block.draw_edge_directions('INVOKE_DEFAULT',show=False) 930 | return {'FINISHED'} 931 | 932 | 933 | # Projection operators 934 | # TODO Projections are saved to a Blender CollectionProperty. At the 935 | # moment if verts, edges or faces have been deleted, the id might not be 936 | # up to date anymore. It would make sense to save the projections to bmesh 937 | # layer. 938 | class SWIFTBLOCK_OT_GetProjection(bpy.types.Operator): 939 | bl_idname = "swift_block.get_projection" 940 | bl_label = "Get Projection" 941 | bl_description = "Get Projection" 942 | 943 | id: bpy.props.IntProperty() 944 | type: bpy.props.StringProperty() 945 | 946 | def invoke(self, context, event): 947 | if not event.shift: 948 | bpy.ops.mesh.select_all(action='DESELECT') 949 | self.execute(context) 950 | return {'FINISHED'} 951 | 952 | 953 | def execute(self, context): 954 | ob = context.active_object 955 | bm = bmesh.from_edit_mesh(ob.data) 956 | if self.type == 'vert2surf': 957 | bm.verts.ensure_lookup_table() 958 | bm.verts[self.id].select = True 959 | elif self.type == 'edge2surf': 960 | bm.edges[self.id].select = True 961 | elif self.type == 'face2surf': 962 | bm.faces[self.id].select = True 963 | ob.data.update() 964 | return {'FINISHED'} 965 | 966 | class SWIFTBLOCK_OT_AddProjections(bpy.types.Operator): 967 | bl_idname = "swift_block.add_projections" 968 | bl_label = "Project to Selected Object" 969 | bl_description = "Project to Selected Object" 970 | bl_options = {"REGISTER","UNDO"} 971 | 972 | pob: bpy.props.EnumProperty(name="Projection Object", 973 | items=getProjectionObjects, description = "Projection Object") 974 | 975 | verts: bpy.props.BoolProperty(default=True) 976 | edges: bpy.props.BoolProperty(default=True) 977 | faces: bpy.props.BoolProperty(default=True) 978 | 979 | def invoke(self, context, event): 980 | ob = context.active_object 981 | self.pob = ob.swiftBlock_ProjectionObject 982 | self.added = 1 983 | return self.execute(context) 984 | 985 | def execute(self, context): 986 | def projectionExists(ob, ptype, index, pob): 987 | for p in ob.swiftBlock_projections: 988 | if p.type == ptype and p.id == index and p.ob == pob: 989 | return True 990 | return False 991 | def addProjection(ptype, index): 992 | for p in ob.swiftBlock_projections: 993 | if p.type == ptype and p.id == index and p.ob == self.pob: 994 | return 995 | newp = ob.swiftBlock_projections.add() 996 | newp.type = ptype 997 | newp.id = index 998 | newp.ob = self.pob 999 | self.added += 1 1000 | 1001 | ob = context.active_object 1002 | bm = bmesh.from_edit_mesh(ob.data) 1003 | 1004 | if not self.pob: 1005 | return {"CANCELLED"} 1006 | 1007 | np = len(ob.swiftBlock_projections) 1008 | for i in range(self.added): 1009 | ob.swiftBlock_projections.remove(np-i) 1010 | 1011 | for v in bm.verts: 1012 | if v.select and self.verts: 1013 | addProjection('vert2surf', v.index) 1014 | 1015 | for e in bm.edges: 1016 | if e.select and self.edges: 1017 | addProjection('edge2surf', e.index) 1018 | 1019 | for f in bm.faces: 1020 | if f.select and self.faces: 1021 | addProjection('face2surf', f.index) 1022 | return {"FINISHED"} 1023 | 1024 | class SWIFTBLOCK_OT_RemoveProjection(bpy.types.Operator): 1025 | bl_idname = "swift_block.remove_projection" 1026 | bl_label = "Remove Projection" 1027 | bl_description = "Remove Projection" 1028 | bl_options = {"UNDO"} 1029 | 1030 | proj_id: bpy.props.IntProperty(default = -1) 1031 | 1032 | def execute(self, context): 1033 | ob = context.active_object 1034 | if self.proj_id != -1: 1035 | ob.swiftBlock_projections.remove(self.proj_id) 1036 | return {"FINISHED"} 1037 | 1038 | class SWIFTBLOCK_OT_RemoveProjections(bpy.types.Operator): 1039 | bl_idname = "swift_block.remove_projections" 1040 | bl_label = "Remove Projections" 1041 | bl_description = "Remove All Projections" 1042 | bl_options = {"UNDO"} 1043 | 1044 | 1045 | def execute(self, context): 1046 | ob = context.active_object 1047 | bm = bmesh.from_edit_mesh(ob.data) 1048 | 1049 | remove_projections = [] 1050 | 1051 | for v in bm.verts: 1052 | if v.select: 1053 | for i,p in enumerate(ob.swiftBlock_projections): 1054 | if p.type == 'vert2surf' and p.id == v.index: 1055 | remove_projections.append(i) 1056 | for e in bm.edges: 1057 | if e.select: 1058 | for i,p in enumerate(ob.swiftBlock_projections): 1059 | if p.type == 'edge2surf' and p.id == e.index: 1060 | remove_projections.append(i) 1061 | for f in bm.faces: 1062 | if f.select: 1063 | for i,p in enumerate(ob.swiftBlock_projections): 1064 | if p.type == 'face2surf' and p.id == f.index: 1065 | remove_projections.append(i) 1066 | remove_projections = reversed(sorted(remove_projections)) 1067 | for i in remove_projections: 1068 | ob.swiftBlock_projections.remove(i) 1069 | return {"FINISHED"} 1070 | 1071 | class SWIFTBLOCK_OT_ActivateSnap(bpy.types.Operator): 1072 | bl_idname = "swift_block.activate_snap" 1073 | bl_label = "Activate Snapping Object" 1074 | bl_description = "Activate Snapping Object" 1075 | bl_options = {"UNDO"} 1076 | 1077 | ob: bpy.props.StringProperty() 1078 | 1079 | 1080 | def invoke(self, context, event): 1081 | ob = context.active_object 1082 | pob = bpy.data.objects[self.ob] 1083 | pob.swiftBlock_blocking_object = ob.name 1084 | blender_utils.activateObject(pob, False) 1085 | return {'FINISHED'} 1086 | 1087 | class SWIFTBLOCK_OT_EdgetoPolyLine(bpy.types.Operator): 1088 | bl_idname = "swift_block.edge_to_polyline" 1089 | bl_label = "Project Edge to Polyline" 1090 | bl_description = "Project Edge to Polyline" 1091 | bl_options = {"REGISTER", "UNDO"} 1092 | 1093 | def invoke(self, context, event): 1094 | self.ob = context.active_object 1095 | bm = bmesh.from_edit_mesh(self.ob.data) 1096 | 1097 | for e in bm.edges: 1098 | if e.select: 1099 | self.edge = e.index 1100 | 1101 | self.proj_ob = bpy.data.objects[self.ob.swiftBlock_ProjectionObject] 1102 | blender_utils.activateObject(self.proj_ob) 1103 | context.window_manager.modal_handler_add(self) 1104 | 1105 | return {'RUNNING_MODAL'} 1106 | 1107 | def modal(self, context, event): 1108 | if event.type in {'RIGHTMOUSE', 'RETURN'}: 1109 | bm = bmesh.from_edit_mesh(self.proj_ob.data) 1110 | selected_edges = [] 1111 | projl = bm.edges.layers.string.get("projectionEdgeId") 1112 | if not projl: 1113 | projl = bm.edges.layers.int.new("projectionEdgeId") 1114 | self.proj_ob.data.update() 1115 | for e in bm.edges: 1116 | if e.select: 1117 | e[projl] = self.edge 1118 | blender_utils.activateObject(self.ob) 1119 | return {'FINISHED'} 1120 | elif event.type in 'ESC': 1121 | return {'CANCELLED'} 1122 | else: 1123 | return {'PASS_THROUGH'} 1124 | 1125 | class SWIFTBLOCK_OT_ExtrudeBlocks(bpy.types.Operator): 1126 | """Extrude blocks without removing internal edges""" 1127 | bl_idname = "swift_block.extrude_blocks" 1128 | bl_label = "Extrude Blocks (Retain Internal Edges)" 1129 | bl_description = "Extrude Blocks without Removing Internal Edges (SwiftBlock)" 1130 | bl_options = {'REGISTER', 'UNDO'} 1131 | 1132 | def execute(self, context): 1133 | ob = bpy.context.active_object 1134 | bm = bmesh.from_edit_mesh(ob.data) 1135 | sel_mode = bpy.context.scene.tool_settings.mesh_select_mode 1136 | bpy.context.scene.tool_settings.mesh_select_mode = (False, True, False) 1137 | selected_faces = [] 1138 | for f in bm.faces: 1139 | if f.select: 1140 | selected_faces.append((f,[v for v in f.verts])) 1141 | bpy.ops.mesh.extrude_faces_move() 1142 | for f in selected_faces: 1143 | newf = bm.faces.new(f[1]) 1144 | for p in ob.swiftBlock_projections: 1145 | if p.type == 'face' and p.id == f[0].index: 1146 | p.id = newf.index 1147 | bpy.ops.mesh.remove_doubles(threshold=0.0001, use_unselected=False) 1148 | ob.data.update() 1149 | bpy.ops.transform.translate('INVOKE_REGION_WIN') 1150 | return {"FINISHED"} 1151 | 1152 | class SWIFTBLOCK_OT_DrawEdgeDirections(bpy.types.Operator): 1153 | """Draw edge directions""" 1154 | bl_idname = "swift_block.draw_edge_directions" 1155 | bl_label = "Draw Edge Directions" 1156 | bl_description = "Draw Edge Directions" 1157 | bl_options = {'REGISTER', 'UNDO'} 1158 | 1159 | show: bpy.props.BoolProperty(default=True) 1160 | size: bpy.props.FloatProperty(default=0,min=0) 1161 | verts: bpy.props.IntProperty(default=12,min=0) 1162 | relativeSize: bpy.props.BoolProperty(default=True) 1163 | 1164 | def invoke(self, context, event): 1165 | self.bob = bpy.context.active_object 1166 | bm = bmesh.from_edit_mesh(self.bob.data) 1167 | self.edges = [] 1168 | for e in bm.edges: 1169 | if not e.hide: 1170 | self.edges.append((Vector(e.verts[0].co[:]),Vector(e.verts[1].co[:]))) 1171 | self.lengths = [(e[0]-e[1]).length for e in self.edges] 1172 | self.size = 0.1 1173 | self.execute(context) 1174 | return {"FINISHED"} 1175 | 1176 | 1177 | def execute(self,context): 1178 | try: 1179 | eob = bpy.data.objects['Edge_directions'] 1180 | self.remove(context,eob) 1181 | except: 1182 | pass 1183 | if not self.edges or not self.show: 1184 | self.bob.swiftBlock_direction_object = '' 1185 | return {"CANCELLED"} 1186 | bpy.ops.object.mode_set(mode='OBJECT') 1187 | bpy.ops.mesh.primitive_cone_add(vertices=self.verts,radius1=0.3,depth=1)#,end_fill_type='NOTHING') 1188 | default_arrow = context.active_object 1189 | arrows = [] 1190 | # this is "a bit" slow 1191 | for e,l in zip(self.edges,self.lengths): 1192 | v1 = Vector(e[0]) 1193 | v2 = Vector(e[1]) 1194 | tob = bpy.data.objects.new("Arrow_duplicate", default_arrow.data) 1195 | tob.location = v1+0.5*(v2-v1) 1196 | if self.relativeSize: 1197 | scale = self.size*l 1198 | else: 1199 | scale = self.size 1200 | tob.scale = (scale,scale,scale) 1201 | tob.rotation_mode = 'QUATERNION' 1202 | tob.rotation_quaternion = (v1-v2).to_track_quat('Z','Y') 1203 | bpy.context.collection.objects.link(tob) 1204 | arrows.append(tob) 1205 | tob.select_set(True) 1206 | aob = arrows[0] 1207 | bpy.context.view_layer.objects.active = aob 1208 | aob.name = 'Edge_directions' 1209 | aob.hide_select = True 1210 | 1211 | mat = bpy.data.materials.new('black') 1212 | #mat.emit = 2 1213 | mat.diffuse_color = (0,0,0,1) 1214 | bpy.ops.object.material_slot_add() 1215 | aob.material_slots[-1].material = mat 1216 | self.remove(context, default_arrow) 1217 | aob.swiftBlock_isdirectionObject = True 1218 | 1219 | bpy.ops.object.join() 1220 | bpy.ops.object.shade_smooth() 1221 | blender_utils.activateObject(self.bob) 1222 | self.bob.swiftBlock_direction_object = aob.name 1223 | return {"FINISHED"} 1224 | 1225 | def remove(self, context, ob): 1226 | bpy.context.collection.objects.unlink(ob) 1227 | bpy.data.objects.remove(ob) 1228 | 1229 | # Remove lingering Arrow_duplicate objects 1230 | if not self.show: 1231 | for ob in bpy.data.objects: 1232 | if ob.name.startswith('Arrow_duplicate'): 1233 | bpy.data.objects.remove(ob) 1234 | 1235 | 1236 | class SWIFTBLOCK_OT_EdgeVisualiser(bpy.types.Operator): 1237 | bl_idname = "swift_block.edge_visualiser" 1238 | bl_label = "Show Edge Directions" 1239 | bl_description = "Show Edge Directions" 1240 | 1241 | def modal(self, context, event): 1242 | context.area.tag_redraw() 1243 | if event.type == 'ESC': 1244 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') 1245 | return {"CANCELLED"} 1246 | return {"PASS_THROUGH"} 1247 | 1248 | def invoke(self, context, event): 1249 | args = (self, context) 1250 | if context.area.type == "VIEW_3D": 1251 | self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_edge_direction, args, 'WINDOW', 'POST_VIEW') 1252 | context.window_manager.modal_handler_add(self) 1253 | return {"RUNNING_MODAL"} 1254 | else: 1255 | self.report({"WARNING"}, "View3D not found, can't run operator") 1256 | return {"CANCELLED"} 1257 | 1258 | # ----------------- 1259 | 1260 | classes = ( 1261 | SWIFTBLOCK_PG_BlockProperty, 1262 | SWIFTBLOCK_PG_ProjectionProperty, 1263 | SWIFTBLOCK_PG_EdgeGroupProperty, 1264 | 1265 | VIEW3D_PT_SwiftBlockPanel, 1266 | 1267 | SWIFTBLOCK_UL_block_items, 1268 | SWIFTBLOCK_UL_boundary_items, 1269 | SWIFTBLOCK_UL_projection_items, 1270 | SWIFTBLOCK_UL_edgegroup_items, 1271 | 1272 | SWIFTBLOCK_OT_BoundariesAction, 1273 | SWIFTBLOCK_OT_EdgeGroupsAction, 1274 | SWIFTBLOCK_OT_InitBlocking, 1275 | SWIFTBLOCK_OT_BuildBlocking, 1276 | SWIFTBLOCK_OT_PreviewMesh, 1277 | SWIFTBLOCK_OT_WriteMesh, 1278 | SWIFTBLOCK_OT_ActivateBlocking, 1279 | SWIFTBLOCK_OT_GetBlock, 1280 | SWIFTBLOCK_OT_EditBlock, 1281 | SWIFTBLOCK_OT_EnableBlock, 1282 | SWIFTBLOCK_OT_SetEdge, 1283 | SWIFTBLOCK_OT_GetEdge, 1284 | SWIFTBLOCK_OT_SetCellSize, 1285 | SWIFTBLOCK_OT_EdgeSelectParallel, 1286 | SWIFTBLOCK_OT_FlipEdges, 1287 | SWIFTBLOCK_OT_GetProjection, 1288 | SWIFTBLOCK_OT_AddProjections, 1289 | SWIFTBLOCK_OT_RemoveProjection, 1290 | SWIFTBLOCK_OT_RemoveProjections, 1291 | SWIFTBLOCK_OT_ActivateSnap, 1292 | SWIFTBLOCK_OT_EdgetoPolyLine, 1293 | SWIFTBLOCK_OT_ExtrudeBlocks, 1294 | SWIFTBLOCK_OT_DrawEdgeDirections, 1295 | SWIFTBLOCK_OT_EdgeVisualiser, 1296 | ) 1297 | 1298 | def blockExtrusion_menu(self, context): 1299 | self.layout.operator(SWIFTBLOCK_OT_ExtrudeBlocks.bl_idname) 1300 | 1301 | def register(): 1302 | for cls in classes: 1303 | try: 1304 | bpy.utils.register_class(cls) 1305 | except: 1306 | print("Warning: %s registration failed, continuing.." % cls) 1307 | bpy.types.VIEW3D_MT_edit_mesh_extrude.prepend(blockExtrusion_menu) 1308 | 1309 | def unregister(): 1310 | for cls in classes: 1311 | bpy.utils.unregister_class(cls) 1312 | 1313 | bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(blockExtrusion_menu) 1314 | 1315 | if __name__ == "__main__": 1316 | register() 1317 | -------------------------------------------------------------------------------- /blender_utils.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import bpy 20 | from mathutils import Vector, Matrix, Euler 21 | import bgl 22 | import bmesh 23 | 24 | 25 | 26 | def vertices_from_mesh(ob): 27 | ''' 28 | ''' 29 | 30 | # get the modifiers 31 | if ob.modifiers: 32 | depsgraph = bpy.context.evaluated_depsgraph_get() 33 | obj_eval = ob.evaluated_get(depsgraph) 34 | mesh = obj_eval.to_mesh() 35 | else: 36 | mesh = ob.data 37 | 38 | matrix = ob.matrix_world.copy() 39 | 40 | for v in mesh.vertices: 41 | yield (matrix @ v.co) 42 | 43 | def edges_from_mesh(ob): 44 | ''' 45 | ''' 46 | 47 | # get the modifiers 48 | if ob.modifiers: 49 | depsgraph = bpy.context.evaluated_depsgraph_get() 50 | obj_eval = ob.evaluated_get(depsgraph) 51 | mesh = obj_eval.to_mesh() 52 | else: 53 | mesh = ob.data 54 | 55 | for e in mesh.edges: 56 | yield list(e.vertices) 57 | 58 | def activateObject(ob, hideCurrent = False): 59 | bpy.ops.object.mode_set(mode='OBJECT') 60 | cob = bpy.context.active_object 61 | if cob: 62 | cob.hide_set(hideCurrent) 63 | cob.select_set(False) 64 | ob.select_set(True) 65 | ob.hide_set(False) 66 | bpy.context.view_layer.objects.active = ob 67 | bpy.ops.object.mode_set(mode='EDIT') 68 | 69 | 70 | def previewMesh(ob, points, faces): 71 | blocking = ob 72 | blocking.hide_set(True) 73 | blocking.select_set(False) 74 | if not ob.swiftBlock_preview_object or \ 75 | not ob.swiftBlock_preview_object in bpy.data.objects: 76 | mesh_data = bpy.data.meshes.new("previewMesh") 77 | previewMeshOb = bpy.data.objects.new('previewMesh', mesh_data) 78 | previewMeshOb.swiftBlock_ispreviewObject = True 79 | bpy.context.collection.objects.link(previewMeshOb) 80 | ob.swiftBlock_preview_object = previewMeshOb.name 81 | else: 82 | previewMeshOb = bpy.data.objects["previewMesh"] 83 | oldme = previewMeshOb.data 84 | mesh_data = bpy.data.meshes.new("previewMesh") 85 | previewMeshOb.data = mesh_data 86 | bpy.data.meshes.remove(oldme) 87 | previewMeshOb.hide_set(False) 88 | previewMeshOb.select_set(True) 89 | previewMeshOb.swiftBlock_blocking_object = blocking.name 90 | mesh_data.from_pydata(points, [], faces) 91 | mesh_data.update() 92 | 93 | bpy.context.view_layer.objects.active = previewMeshOb 94 | # FIXME: show_extra_edge_length is now Overlay property, find out how to do this. 95 | #bpy.context.object.data.show_extra_edge_length = True 96 | #bpy.ops.object.mode_set(mode='EDIT') 97 | bpy.ops.object.mode_set(mode='EDIT') 98 | bpy.ops.mesh.select_all(action='DESELECT') 99 | bpy.ops.object.mode_set(mode='OBJECT') 100 | previewMeshOb.show_all_edges = True 101 | previewMeshOb.show_wire = True 102 | 103 | arrow_head = [ 104 | [-1,-1], 105 | [0,0], 106 | [1,-1], 107 | ] 108 | def draw_arrow_head(ob, vecFrom, vecTo): 109 | if ob is None: 110 | return 111 | 112 | direction = Vector(vecTo) - Vector(vecFrom) 113 | print(direction) 114 | # direction.resize_2d() 115 | # print(direction) 116 | angle = direction.angle(Vector((0,1,0))) 117 | 118 | # form a 2d rotation matrix 119 | mat = Matrix().Rotation(angle, 2) 120 | 121 | # middle point 122 | middle = (Vector(vecTo) + Vector(vecFrom)) / 2.0 123 | 124 | 125 | bgl.glEnable(bgl.GL_BLEND) 126 | bgl.glBegin(bgl.GL_LINE_STRIP) 127 | for v in arrow_head: 128 | xy = Vector(v)* 1.0 * mat 129 | xy.resize_3d() 130 | newPos = xy + middle 131 | bgl.glVertex3f(*newPos) 132 | bgl.glLineWidth(2) 133 | bgl.glColor4f(0.0,0.0,0.0,0.5) 134 | bgl.glEnd() 135 | 136 | # bgl.glEnable(bgl.GL_BLEND) 137 | # bgl.glBegin(bgl.GL_LINES) 138 | # bgl.glVertex3f(*(middle-Vector((1,0,0)))) 139 | # bgl.glVertex3f(*(middle-Vector((-1,0,0)))) 140 | # bgl.glVertex3f(*(middle-Vector((0,1,0)))) 141 | # bgl.glVertex3f(*(middle-Vector((0,-1,0)))) 142 | # print((middle-Vector((1,0,0))),(middle-Vector((-1,0,0)))) 143 | 144 | def draw_edge_direction(self,context): 145 | ob = bpy.context.active_object 146 | if ob is None: 147 | return 148 | if bpy.context.active_object.mode != 'EDIT': 149 | return 150 | me = ob.data 151 | bm = bmesh.from_edit_mesh(me) 152 | # draw_arrow_head(bm.edges[0]) 153 | bgl.glEnable(bgl.GL_BLEND) 154 | for e in bm.edges: 155 | draw_arrow_head(ob, e.verts[0].co, e.verts[1].co) 156 | bgl.glEnd() 157 | bgl.glLineWidth(1) 158 | bgl.glDisable(bgl.GL_BLEND) 159 | bgl.glColor4f(0.0,0.0,0.0,1.0) 160 | -------------------------------------------------------------------------------- /blockBuilder.py: -------------------------------------------------------------------------------- 1 | import mathutils 2 | import time 3 | import importlib 4 | import numpy as np 5 | # from . import cycleFinderNumba 6 | # importlib.reload(cycleFinderNumba) 7 | 8 | def removedup(seq): 9 | checked = [] 10 | for e in seq: 11 | if e not in checked: 12 | checked.append(e) 13 | return checked 14 | 15 | def edge(e0, e1): 16 | return [min(e0,e1), max(e0,e1)] 17 | 18 | def couple_edges(dependent_edges): 19 | for es0, edgeSet0 in enumerate(dependent_edges): 20 | for edge in edgeSet0: 21 | for es1, edgeSet1 in enumerate(dependent_edges): 22 | if edge in edgeSet1 and es0 != es1: 23 | for e in edgeSet0: 24 | edgeSet1.append(e) 25 | dependent_edges.pop(es0) 26 | return True 27 | return False 28 | 29 | 30 | 31 | def findFace(faces, vl): 32 | for fid, f in enumerate(faces): 33 | if vl[0] in f and vl[1] in f and vl[2] in f and vl[3] in f: 34 | return fid, f 35 | return -1, [] 36 | 37 | 38 | def cycleFinder(edges,verts): 39 | # Credit: Adam Gaither, An Efficient Block Detection Algorithm For 40 | # Structured Grid Generation. Proc. 5th Int. Conf. Num. Grid 41 | # Generation in Comp. Field Simulations, pp. 443-451 (1996). 42 | 43 | verticesId = verts 44 | edgeVisited = np.zeros(len(edges), dtype=bool) 45 | faces = [] 46 | facesEdges = [] 47 | no_edges = 0 48 | 49 | v_in_edge = [[] for i in range(len(verts))] 50 | for v in verts: 51 | for eid, e in enumerate(edges): 52 | if v in e: 53 | v_in_edge[v].append(eid) 54 | v_in_edges = np.array(v_in_edge, dtype=list) 55 | 56 | for v in verticesId: 57 | currentCycle = [v] 58 | currentCycleEdges = [] 59 | buildFourEdgeFaces(v, v_in_edge, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges) 60 | 61 | faces = np.reshape(faces,(-1,4)) 62 | temp, u = np.unique(np.sort(faces), axis=0, return_index=True) 63 | faces = faces[u] 64 | facesP = faces.tolist() 65 | 66 | facesEdges = np.reshape(facesEdges,(-1,4)) 67 | facesEdges = facesEdges[u] 68 | facesEdgesP = facesEdges.tolist() 69 | 70 | return facesP, facesEdgesP 71 | 72 | def buildFourEdgeFaces(v, v_in_edge, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges): 73 | for eid in v_in_edge[v]: 74 | if not edgeVisited[eid]: 75 | e = edges[eid] 76 | no_edges += 1 77 | edgeVisited[eid] = True 78 | opposite_v = e[0] 79 | if opposite_v == v: # seems the other vertex is in e[1]! 80 | opposite_v = e[1] 81 | currentCycle.append(opposite_v) 82 | currentCycleEdges.append(eid) 83 | if currentCycle[0] == currentCycle[-1]: # First equals last -> we have a face 84 | if len(currentCycle) == 5: 85 | faces.extend(currentCycle[0:4]) 86 | if len(currentCycleEdges) == 4: 87 | facesEdges.extend(currentCycleEdges[0:4]) 88 | else: 89 | if no_edges < 4: 90 | buildFourEdgeFaces(opposite_v, v_in_edge, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges) 91 | no_edges -= 1 92 | currentCycle.pop() 93 | currentCycleEdges.pop() 94 | edgeVisited[eid] = False 95 | 96 | 97 | def blockFinder(edges, vertices_coord, logFileName='', debugFileName='', disabled = [], numba=False): 98 | if len(logFileName) > 0: 99 | logFile = open(logFileName,'w') 100 | else: 101 | logFile = '' 102 | 103 | # Use the cycle finder class to find all edges forming quad faces 104 | if numba: 105 | from . import cycleFinderNumba 106 | tmp_v,tmp_e = cycleFinderNumba.cycleFinder(edges,range(len(vertices_coord))) 107 | else: 108 | tmp_v,tmp_e = cycleFinder(edges,range(len(vertices_coord))) 109 | 110 | faces_as_list_of_vertices = [] 111 | faces_as_list_of_nodes = [] 112 | faces_as_list_of_edges = [] 113 | for ii, i in enumerate(tmp_v): # get rid of possible triangles 114 | if len(i) == 4: 115 | faces_as_list_of_vertices.append([vertices_coord[i[0]], vertices_coord[i[1]], vertices_coord[i[2]], vertices_coord[i[3]]]) 116 | faces_as_list_of_nodes.append(i) 117 | faces_as_list_of_edges.append(tmp_e[ii]) 118 | # Create a wavefront obj file showing all the faces just found 119 | if len(debugFileName) > 0: 120 | debugFile = open(debugFileName,'w') 121 | for v in vertices_coord: 122 | debugFile.write('v {} {} {}\n'.format(*v)) 123 | for f in faces_as_list_of_nodes: 124 | debugFile.write('f ') 125 | for n in f: 126 | debugFile.write('{} '.format(n+1)) 127 | debugFile.write('\n') 128 | debugFile.close() 129 | 130 | # Store some info for the faces in a dict 131 | face_info = {} 132 | for fid, f in enumerate(faces_as_list_of_vertices): 133 | normal = mathutils.geometry.normal(f[0],f[1],f[2],f[3]) 134 | facecentre = mathutils.Vector((0,0,0)) 135 | for v in f: 136 | facecentre += 0.25*v 137 | face_info[fid] = {} 138 | face_info[fid]['normal'] = normal 139 | face_info[fid]['pos'] = [] 140 | face_info[fid]['neg'] = [] 141 | face_info[fid]['centre'] = facecentre 142 | 143 | connections_between_faces = [] 144 | # Find connections between faces, i.e. they share one edge 145 | for fid1, f1 in enumerate(faces_as_list_of_edges): 146 | for e in f1: 147 | for fid2, f2 in enumerate(faces_as_list_of_edges): 148 | if e in f2 and not fid1 == fid2: 149 | if not [min(fid1,fid2),max(fid1,fid2)] in connections_between_faces: 150 | connections_between_faces.append([min(fid1,fid2),max(fid1,fid2)]) 151 | 152 | #this is the most time consuming step 153 | # Use these connections to find cycles of connected faces; called faceLoops 154 | if numba: 155 | faceLoops_as_list_of_faces, faceLoops_as_list_of_connections = cycleFinderNumba.cycleFinder(connections_between_faces,range(len(faces_as_list_of_vertices))) 156 | else: 157 | faceLoops_as_list_of_faces, faceLoops_as_list_of_connections = cycleFinder(connections_between_faces,range(len(faces_as_list_of_vertices))) 158 | # faceLoops_as_list_of_faces, faceLoops_as_list_of_connections = blockBuilder2.cycleFinder(connections_between_faces,range(len(faces_as_list_of_vertices))) 159 | 160 | 161 | # Dig out block structures from these face loops 162 | block_as_faceLoop = [] 163 | for qf in faceLoops_as_list_of_faces: 164 | qf_is_a_block = True 165 | for n in faces_as_list_of_nodes[qf[0]]: 166 | if n in faces_as_list_of_nodes[qf[2]]: #if any of the vertices in face 0 is in face 2, this is not a block 167 | qf_is_a_block = False 168 | if qf_is_a_block: 169 | block_as_faceLoop.append(qf) 170 | # Get rid of block dublets - there are plenty 171 | faceLoops_nodes = [[] for i in range(len(block_as_faceLoop))] 172 | for qfid, qf in enumerate(block_as_faceLoop): 173 | for f in qf: 174 | for n in faces_as_list_of_nodes[f]: 175 | if not n in faceLoops_nodes[qfid]: 176 | faceLoops_nodes[qfid].append(n) 177 | for qf in faceLoops_nodes: 178 | qf.sort() 179 | tmp = [] 180 | potentialBlocks = [] # Each block is identified several times. Condense and put in potentialBlocks (list of vertices index) 181 | for qfid, qf in enumerate(faceLoops_nodes): 182 | if not qf in tmp: 183 | tmp.append(qf) 184 | if len(qf) == 8: 185 | potentialBlocks.append(block_as_faceLoop[qfid]) 186 | offences = [] 187 | block_centres = [] 188 | formalBlocks = [] 189 | dependent_edges = [] 190 | all_edges = [] 191 | if len(logFileName) > 0: 192 | logFile.write('number of potential blocks identified = ' + str(len(potentialBlocks)) + '\n') 193 | 194 | for b in potentialBlocks: 195 | is_a_real_block = True # more sanity checks soon... 196 | block = [] 197 | for n in faces_as_list_of_nodes[b[0]]: 198 | block.append(n) 199 | for n in faces_as_list_of_nodes[b[2]]: 200 | block.append(n) 201 | q2start = None 202 | for e in edges: # Locate the vertex just above block[0]. Store as q2start 203 | if block[0] == e[0]: 204 | if e[1] in block[4:8]: 205 | q2start = block.index(e[1]) 206 | if block[0] == e[1]: 207 | if e[0] in block[4:8]: 208 | q2start = block.index(e[0]) 209 | if q2start == None: # if not found above - this is not a complete block. 210 | q1nodes = block[0:4] 211 | q2nodes = block[4:-1] 212 | if len(logFileName) > 0: 213 | logFile.write('one block found was incomplete! ' + str(q1nodes) + str(q2nodes) + '\n') 214 | continue 215 | q2start = 0 #just set it to something. block wont be printed anyway 216 | quad1 = block[0:4] 217 | quad2 = [] 218 | for i in range(4): 219 | quad2.append(block[(i + q2start) % 4 + 4]) 220 | q1verts = [vertices_coord[quad1[0]],vertices_coord[quad1[1]],vertices_coord[quad1[2]],vertices_coord[quad1[3]]] 221 | q2verts = [vertices_coord[quad2[0]],vertices_coord[quad2[1]],vertices_coord[quad2[2]],vertices_coord[quad2[3]]] 222 | 223 | blockcentre = mathutils.Vector((0,0,0)) 224 | for n in block: 225 | blockcentre += 0.125*vertices_coord[n] 226 | q1fid, tmp = findFace(faces_as_list_of_nodes, quad1) 227 | q2fid, tmp = findFace(faces_as_list_of_nodes, quad2) 228 | 229 | normal1 = mathutils.geometry.normal(*q1verts) 230 | normal2 = mathutils.geometry.normal(*q2verts) 231 | 232 | facecentre1 = face_info[q1fid]['centre'] 233 | facecentre2 = face_info[q2fid]['centre'] 234 | direction1 = blockcentre-facecentre1 235 | direction2 = blockcentre-facecentre2 236 | 237 | v04 = q2verts[0] - q1verts[0] 238 | scalarProd1 = direction1.dot(normal1) 239 | scalarProd2 = direction2.dot(normal2) 240 | scalarProd3 = normal1.dot(v04) 241 | 242 | if scalarProd1*scalarProd2 > 0.: # make quad1 and quad2 rotate in the same direction 243 | quad2 = [quad2[0], quad2[-1], quad2[-2], quad2[-3]] 244 | normal2 *= -1.0 245 | 246 | if scalarProd3 < 0.: # Maintain righthanded system in each block 247 | tmp = list(quad2) 248 | quad2 = list(quad1) 249 | quad1 = tmp 250 | 251 | for nid,n in enumerate(quad1): #check that all edges are present 252 | if not (([n,quad2[nid]] in edges) or ([quad2[nid],n] in edges)): 253 | if len(logFileName) > 0: 254 | logFile.write('one block did not have all edges! ' + str(quad1) + str(quad2) + '\n') 255 | is_a_real_block = False 256 | break 257 | if not is_a_real_block: 258 | continue 259 | # more sanity... 260 | scale = v04.magnitude * normal1.magnitude 261 | if (abs(scalarProd3/scale) < 0.01): # abs(sin(alpha)) < 0.01, where alpha is angle for normal1 and v04 262 | if len(logFileName) > 0: 263 | logFile.write('flat block ruled out!' + str(quad1) + str(quad2) + '\n') 264 | continue 265 | 266 | if is_a_real_block: # this write-out only works if blenders own vertex numbering starts at zero!! seems to work... 267 | offences.append(0) 268 | block_centres.append(blockcentre) 269 | 270 | vl = quad1 + quad2 271 | formalBlocks.append(vl) # list of verts defining the block in correct order 272 | # formalBlocks are blocks that hava formal block structure and are not flat. Still in an O-mesh there are more formal 273 | # blocks present than what we want. More filtering... 274 | 275 | for bid, vl in enumerate(formalBlocks): 276 | fs = [] 277 | fs.append(vl[0:4]) 278 | fs.append(vl[4:8]) 279 | fs.append([vl[0], vl[1], vl[5], vl[4]]) 280 | fs.append([vl[1], vl[2], vl[6], vl[5]]) 281 | fs.append([vl[2], vl[3], vl[7], vl[6]]) 282 | fs.append([vl[3], vl[0], vl[4], vl[7]]) 283 | blockcentre = block_centres[bid] 284 | for f in fs: 285 | fid, tmp = findFace(faces_as_list_of_nodes, f) 286 | normal = face_info[fid]['normal'] 287 | facecentre = face_info[fid]['centre'] 288 | direction = normal.dot((blockcentre-facecentre)) 289 | if direction >= 0.: 290 | face_info[fid]['pos'].append(bid) 291 | else: 292 | face_info[fid]['neg'].append(bid) 293 | for f in face_info: # Not more than two blocks on each side of a face. If a block scores too high in 'offences' it will be ruled out 294 | if len(face_info[f]['pos'])>1: 295 | for bid in face_info[f]['pos']: 296 | offences[bid] += 1 297 | if len(face_info[f]['neg'])>1: 298 | for bid in face_info[f]['neg']: 299 | offences[bid] += 1 300 | block_print_out = [] 301 | for bid, vl in enumerate(formalBlocks): 302 | if offences[bid] <= 3 and not all( v in disabled for v in vl ): 303 | block_print_out.append(vl) 304 | i_edges = [edge(vl[0],vl[1]), edge(vl[2],vl[3]), edge(vl[4],vl[5]), edge(vl[6],vl[7])] 305 | j_edges = [edge(vl[1],vl[2]), edge(vl[3],vl[0]), edge(vl[5],vl[6]), edge(vl[7],vl[4])] 306 | k_edges = [edge(vl[0],vl[4]), edge(vl[1],vl[5]), edge(vl[2],vl[6]), edge(vl[3],vl[7])] 307 | # i_edges = [[vl[0],vl[1]], [vl[2],vl[3]], [vl[4],vl[5]], [vl[6],vl[7]]] 308 | # j_edges = [[vl[1],vl[2]], [vl[3],vl[0]], [vl[5],vl[6]], [vl[7],vl[4]]] 309 | # k_edges = [[vl[0],vl[4]], [vl[1],vl[5]], [vl[2],vl[6]], [vl[3],vl[7]]] 310 | dependent_edges.append(i_edges) #these 4 edges have the same resolution 311 | dependent_edges.append(j_edges) #these 4 edges have the same resolution 312 | dependent_edges.append(k_edges) #these 4 edges have the same resolution 313 | for e in range(4): 314 | if not i_edges[e] in all_edges: 315 | all_edges.append(i_edges[e]) 316 | if not j_edges[e] in all_edges: 317 | all_edges.append(j_edges[e]) 318 | if not k_edges[e] in all_edges: 319 | all_edges.append(k_edges[e]) 320 | else: # Dont let non-allowed blocks to stop definition of patch names 321 | for f in face_info: 322 | if bid in face_info[f]['pos']: 323 | ind = face_info[f]['pos'].index(bid) 324 | face_info[f]['pos'].pop(ind) 325 | if bid in face_info[f]['neg']: 326 | ind = face_info[f]['neg'].index(bid) 327 | face_info[f]['neg'].pop(ind) 328 | # stime = time.time() 329 | #this is the second most time consuming step 330 | still_coupling = True 331 | while still_coupling: 332 | still_coupling = couple_edges(dependent_edges) 333 | # blockBuilder2.couple_edges(dependent_edges) 334 | # print('still coupling, t',time.time() - stime) 335 | 336 | for es, edgeSet in enumerate(dependent_edges): # remove duplicates in lists 337 | dependent_edges[es] = removedup(edgeSet) 338 | return logFile, block_print_out, dependent_edges, face_info, all_edges, faces_as_list_of_nodes 339 | -------------------------------------------------------------------------------- /blockMeshBodyFit.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import os 3 | import numpy as np 4 | import subprocess 5 | import shutil 6 | import itertools 7 | import glob 8 | from . import utils 9 | class PreviewMesh(): 10 | def __init__(self, folder=None): 11 | if shutil.which('blockMeshBodyFit'): 12 | self.blockMeshbin = 'blockMeshBodyFit' 13 | elif shutil.which('blockMeshBoyFit'): 14 | self.blockMeshbin = 'blockMeshBoyFit' 15 | else: 16 | raise RuntimeError('ERROR: No BlockMeshBodyFit Found!') 17 | if folder: 18 | if os.path.isfile(folder) or not os.path.exists(folder): 19 | folder = os.path.dirname(folder) 20 | print("Exporting to directory " + str(folder)) 21 | if not os.path.isdir(folder): 22 | os.mkdir(folder) 23 | if not os.path.isdir(folder+'/constant'): 24 | os.mkdir(folder+'/constant') 25 | if not os.path.isdir(folder+'/constant/triSurface'): 26 | os.mkdir(folder+'/constant/triSurface') 27 | if not os.path.isdir(folder+'/system'): 28 | os.mkdir(folder+'/system') 29 | self.blockMeshDictPath = folder+ '/system/blockMeshDict' 30 | self.triSurfacePath = folder+'/constant/triSurface' 31 | else: 32 | self.tempdir = tempfile.mkdtemp() 33 | self.blockMeshDictPath = self.tempdir+"/constant/polyMesh/blockMeshDict" 34 | os.mkdir(self.tempdir+'/constant') 35 | os.mkdir(self.tempdir+'/constant/polyMesh') 36 | self.triSurfacePath = self.tempdir+'/constant/triSurface' 37 | os.mkdir(self.triSurfacePath) 38 | os.mkdir(self.tempdir+'/system') 39 | os.mkdir(self.tempdir+'/0') 40 | cd = open(self.tempdir+'/system/controlDict','w') 41 | cd.write(self.header()) 42 | print('OpenFOAM temp directory: {}'.format(self.tempdir)) 43 | 44 | def writeBlockMeshDict(self, verts, convertToMeters, boundaries, polyLines, edgeInfo, blockNames, blocks, dependent_edges, projections, searchLength): 45 | bmFile = open(self.blockMeshDictPath,'w') 46 | bmFile.write(self.header()) 47 | bmFile.write("\nconvertToMeters " + str(convertToMeters) + ";\n") 48 | bmFile.write("\nsearchLength {};\n\n\nvertices\n(\n".format(searchLength)) 49 | 50 | for v in verts: 51 | bmFile.write(' ({} {} {})\n'.format(*v)) 52 | bmFile.write(");\nblocks\n(\n") 53 | NoCells = 0 54 | 55 | edge = lambda e0,e1: [min(e0,e1), max(e0,e1)] 56 | 57 | for bid, (vl, blockName) in enumerate(zip(blocks, blockNames)): 58 | edges = [(vl[e[0]],vl[e[1]]) for e in [(0,1),(3,2),(7,6),(4,5),(0,3),(1,2),(5,6),(4,7),(0,4),(1,5),(2,6),(3,7)]] 59 | gradingStr = "" 60 | for ei in edges: 61 | e = edgeInfo[ei] 62 | gradingStr+= '{:.6g} '.format(e["ratio"]) 63 | ires = edgeInfo[edges[0]]["N"] 64 | jres = edgeInfo[edges[4]]["N"] 65 | kres = edgeInfo[edges[8]]["N"] 66 | 67 | NoCells += ires*jres*kres 68 | bmFile.write('// block id {} \nhex ({} {} {} {} {} {} {} {}) '.format(bid,*vl) \ 69 | + blockName + ' ({} {} {}) '.format(ires,jres,kres)\ 70 | + 'edgeGrading (' + gradingStr + ')\n' ) 71 | 72 | snapFaces = dict() 73 | for key,value in projections['face2surf'].items(): 74 | if value not in snapFaces: 75 | snapFaces[value] = [] 76 | snapFaces[value].append(key) 77 | bmFile.write(');\n\nsnapFaces\n{\n') 78 | for key, value in snapFaces.items(): 79 | bmFile.write(' %s.stl\n {\n faces\n (\n'%key) 80 | for v in value: 81 | bmFile.write(' ({} {} {} {})\n'.format(*v)) 82 | bmFile.write(' );\n }\n') 83 | 84 | bmFile.write('};\n\npatches\n(\n') 85 | for b in boundaries: 86 | bmFile.write(' {} {}\n (\n'.format(b['type'],b['name'] )) 87 | for v in b['faceVerts']: 88 | bmFile.write(' ({} {} {} {})\n'.format(*v)) 89 | bmFile.write(' )\n') 90 | bmFile.write(');\n\nedges\n(\n') 91 | for pl in polyLines: 92 | bmFile.write(pl) 93 | bmFile.write(');') 94 | bmFile.close() 95 | return NoCells 96 | 97 | 98 | def readHeader(self,dicfile): 99 | numberOfFields = 0 100 | startLine = False 101 | with open(dicfile) as fin: 102 | for lidx,line in enumerate(fin): 103 | if not numberOfFields: 104 | try: 105 | numberOfFields = int(line) 106 | except ValueError: 107 | pass 108 | if '(' in line: 109 | startLine = lidx + 1 110 | break 111 | return startLine, numberOfFields 112 | 113 | def readBoundaries(self,files): 114 | data = [] 115 | readingField = False 116 | for line in files: 117 | if not line.strip(): 118 | continue 119 | if not readingField and line.strip() == '{': 120 | readingField = True 121 | elif not readingField: 122 | temp = dict() 123 | temp['name']= line.strip() 124 | elif readingField and 'type' in line: 125 | temp['type'] = line.strip().split()[1][:-1] 126 | elif readingField and 'nFaces' in line: 127 | temp['nFaces'] = int(line.strip().split()[1][:-1]) 128 | elif readingField and 'startFace' in line: 129 | temp['startFace'] = int(line.strip().split()[1][:-1]) 130 | elif readingField and line.strip() == '}': 131 | data.append(temp) 132 | readingField = False 133 | elif not readingField and line.strip() == ')': 134 | break 135 | return data 136 | 137 | def getPoints(self,faces=None): 138 | pointsFile = self.tempdir +'/constant/polyMesh/points' 139 | startLine, numberofLines = self.readHeader(pointsFile) 140 | convertfnc1 = lambda x: float(x[1:]) 141 | convertfnc2 = lambda x:float(x[:-1]) 142 | with open(pointsFile,'rb') as fin: 143 | points = np.genfromtxt(itertools.islice(fin,startLine,startLine+numberofLines),\ 144 | converters={0:convertfnc1,2:convertfnc2},dtype=float) 145 | if faces!=None: 146 | pidxs = np.unique(np.ravel(faces)) 147 | points = points[pidxs] 148 | points=points.tolist() 149 | return points 150 | 151 | def getFaces(self): 152 | facesFile = self.tempdir +'/constant/polyMesh/faces' 153 | startLine, numberofLines = self.readHeader(facesFile) 154 | convertfnc1 = lambda x: int(x[2:]) 155 | convertfnc2 = lambda x: int(x[:-1]) 156 | with open(facesFile,'rb') as fin: 157 | faces = np.genfromtxt(itertools.islice(fin,startLine,startLine+numberofLines),\ 158 | converters={0:convertfnc1,3:convertfnc2},dtype=int) 159 | faces = faces.tolist() 160 | return faces 161 | 162 | def getBCFaces(self,internalCells): 163 | faces = self.getFaces() 164 | bcifaces = faces 165 | bcfaces = faces 166 | if not internalCells: 167 | boundaryFile = self.tempdir + '/constant/polyMesh/boundary' 168 | startLine, boundaries = self.readHeader(boundaryFile) 169 | with open(boundaryFile) as fin: 170 | fields = self.readBoundaries(itertools.islice(fin,startLine,None)) 171 | self.fields = fields 172 | bcifaces = [] 173 | bcfaces=[] 174 | for bc in sorted(fields, key=lambda k: k['startFace']): 175 | bcfaces.extend(faces[bc['startFace']:bc['startFace']+bc['nFaces']]) 176 | bcifaces = np.array(bcfaces,dtype=int) 177 | bcifaces = np.unique(bcifaces.ravel(),return_inverse=True)[1].reshape(bcifaces.shape) 178 | bcifaces = bcifaces.astype(int).tolist() 179 | return bcfaces,bcifaces 180 | 181 | #this is faster 182 | def getBCFaces2(self,internalCells): 183 | facesFile = self.tempdir +'/constant/polyMesh/faces' 184 | startLine, numberofLines = self.readHeader(facesFile) 185 | faces = open(facesFile).readlines() 186 | faces = faces[startLine:startLine+numberofLines] 187 | subs = lambda s: list(map(int,s.__getitem__(slice(2,-2)).split())) 188 | faces = list(map(subs, faces)) 189 | boundaryFile = self.tempdir + '/constant/polyMesh/boundary' 190 | startLine, boundaries = self.readHeader(boundaryFile) 191 | with open(boundaryFile) as fin: 192 | fields = self.readBoundaries(itertools.islice(fin,startLine,None)) 193 | self.fields = fields 194 | bcifaces = [] 195 | bcfaces=[] 196 | for bc in sorted(fields, key=lambda k: k['startFace']): 197 | bcfaces.extend(faces[bc['startFace']:bc['startFace']+bc['nFaces']]) 198 | bcifaces = np.array(bcfaces,dtype=int) 199 | bcifaces = np.unique(bcifaces.ravel(),return_inverse=True)[1].reshape(bcifaces.shape) 200 | bcifaces = bcifaces.astype(int).tolist() 201 | return bcfaces,bcifaces 202 | 203 | def runBlockMesh(self): 204 | subprocess.call([self.blockMeshbin,'-case',self.tempdir],stdout=subprocess.PIPE) 205 | 206 | def runMesh(self,runBlockMesh=True,internalCells=False): 207 | print('running blockmesh') 208 | if runBlockMesh: 209 | self.runBlockMesh() 210 | faces, bcifaces=self.getBCFaces2(internalCells) 211 | points=self.getPoints(faces) 212 | # shutil.rmtree(self.tempdir) 213 | return points, bcifaces 214 | 215 | def header(self): 216 | return \ 217 | ''' 218 | /*--------------------------------*- C++ -*----------------------------------*/ 219 | 220 | // File was generated by SwiftBlock, a Blender 3D addon. 221 | 222 | FoamFile 223 | { 224 | version 2.0; 225 | format ascii; 226 | class dictionary; 227 | object blockMeshDict; 228 | } 229 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 230 | 231 | 232 | deltaT 1; 233 | 234 | writeInterval 1; 235 | 236 | 237 | 238 | // ************************************************************************* // 239 | 240 | ''' 241 | -------------------------------------------------------------------------------- /blockMeshMG.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import os 3 | import numpy as np 4 | import subprocess 5 | import shutil 6 | import itertools 7 | import glob 8 | from . import utils 9 | class PreviewMesh(): 10 | def __init__(self, folder=None): 11 | #if not shutil.which('blockMesh'): 12 | # # raise RuntimeError('ERROR: Could not find blockMesh! Please source OpenFOAM in terminal and start Blender from that terminal so that BlockMeshMG finds blockMesh command.') 13 | #else: 14 | # self.blockMeshbin = 'blockMesh' 15 | if folder: 16 | if os.path.isfile(folder) or not os.path.exists(folder): 17 | folder = os.path.dirname(folder) 18 | print("Exporting to directory " + str(folder)) 19 | if not os.path.isdir(folder): 20 | os.mkdir(folder) 21 | if not os.path.isdir(os.path.join(folder, 'constant')): 22 | os.mkdir(os.path.join(folder, 'constant')) 23 | if not os.path.isdir(os.path.join(folder, 'constant', 'geometry')): 24 | os.mkdir(os.path.join(folder, 'constant', 'geometry')) 25 | if not os.path.isdir(os.path.join(folder, 'system')): 26 | os.mkdir(os.path.join(folder, 'system')) 27 | self.blockMeshDictPath = os.path.join(folder, 'system', 'blockMeshDict') 28 | self.geomPath = os.path.join(folder, 'constant', 'geometry') 29 | # blockMesh requires controlDict, create one if there is none 30 | if not os.path.isfile(os.path.join(folder, 'system', 'controlDict')): 31 | cd = open(os.path.join(folder, 'system', 'controlDict'), 'w') 32 | cd.write(self.header()) 33 | else: 34 | self.tempdir = tempfile.mkdtemp() 35 | self.blockMeshDictPath = os.path.join(self.tempdir, 'system', 'blockMeshDict') 36 | os.mkdir(os.path.join(self.tempdir, 'constant')) 37 | os.mkdir(os.path.join(self.tempdir, 'constant', 'polyMesh')) 38 | self.geomPath = os.path.join(self.tempdir, 'constant', 'geometry') 39 | os.mkdir(self.geomPath) 40 | os.mkdir(os.path.join(self.tempdir, 'system')) 41 | os.mkdir(os.path.join(self.tempdir, '0')) 42 | cd = open(os.path.join(self.tempdir, 'system', 'controlDict'), 'w') 43 | cd.write(self.header()) 44 | print('OpenFOAM temp directory: {}'.format(self.tempdir)) 45 | 46 | def writeBlockMeshDict(self, verts, convertToMeters, boundaries, polyLines, edgeInfo, blockNames, blocks, dependent_edges,\ 47 | projections): 48 | bmFile = open(self.blockMeshDictPath,'w') 49 | bmFile.write(self.header()) 50 | bmFile.write('\ngeometry\n{\n') 51 | for g in projections['geo']: 52 | bmFile.write(' {geo}\n {{\n type triSurfaceMesh;\n file "{geo}.stl";\n }}\n'.format(geo=g)) 53 | 54 | bmFile.write("}\nvertices\n(\n") 55 | for i,v in enumerate(verts): 56 | if i in projections['vert2surf']: 57 | bmFile.write(' project ({} {} {}) ({})\n'.format(*v,projections['vert2surf'][i])) 58 | else: 59 | bmFile.write(' ({} {} {})\n'.format(*v)) 60 | 61 | bmFile.write(");\nedges\n(\n") 62 | for key, value in projections['edge2surf'].items(): 63 | bmFile.write(' projectCurve {} {} ({})\n'.format(*key, value)) 64 | for pl in polyLines: 65 | bmFile.write(pl) 66 | 67 | bmFile.write(");\nfaces\n(\n") 68 | for key, value in projections['face2surf'].items(): 69 | # for v in value: 70 | bmFile.write(' project ({} {} {} {}) {}\n'.format(*key,value)) 71 | 72 | bmFile.write(");\nblocks\n(\n") 73 | 74 | 75 | NoCells = 0 76 | for bid, (vl, blockName) in enumerate(zip(blocks, blockNames)): 77 | edges = [(vl[e[0]],vl[e[1]]) for e in [(0,1),(3,2),(7,6),(4,5),(0,3),(1,2),(5,6),(4,7),(0,4),(1,5),(2,6),(3,7)]] 78 | gradingStr = "" 79 | for ei in edges: 80 | e = edgeInfo[ei] 81 | gradingStr+='\n(\n ({:.6g} {:.6g} {:.6g}) ({:.6g} {:.6g} {:.6g}) ({:.6g} {:.6g} {:.6g}) '.format( 82 | e["l1"],e["n1"],e["ratio1"],\ 83 | e["dL"],e["nL"],1,\ 84 | e["l2"],e["n2"],1/e["ratio2"]) + '\n)' 85 | ires = edgeInfo[edges[0]]["N"] 86 | jres = edgeInfo[edges[4]]["N"] 87 | kres = edgeInfo[edges[8]]["N"] 88 | 89 | NoCells += ires*jres*kres 90 | bmFile.write('// block id {} \nhex ({} {} {} {} {} {} {} {}) '.format(bid,*vl) \ 91 | + blockName + ' ({} {} {}) '.format(ires,jres,kres)\ 92 | + 'edgeGrading (' + gradingStr + '\n)\n' ) 93 | bmFile.write(');\n\npatches\n(\n') 94 | for b in boundaries: 95 | bmFile.write(' {} {}\n (\n'.format(b['type'],b['name'] )) 96 | for v in b['faceVerts']: 97 | bmFile.write(' ({} {} {} {})\n'.format(*v)) 98 | bmFile.write(' )\n') 99 | bmFile.write(');') 100 | bmFile.close() 101 | return NoCells 102 | 103 | 104 | def readHeader(self,dicfile): 105 | numberOfFields = 0 106 | startLine = False 107 | with open(dicfile) as fin: 108 | for lidx,line in enumerate(fin): 109 | if not numberOfFields: 110 | try: 111 | numberOfFields = int(line) 112 | except ValueError: 113 | pass 114 | if '(' in line: 115 | startLine = lidx + 1 116 | break 117 | return startLine, numberOfFields 118 | 119 | def readBoundaries(self,files): 120 | data = [] 121 | readingField = False 122 | for line in files: 123 | if not line.strip(): 124 | continue 125 | if not readingField and line.strip() == '{': 126 | readingField = True 127 | elif not readingField: 128 | temp = dict() 129 | temp['name']= line.strip() 130 | elif readingField and 'type' in line: 131 | temp['type'] = line.strip().split()[1][:-1] 132 | elif readingField and 'nFaces' in line: 133 | temp['nFaces'] = int(line.strip().split()[1][:-1]) 134 | elif readingField and 'startFace' in line: 135 | temp['startFace'] = int(line.strip().split()[1][:-1]) 136 | elif readingField and line.strip() == '}': 137 | data.append(temp) 138 | readingField = False 139 | elif not readingField and line.strip() == ')': 140 | break 141 | return data 142 | 143 | def getPoints(self,faces=None): 144 | pointsFile = os.path.join(self.tempdir, 'constant', 'polyMesh', 'points') 145 | startLine, numberofLines = self.readHeader(pointsFile) 146 | convertfnc1 = lambda x: float(x[1:]) 147 | convertfnc2 = lambda x:float(x[:-1]) 148 | with open(pointsFile,'rb') as fin: 149 | points = np.genfromtxt(itertools.islice(fin,startLine,startLine+numberofLines),\ 150 | converters={0:convertfnc1,2:convertfnc2},dtype=float) 151 | if faces!=None: 152 | pidxs = np.unique(np.ravel(faces)) 153 | points = points[pidxs] 154 | points=points.tolist() 155 | return points 156 | 157 | def getFaces(self): 158 | facesFile = os.path.join(self.tempdir, 'constant', 'polyMesh', 'faces') 159 | startLine, numberofLines = self.readHeader(facesFile) 160 | convertfnc1 = lambda x: int(x[2:]) 161 | convertfnc2 = lambda x: int(x[:-1]) 162 | with open(facesFile,'rb') as fin: 163 | faces = np.genfromtxt(itertools.islice(fin,startLine,startLine+numberofLines),\ 164 | converters={0:convertfnc1,3:convertfnc2},dtype=int) 165 | faces = faces.tolist() 166 | return faces 167 | 168 | def getBCFaces(self,internalCells): 169 | faces = self.getFaces() 170 | bcifaces = faces 171 | bcfaces = faces 172 | if not internalCells: 173 | boundaryFile = os.path.join(self.tempdir, 'constant', 'polyMesh', 'boundary') 174 | startLine, boundaries = self.readHeader(boundaryFile) 175 | with open(boundaryFile) as fin: 176 | fields = self.readBoundaries(itertools.islice(fin,startLine,None)) 177 | self.fields = fields 178 | bcifaces = [] 179 | bcfaces=[] 180 | for bc in sorted(fields, key=lambda k: k['startFace']): 181 | bcfaces.extend(faces[bc['startFace']:bc['startFace']+bc['nFaces']]) 182 | bcifaces = np.array(bcfaces,dtype=int) 183 | bcifaces = np.unique(bcifaces.ravel(),return_inverse=True)[1].reshape(bcifaces.shape) 184 | bcifaces = bcifaces.astype(int).tolist() 185 | return bcfaces,bcifaces 186 | 187 | #this is faster 188 | def getBCFaces2(self,internalCells): 189 | facesFile = os.path.join(self.tempdir, 'constant', 'polyMesh', 'faces') 190 | startLine, numberofLines = self.readHeader(facesFile) 191 | faces = open(facesFile).readlines() 192 | faces = faces[startLine:startLine+numberofLines] 193 | subs = lambda s: list(map(int,s.__getitem__(slice(2,-2)).split())) 194 | faces = list(map(subs, faces)) 195 | boundaryFile = os.path.join(self.tempdir, 'constant', 'polyMesh', 'boundary') 196 | startLine, boundaries = self.readHeader(boundaryFile) 197 | with open(boundaryFile) as fin: 198 | fields = self.readBoundaries(itertools.islice(fin,startLine,None)) 199 | self.fields = fields 200 | bcifaces = [] 201 | bcfaces=[] 202 | for bc in sorted(fields, key=lambda k: k['startFace']): 203 | bcfaces.extend(faces[bc['startFace']:bc['startFace']+bc['nFaces']]) 204 | bcifaces = np.array(bcfaces,dtype=int) 205 | bcifaces = np.unique(bcifaces.ravel(),return_inverse=True)[1].reshape(bcifaces.shape) 206 | bcifaces = bcifaces.astype(int).tolist() 207 | return bcfaces,bcifaces 208 | 209 | def runBlockMesh(self): 210 | subprocess.call([self.blockMeshbin,'-case',self.tempdir],stdout=subprocess.PIPE) 211 | 212 | def runMesh(self,runBlockMesh=True,internalCells=False): 213 | if not shutil.which('blockMesh'): 214 | return [], [] 215 | if runBlockMesh: 216 | self.blockMeshbin = 'blockMesh' 217 | print('running blockMesh') 218 | self.runBlockMesh() 219 | faces, bcifaces=self.getBCFaces2(internalCells) 220 | points=self.getPoints(faces) 221 | shutil.rmtree(self.tempdir) 222 | return points, bcifaces 223 | 224 | def header(self): 225 | return \ 226 | ''' 227 | /*--------------------------------*- C++ -*----------------------------------*/ 228 | 229 | // File was generated by SwiftBlock, a Blender 3D addon. 230 | 231 | FoamFile 232 | { 233 | version 2.0; 234 | format ascii; 235 | class dictionary; 236 | object blockMeshDict; 237 | } 238 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 239 | 240 | deltaT 1; 241 | 242 | writeInterval 1; 243 | 244 | 245 | 246 | // ************************************************************************* // 247 | 248 | ''' 249 | -------------------------------------------------------------------------------- /cycleFinderNumba.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | import numpy as np 3 | 4 | # @jit(nopython=True) 5 | def still_coupling(dependent_edges): 6 | for es0 in range(len(dependent_edges)): 7 | edgeSet0 = dependent_edges[es0] 8 | for edge in edgeSet0: 9 | for es1 in range(len(dependent_edges)): 10 | edgeSet1 = dependent_edges[es1] 11 | if edge in edgeSet1 and es0 != es1: 12 | for e in edgeSet0: 13 | edgeSet1.append(e) 14 | dependent_edges.pop(es0) 15 | return True 16 | return False 17 | 18 | def couple_edges(dependent_edges): 19 | while still_coupling(dependent_edges): 20 | pass 21 | 22 | def cycleFinder(edges,verts): 23 | verticesId = np.array(verts) 24 | edges = np.array(edges) 25 | edgeVisited = np.zeros(len(edges), dtype=bool) 26 | faces = [-1] # For Numba the arrays have to contain element 27 | facesEdges = [-1] 28 | no_edges = 0 29 | 30 | # 50 is the limit of vertices in one edge. Should be enough, maybe too many? 31 | v_in_edge = np.ones((len(verticesId),50),dtype=int) * -1 32 | for v in verticesId: 33 | for eid, e in enumerate(edges): 34 | if v in e: 35 | v_in_edge[v][np.where(v_in_edge[v] == -1)[0][0]] = eid 36 | 37 | run(verticesId,edges,edgeVisited,faces,facesEdges,no_edges, v_in_edge) 38 | 39 | # Clean double faces 40 | faces = np.reshape(faces[1:],(-1,4)) 41 | temp, u = np.unique(np.sort(faces), axis=0, return_index=True) 42 | faces = faces[u] 43 | facesP = [list(map(np.asscalar,f)) for f in faces] 44 | 45 | facesEdges = np.reshape(facesEdges[1:],(-1,4)) 46 | facesEdges = facesEdges[u] 47 | facesEdgesP = [list(map(np.asscalar,f)) for f in facesEdges] 48 | 49 | return facesP, facesEdgesP 50 | 51 | @jit(nopython=True) 52 | def run(verticesId,edges,edgeVisited,faces,facesEdges,no_edges, v_in_edge): 53 | for v in verticesId: 54 | currentCycle = [v] 55 | currentCycleEdges = [-1] 56 | buildFourEdgeFaces(v, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges, v_in_edge, first=True) 57 | 58 | @jit(nopython=True) 59 | def buildFourEdgeFaces(v, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges, v_in_edge, first=False): 60 | for eid in v_in_edge[v]: 61 | if eid == -1: 62 | break 63 | e = edges[eid] 64 | if v == e[0]: 65 | opposite_v = e[1] 66 | elif v == e[1]: 67 | opposite_v = e[0] 68 | else: 69 | continue 70 | if not edgeVisited[eid]: 71 | no_edges += 1 72 | edgeVisited[eid] = True 73 | currentCycle.append(opposite_v) 74 | if first: 75 | currentCycleEdges[0] = eid 76 | first = False 77 | else: 78 | currentCycleEdges.append(eid) 79 | if currentCycle[0] == currentCycle[-1]: # First equals last -> we have a face 80 | # we are only interested in quads 81 | if len(currentCycle) == 5: 82 | faces.extend(currentCycle[0:4]) 83 | facesEdges.extend(currentCycleEdges[0:4]) 84 | else: 85 | if no_edges < 4: 86 | buildFourEdgeFaces(opposite_v, edgeVisited, edges, no_edges, currentCycle, currentCycleEdges, faces, facesEdges, v_in_edge) 87 | no_edges -= 1 88 | currentCycle.pop() 89 | currentCycleEdges.pop() 90 | edgeVisited[eid] = False 91 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = swiftBlock 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SwiftBlock documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 22 15:28:51 2019. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'SwiftBlock' 49 | copyright = u'2023, Karl-Johan Nogenmyr, Mikko Folkersma, Turo Valikangas, Tuomo Keskitalo' 50 | author = u'Karl-Johan Nogenmyr, Mikko Folkersma, Turo Valikangas, Tuomo Keskitalo' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = u'0.5' 58 | # The full version, including alpha/beta/rc tags. 59 | release = u'0.5' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | # If true, `todo` and `todoList` produce output, else they produce nothing. 77 | todo_include_todos = False 78 | 79 | 80 | # -- Options for HTML output ---------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | #html_theme = 'alabaster' 86 | html_theme = 'sphinx_rtd_theme' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | # Custom sidebar templates, must be a dictionary that maps document names 100 | # to template names. 101 | # 102 | # This is required for the alabaster theme 103 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 104 | html_sidebars = { 105 | '**': [ 106 | 'relations.html', # needs 'show_related': True theme option to display 107 | 'searchbox.html', 108 | ] 109 | } 110 | 111 | 112 | # -- Options for HTMLHelp output ------------------------------------------ 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'SwiftBlockdoc' 116 | 117 | 118 | # -- Options for LaTeX output --------------------------------------------- 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'swiftBlock.tex', u'SwiftBlock Documentation', 143 | u'Karl-Johan Nogenmyr, Mikko Folkersma, Turo Valikangas, Tuomo Keskitalo', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output --------------------------------------- 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'swiftblock', u'SwiftBlock Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'swiftBlock', u'SwiftBlock Documentation', 164 | author, 'swiftBlock', 'One line description of project.', 165 | 'Miscellaneous'), 166 | ] 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /docs/images/block_method_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/block_method_settings.png -------------------------------------------------------------------------------- /docs/images/boundary_patches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/boundary_patches.png -------------------------------------------------------------------------------- /docs/images/complex_block_with_elongations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/complex_block_with_elongations.png -------------------------------------------------------------------------------- /docs/images/edge_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/edge_settings.png -------------------------------------------------------------------------------- /docs/images/flow_around_sphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/flow_around_sphere.png -------------------------------------------------------------------------------- /docs/images/naca_airfoil_mesh_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/naca_airfoil_mesh_preview.png -------------------------------------------------------------------------------- /docs/images/projections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/projections.png -------------------------------------------------------------------------------- /docs/images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/docs/images/ui.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. swiftBlock documentation master file, created by 2 | sphinx-quickstart on Mon Apr 22 15:28:51 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | swiftBlock Addon for Blender 7 | ============================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | swift.rst 14 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.1.2 2 | sphinx-rtd-theme==1.3.0rc1 3 | 4 | -------------------------------------------------------------------------------- /docs/swift.rst: -------------------------------------------------------------------------------- 1 | SwiftBlock Addon for Blender 2 | ============================ 3 | 4 | .. image:: images/naca_airfoil_mesh_preview.png 5 | 6 | Introduction 7 | ------------ 8 | 9 | SwiftBlock is a `Blender `_ GUI add-on for 10 | the OpenFOAM® *blockMesh* utility, which creates hexahedral block 11 | structured volume meshes for OpenFOAM simulations. 12 | The target of SwiftBlock is to ease the creation of structured 13 | block meshes for controlled grading (e.g. boundary layers) or streched 14 | cells composed of hexahedral cell blocks. 15 | 16 | Block structure is first modelled as a mesh object in Blender. A graph 17 | theory based method implemented in the addon identifies the discrete 18 | hexahedral blocks in the mesh object and generates blockMeshDict. Main 19 | features include 20 | 21 | * user specified divisions and optional grading of block edges 22 | * specification of patches (boundary surfaces) 23 | * specification of blocks to create cell zones/sets 24 | * easy block manipulations including selection, visualisation and disabling of blocks 25 | * visualization of edge directions 26 | * projection of block edges to surfaces on mesh objects to 27 | create e.g. curved shapes 28 | 29 | 30 | Versions 31 | -------- 32 | 33 | This documentation describes the version of the add-on available under 34 | `github/tkeskita `_. 35 | The add-on is aimed to work with latest 36 | `Blender LTS version `_ and 37 | latest `OpenFOAM Foundation `_ and 38 | latest `OpenFOAM.com `_ versions. 39 | Tested with Blender 4.2 and OpenFOAM.org v12 40 | 41 | Please note that 42 | `Preview command has an issue on Windows OS `_. 43 | 44 | Previous versions of the add-on are available in 45 | `github/nogenmyr `_ and 46 | `github/Flowkersma `_, 47 | and the documentation for original version is available at 48 | `OpenFOAM wiki `_. 49 | 50 | 51 | Installation and Start-up 52 | ------------------------- 53 | 54 | * Add-on code is available at https://github.com/tkeskita/swiftBlock. 55 | To download add-on from Github, Select "Code", then 56 | "Download ZIP". 57 | * Start a terminal (command line window). 58 | * Source OpenFOAM in the terminal window if needed (see `OpenFOAM environment variables `_). 59 | If OpenFOAM commands (e.g. `blockMesh`) are not available in the terminal, 60 | you can't run the *Preview* command in the add-on. 61 | * Start Blender from the same terminal window. 62 | * Add-on installation: 63 | 64 | * In Blender, go to 65 | "Edit" --> "Preferences" --> "Add-ons" --> "Install" --> open the add-on zip file. 66 | * Activate the "SwiftBlock" add-on in Preferences. 67 | Add-on is located in OpenFOAM category, Testing level of Blender add-ons. 68 | 69 | 70 | Add-on visibility 71 | ----------------- 72 | 73 | Add-on is visible in Blender's 3D Viewport in Sidebar as a separate 74 | tab in Edit Mode. To view the add-on panels, you must 75 | 76 | * Select a mesh object (in 3D Viewport or in Outliner) 77 | * View Sidebar ("View" --> "Toggle Sidebar" or press "N" key in 3D Viewport) 78 | * Select "SwiftBlock" tab in the Sidebar 79 | 80 | Quickstart 81 | ---------- 82 | 83 | * Install and start Blender as specified above 84 | * In Blender, select the default Cube object in 3D Viewport or in Outliner 85 | * Make add-on panels visible as described above 86 | * Click *Initialize Object* 87 | * Model block geometry in Blender 88 | * Set Edge Parameters for each edge 89 | * Optionally add projections and/or Boundary Patches 90 | * Optionally preview blocks 91 | * Press *Export* to save blockMeshDict to a case folder 92 | * Run blockMesh OpenFOAM command in terminal to generate mesh 93 | 94 | Panels and Settings 95 | ------------------- 96 | 97 | SwiftBlock GUI consists of four panels: Block Method Settings, Edge 98 | Settings, Projections and Boundary Patches. 99 | 100 | .. image:: images/ui.png 101 | 102 | Block Method Settings 103 | ^^^^^^^^^^^^^^^^^^^^^ 104 | 105 | This panel contains overall settings and tool buttons. 106 | You can hover mouse cursor over fields to see tool tips for more 107 | information. 108 | 109 | .. image:: images/block_method_settings.png 110 | 111 | * *Initialize Object* tool appears when a mesh object is selected 112 | Running the tool will intialize the selected object for SwiftBlock, 113 | enables Edit Mode, and reveals the rest of the GUI. 114 | **Note**: SwiftBlock tools are available only in Edit Mode. 115 | * *Method* defines the block generation method. Default value is 116 | *BlockMeshMG*, which supports multi grading. 117 | **Note:** Currently no other methods are available. The source 118 | code also includes the original *blockMeshBodyFit* method, but is has 119 | not been upgraded/tested with Blender 2.8, so it is currently disabled. 120 | * *Build* tool identifies the blocks from the current mesh. 121 | Blocks are listed in the panel window after building is completed. 122 | **Warning**: Bulding may take a long time for complex block systems. 123 | * *Use Numba* option box enables Python Numba performance library. 124 | Numba compiles the Build tool into machine code, which decreases the 125 | time required for *Build*. 126 | **Note**: Numba requires installation of the Numba Python libraries 127 | into Blender. You can use similar installation procedure as 128 | `installation of VTK into Blender `_. 129 | **Warning**: Not tested. 130 | * *Preview* tool shows preview of the edges on the result block mesh. 131 | Preview requires that the OpenFOAM blockMesh utility is available in 132 | Blender. An error message is displayed if blockMesh command is not 133 | found. To make blockMesh available, you must start terminal command 134 | prompt, source OpenFOAM in the terminal, and start blender from the 135 | same terminal. Preview will automatically run *Build* tool if needed. 136 | * *Export* tool saves blockMeshDict file into a case folder. The user 137 | is prompted to select the case folder. Any file name is ignored, only 138 | the directory matters. 139 | * *Block list* contains the list of blocks identified by the *Build* tool. 140 | 141 | * Clicking a block selects and highlights the block in the 3D 142 | Viewport. Enable e.g. "Show whole scene transparent" option in 143 | the 3D Viewport header to see blocks inside. 144 | * Check box can be deselected to disable a block 145 | 146 | * *Get Block from Selection* selects the block attached to the current 147 | selection. 148 | * *Extrude Blocks (Retain Internal Edges)* is a special extrusion tool 149 | for Swift Block, which keeps the internal edges in extrusion. 150 | 151 | Edge Settings 152 | ^^^^^^^^^^^^^ 153 | 154 | This panel is used to set parameter values and apply them on selected edges. 155 | 156 | .. image:: images/edge_settings.png 157 | 158 | * *Cells* specifies the number of divisions. 159 | * *Start* and *End* refer to optional edge grading at the start and 160 | end of edges. 161 | 162 | * *x1* is the first cell length at the start of edge. 163 | * *x2* is the last cell length at the end of edge. 164 | * *r1* is the geometric boundary layer ratio of neighbor cell 165 | lengths at the start of edge. Values larger than 1.0 create grading. 166 | * *r2* is the geometric boundary layer ratio of neighbor cell 167 | lengths at the end of edge. 168 | 169 | * *Set Params* tool applies the above parameter values to currently 170 | selected edges. 171 | * *Get Params* tool gets the parameter values from active edge. 172 | * *Select Group* tool adds edges in same edge group to selection. This 173 | is a convenience tool to select aligned or connected edges, to ease 174 | specification of consistent parameter values. 175 | * *Flip Dir* tool flips the edge direction of selected edges. 176 | * *Show Edge Directions* tool visualizes the edge directions by adding 177 | cones to edge centers. 178 | 179 | 180 | Edge Groups 181 | ^^^^^^^^^^^ 182 | 183 | Edge Groups is a new panel which allows creation of Edge Groups, 184 | similar to Vertex Groups in Blender. 185 | 186 | * *Add (+)* will create a new edge group 187 | * *Delete (-)* will remove the active edge group 188 | * *Assign* will add the selected edges to active edge group 189 | * *Remove* will remove the selected edges from the active edge group 190 | * *Select* adds the edges in the active edge group into current edge selection 191 | * *Deselect* removes currently selected edges from the active edge group 192 | 193 | 194 | Projections 195 | ^^^^^^^^^^^ 196 | 197 | This panel contains settings for projecting edges to surfaces on other 198 | mesh objects. 199 | 200 | .. image:: images/projections.png 201 | 202 | * *Icon* drop down menu specifies the projection object. 203 | **Note**: Projection object must be visible (not hidden) for 204 | projection to work correctly. 205 | **Note**: Object name must start with a letter and not a number, 206 | so that OpenFOAM interprets the result correctly. If object name 207 | starts with a number, you will get error like 208 | ``Expected a '(' or a '{' while reading List...`` 209 | * *Add* button will add the specified object as projection object and 210 | populates list of projected vertices, edges and faces. 211 | * *Remove* button will remove all projections 212 | * *Projection list* contains the projected vertices, edges and faces. 213 | 214 | * Clicking on a list row will highlight the item in 3D Viewport. 215 | Click *Return to SwiftBlock* button to return to GUI panel. 216 | * Click on the cross icon to remove a projection. 217 | 218 | * *Automatic Edge Projection* select box will enable automatic 219 | snapping of edges to geometry. 220 | * *Show Internal Faces* will highlight internal faces. 221 | 222 | 223 | Boundary Patches 224 | ^^^^^^^^^^^^^^^^ 225 | 226 | Boundary Patches panel is used to specify boundary faces and their 227 | types utilizing Blender material system. Patches are shown as a 228 | list. Initially all faces are added to *default* boundary patch. 229 | 230 | **Note**: Blender may add a default "Material" material. You can 231 | remove it from the patch list unless you intend to use it. 232 | 233 | .. image:: images/boundary_patches.png 234 | 235 | * Clicking on list item will select and highlight the faces belonging 236 | to a boundary 237 | * Double-clicking on item will edit the boundary name 238 | * Clicking on the right column will open a drop-down menu, which 239 | allows to change the boundary patch type. 240 | * New boundaries are created by selecting one or more faces and then 241 | click on the plus icon. 242 | * Selected boundary is deleted by clicking on minus icon. 243 | * *Assign* button will assign selected faces to a selected boundary patch 244 | 245 | Feedback and Help 246 | ----------------- 247 | 248 | If you're new to OpenFOAM, please see links at 249 | https://holzmann-cfd.com/community/learn-openfoam, 250 | https://openfoamwiki.net and 251 | https://www.cfd-online.com/Forums/openfoam/. 252 | 253 | File bug reports for the add-on in 254 | `GitHub `_. 255 | Please ask for help for the add-on or discuss in the 256 | `SwiftBlock thread on CFD-Online `_. 257 | 258 | If you use this add-on, please star the project in GitHub! 259 | 260 | 261 | Tutorial Example: Flow Around Sphere 262 | ------------------------------------ 263 | 264 | This example shows steps to create a block mesh around a sphere. This 265 | tutorial was originally presented for the previous Blender version on 266 | `Youtube `_. 267 | The final Blender file is included in the 268 | add-on source at *example/flow_around_sphere.blend*. **Note**: This 269 | tutorial assumes that the user is familiar with mesh modelling 270 | in Blender. 271 | 272 | .. image:: images/flow_around_sphere.png 273 | 274 | * Select the default Cube object and click on *Initialize Object* in 275 | SwiftBlock panel 276 | * Select all vertices, run Swift Block operator *Extrude Blocks (Retain Internal Edges)* from 277 | either the button on the panel or 278 | the operator search menu by pressing F3 in the 3D Viewport, type 279 | name of operator, click operator name in the list. Finally, 280 | right-click to cancel moving. *Extrude Blocks (Retain Internal Edges)* creates face 281 | extrusion retaining internal edges. 282 | * Scale exruded vertices by factor 3 using selection center or origin 283 | as pivot point. This positions 6 blocks around center cube block. 284 | * Extrude the face in positive X direction by 12 m to create a 285 | block towards outlet. 286 | * Extrude the face in negative X direction by 6 m to create another 287 | block towards the inlet. 288 | * Click on *Build*, which creates 9 blocks 289 | * Click on *block 0* in the block list to select and highlight the 290 | original cube block in the center, then disable that block by 291 | clicking on the check box next to block name, so that the inside of 292 | that block will not be meshed 293 | * Go to Object Mode and add UV Sphere to origin 294 | * Go back to Object Mode, select Cube object and go to Edit Mode to 295 | view SwiftBlock panels 296 | * Click on *block 0* in block list to select center block faces. 297 | * In Projections panel, the Sphere is automatically selected as the 298 | projection object. Click *Add* to add projections to Sphere. Check 299 | that the projection list includes the 6 center faces by clicking on 300 | the face items at the end of the list to highlight them. 301 | * In order to preview mesh near sphere, you must disable one of the 302 | four outer central blocks. To do that, select one of the outer faces 303 | in the center piece, and click *Get Block from Selection* to 304 | identify the block. Then uncheck the box next to the block in the 305 | block list. 306 | * Click on *Preview* to preview the default mesh, then return to SwiftBlock. 307 | * To add grading towards the sphere, first select one of the diagonal edges 308 | inside, then click on *Select Group*, which will select all diagonal edges. 309 | * Enter values to edge parameters: Cells: 20, x1: 0.01, r1: 1.2 310 | * Then click on *Set Params* to assign those values to selected edges. 311 | * Click on *Preview* to preview the graded mesh, then return to SwiftBlock. 312 | * Enable the block that was disabled 6 steps ago. 313 | * Name Boundary Patches by selecting face(s) for each patch and click 314 | on plus icon, then rename patch and change patch type: 315 | 316 | * inlet: face on negative X end, patch type: patch 317 | * outlet: face on positive X end, patch type: patch 318 | * wall_outer: other 12 outer faces, patch type: wall 319 | 320 | * Finally rename default patch (which should now contain 6 internal 321 | faces) to wall_sphere. 322 | * Save Blender file, then click on *Export* to export blockMeshDict 323 | into an OpenFOAM case folder 324 | * Run OpenFOAM command *blockMesh* in case folder to create block 325 | volume mesh and inspect the result with e.g. 326 | `Paraview `_ 327 | 328 | 329 | More examples 330 | ------------- 331 | 332 | More examples can be found in the `example` folder of the add-on sources. 333 | 334 | .. image:: images/naca_airfoil_mesh_preview.png 335 | 336 | NACA airfoil example 337 | 338 | .. image:: images/complex_block_with_elongations.png 339 | 340 | Complex block example 341 | 342 | 343 | OpenFOAM Trade Mark Notice 344 | -------------------------- 345 | 346 | This offering is not approved or endorsed by OpenCFD Limited, producer 347 | and distributor of the OpenFOAM software via www.openfoam.com, and 348 | owner of the OPENFOAM® and OpenCFD® trade marks. 349 | -------------------------------------------------------------------------------- /example/complex_block_example.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/example/complex_block_example.blend -------------------------------------------------------------------------------- /example/flow_around_sphere.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/example/flow_around_sphere.blend -------------------------------------------------------------------------------- /example/naca_airfoil_example.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkeskita/swiftBlock/c237a9664faa36c1795381c11aa283b981879146/example/naca_airfoil_example.blend -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import numpy as np 3 | import bmesh 4 | from . import blender_utils 5 | import importlib 6 | 7 | def edgeMapping(edge): 8 | if edge["type"] == "Geometric MG": 9 | return multiGrading(edge) 10 | elif edge["type"] == "Geometric": 11 | edge["ratio"] == edge["ratio"] 12 | return edge 13 | 14 | def multiGrading(edge): 15 | eps = 1e-6 16 | grading1 = True 17 | grading2 = True 18 | x1,x2 = edge['x1'], edge['x2'] 19 | r1,r2 = edge['r1'], edge['r2'] 20 | N, L = edge['N'], edge['L'] 21 | 22 | def both(L,N,x1,x2,r1,r2,dx): 23 | n1 = np.log(dx/x1) / np.log(r1) + 1 24 | n2 = np.log(dx/x2) / np.log(r2) + 1 25 | l1 = x1*(1-r1**n1)/(1-r1) 26 | l2 = x2*(1-r2**n2)/(1-r2) 27 | Lapprox = l1 + l2 + (N - n1 - n2-1)*dx 28 | err = (L-Lapprox) 29 | return err,(n1,n2,l1,l2) 30 | 31 | def oneside(L,N,x,r,dx): 32 | n = np.log(dx/x) / np.log(r) + 1 33 | l = x*(1-r**n)/(1-r) 34 | Lapprox = l + (N - n)*dx 35 | err = (L-Lapprox) 36 | return err,(n,l) 37 | 38 | if abs(x1) < eps or (abs(r1) - 1) < eps: 39 | grading1 = False 40 | if abs(x2) < eps or (abs(r2) - 1) < eps: 41 | grading2 = False 42 | 43 | if not grading1 and not grading2: 44 | edge["l1"], edge["l2"] = 0,0 45 | edge["n1"], edge["n2"] = 0,0 46 | edge["ratio1"], edge["ratio2"] = 1,1 47 | edge["dL"], edge["nL"] = 1, N 48 | return edge 49 | 50 | elif grading1 and not grading2: 51 | l1 = x1*(1-r1**N)/(1-r1) 52 | if l1 < L: 53 | n1 = np.log(1-l1/x1*(1-r1))/np.log(r1) 54 | n1 += 1 55 | dx = x1*r1**n1 56 | edge["l1"], edge["l2"] = L,0 57 | edge["n1"], edge["n2"] = n1,0 58 | edge["ratio1"], edge["ratio2"] = dx/x1,1 59 | edge["dL"], edge["nL"] = 0, 0 60 | return edge 61 | approx = oneside 62 | x,r = x1,r1 63 | dx = L/N #initial guess 64 | parameters = [L,N,x,r,dx] 65 | elif not grading1 and grading2: 66 | l2 = x2*(1-r2**N)/(1-r2) 67 | if l2 < L: 68 | n2 = np.log(1-l2/x2*(1-r2))/np.log(r2) 69 | n2 += 1 70 | dx = x2*r2**n2 71 | edge["l1"], edge["l2"] = 0,L 72 | edge["n1"], edge["n2"] = 0,n2 73 | edge["ratio1"], edge["ratio2"] = 1,dx/x2 74 | edge["dL"], edge["nL"] = 0, 0 75 | return edge 76 | approx = oneside 77 | x,r = x2,r2 78 | dx = L/N 79 | parameters = [L,N,x,r,dx] 80 | else: 81 | n1 = (np.log(x2/x1)+N*np.log(r2))/np.log(r1*r2) 82 | n1 = int(n1+0.5) 83 | n2 = N-n1-1 84 | l1 = x1*((1-r1**n1)/(1-r1)) 85 | l2 = x2*((1-r2**n2)/(1-r2)) 86 | if (l1+l2) < L: 87 | n1 = np.log((L*(1-r1)*(1-r2)-x1-x2+x1*r2+x2*r1)/(-2*x1+x1*r1+x1*r2))/np.log(r1) 88 | n2 = np.log(x1/x2*r1**n1)/np.log(r2) 89 | l1 = x1*((1-r1**n1)/(1-r1)) 90 | l2 = x2*((1-r2**n2)/(1-r2)) 91 | dx = x1*r1**n1 92 | n1 += 1 93 | n2 += 1 94 | edge["l1"], edge["l2"] = l1,l2 95 | edge["n1"], edge["n2"] = n1,n2 96 | edge["ratio1"], edge["ratio2"] = dx/x1, dx/x2 97 | edge["dL"], edge["nL"] = 0, 0 98 | return edge 99 | # l2 = (x2-x1+L-L*r1)/(2-r2-r1) 100 | # l1 = L - l2 101 | # n1 = np.log(1-l1/x1*(1-r1))/np.log(r1) 102 | # n2 = N-n1 103 | approx = both 104 | dx = L/N 105 | parameters = [L,N,x1,x2,r1,r2,dx] 106 | 107 | 108 | Lapprox = 0.0 109 | err = 1.0 110 | count = 0 111 | 112 | err,pars=approx(*parameters) 113 | dx_old = dx 114 | err_old = err 115 | dx = dx*1.2*1e-10 # small perturbation 116 | parameters[-1] = dx 117 | err,pars=approx(*parameters) 118 | 119 | while abs(err)>1e-12 and count < 1000: 120 | dx_temp = dx 121 | derr = (err - err_old)/(dx - dx_old) 122 | dx = dx - err/derr 123 | dx_old = dx_temp 124 | err_old = err 125 | parameters[-1] = dx 126 | err, out = approx(*parameters) 127 | count = count+1 128 | 129 | if grading1 and not grading2: 130 | n1,l1 = out 131 | ratio1 = dx/x1 132 | n2,l2,ratio2 = 0,0,1 133 | elif not grading1 and grading2: 134 | n2,l2 = out 135 | ratio2 = dx/x2 136 | n1,l1,ratio1 = 0,0,1 137 | else: 138 | n1,n2,l1,l2 = out 139 | ratio1 = dx/x1 140 | ratio2 = dx/x2 141 | 142 | if (dx < x1 and abs(x1) > eps) or (dx < x2 and abs(x2) > eps): 143 | dx = x1 144 | l1, l2 = 0,0 145 | n1, n2 = 0,0 146 | ratio1, ratio2 = 1, 1 147 | 148 | dL = L-l1-l2 149 | nL = N-n1-n2 150 | dx = dL/nL 151 | edge['l1'], edge['l2'] = l1, l2 152 | edge['n1'], edge['n2'] = n1, n2 153 | edge['ratio1'], edge['ratio2'] = ratio1, ratio2 154 | edge['dL'], edge['nL'] = dL, nL 155 | return edge 156 | 157 | def getNodes(x1,x2,r1,r2,L,dx): 158 | n1 = np.log(dx/x1)/np.log(r1) + 1 159 | n2 = np.log(dx/x1)/np.log(r1) + 1 160 | l1 = x1*(1-r1**n1)/(1-r1) 161 | l2 = x2*(1-r2**n2)/(1-r2) 162 | if (l1+l2) > L: 163 | n1 = np.log((L*(1-r1)*(1-r2)-x1-x2+x1*r2+x2*r1)/(-2*x1+x1*r1+x1*r2))/np.log(r1) 164 | n1 = int(n1+0.5)+1 165 | n2 = np.log(x1/x2*r1**n1)/np.log(r2) 166 | n2 = int(n2+0.5) 167 | l1 = x1*((1-r1**n1)/(1-r1)) 168 | l2 = x2*((1-r2**n2)/(1-r2)) 169 | dx = x1*r1**n1 170 | return n1+n2 171 | else: 172 | return n1+n2+(L-l1-l2)/dx 173 | 174 | def edge(e0, e1): 175 | return [min(e0,e1), max(e0,e1)] 176 | 177 | def findFace(faces, vl): 178 | for fid, f in enumerate(faces): 179 | if vl[0] in f and vl[1] in f and vl[2] in f and vl[3] in f: 180 | return fid, f 181 | return -1, [] 182 | 183 | 184 | # No comments. Just works. 185 | def getEdgeDirections(block_print_out, dependent_edges): 186 | edgeDirections = [set() for i in dependent_edges] 187 | positiveBlockEdges = [[(0,1),(3,2),(7,6),(4,5)],[(0,3),(1,2),(5,6),(4,7)],[(0,4),(1,5),(2,6),(3,7)]] 188 | for i in range(1000): 189 | ready = True 190 | for ed, de in zip(edgeDirections,dependent_edges): 191 | if not len(ed)==len(de): 192 | ready = False 193 | if ready: 194 | break 195 | for bid, vl in enumerate(block_print_out): 196 | for es, edgeSet in enumerate(dependent_edges): 197 | for direction in range(3): 198 | if edge(vl[positiveBlockEdges[direction][0][0]],vl[positiveBlockEdges[direction][0][1]]) in edgeSet: 199 | if not edgeDirections[es]: 200 | edgeDirections[es] = set([(vl[e[0]],vl[e[1]]) for e in positiveBlockEdges[direction]]) 201 | else: 202 | simedges = edgeDirections[es].intersection([(vl[e[0]],vl[e[1]]) for e in positiveBlockEdges[direction]]) 203 | if simedges: 204 | edgeDirections[es] |= set([(vl[e[0]],vl[e[1]]) for e in positiveBlockEdges[direction]]) 205 | else: 206 | asimedges= set(edgeDirections[es]).intersection([(vl[e[1]],vl[e[0]]) for e in positiveBlockEdges[direction]]) 207 | if asimedges: 208 | edgeDirections[es] |= set([(vl[e[1]],vl[e[0]]) for e in positiveBlockEdges[direction]]) 209 | 210 | return edgeDirections 211 | 212 | def sortEdges(edges): 213 | sorted=[] 214 | # Find out if the edges form a loop 215 | edges1D=np.ravel(edges) 216 | occ=np.bincount(edges1D) 217 | # This is a loop, let's just start sorting anywhere (from first element here) 218 | if len(np.where(occ==1)[0])==0: 219 | sorted.append(edges[0][0]) 220 | # This is not a loop, let's find the first or last element 221 | else: 222 | # Find a vertex which occurs only 1 and then it's place in 2D list 223 | firstidx1D=np.where(edges1D==np.where(occ==1)[0][0])[0][0] 224 | if firstidx1D % 2 == 0: 225 | sorted.append(edges[int(firstidx1D/2)][0]) 226 | else: 227 | sorted.append(edges[int((firstidx1D-1)/2)][1]) 228 | edgesTemp = [] 229 | vertids = [] 230 | for e in edges: 231 | vertids.append(e[0]) 232 | vertids.append(e[1]) 233 | vertids = list(set(vertids)) 234 | vertid=sorted[0] 235 | edgesTemp=edges[:] 236 | for i in range(len(vertids)): 237 | for eid, e in enumerate(edgesTemp): 238 | if vertid in e: 239 | if e[0] == vertid: 240 | sorted.append(e[1]) 241 | else: 242 | sorted.append(e[0]) 243 | edgesTemp.pop(eid) 244 | vertid = sorted[-1] 245 | break 246 | return sorted 247 | 248 | def obFromStructuredMesh(verts, dim, objName): 249 | context = bpy.context 250 | nx, ny, nz = dim 251 | edges = [] 252 | faces = [] 253 | boundary_verts = [] 254 | boundary_mes = [] 255 | boundary_verts.append(list(verts[0:nx*ny])) 256 | boundary_verts.append(verts[nx*ny*nz-nx*ny:]) 257 | boundary_verts.append(verts[::nx]) 258 | boundary_verts.append(verts[nx-1::nx]) 259 | boundary_verts.append([]) 260 | 261 | for sverts in range(0,nx*ny*nz,nx*ny): 262 | boundary_verts[-1].extend(verts[sverts:sverts+nx]) 263 | 264 | boundary_verts.append([]) 265 | for sverts in range(nx*ny-nx,nx*ny*nz,nx*ny): 266 | boundary_verts[-1].extend(verts[sverts:sverts+nx]) 267 | 268 | verts = [v for bv in boundary_verts for v in bv] 269 | boundary_faces = [] 270 | boundary_ij = [[nx,ny], [ny,nz], [nx,nz]] 271 | vert_idx = 0 272 | # With Numpy slicing? 273 | for ni, nj in boundary_ij: 274 | bf = [] 275 | for j in range(nj-1): 276 | for i in range(ni-1): 277 | bf.append((i+j*ni,1+i+j*ni,1+i+(1+j)*ni,i+(1+j)*ni)) 278 | 279 | boundary_faces.append(bf) 280 | boundary_faces.append(bf) 281 | # Blender face arrays do not work with np.ints 282 | faces.extend((np.array(bf)+vert_idx).tolist()) 283 | vert_idx += ni*nj 284 | 285 | faces.extend((np.array(bf)+vert_idx).tolist()) 286 | vert_idx += ni*nj 287 | 288 | 289 | boundary_mes = [bpy.data.meshes.new('boundary_%s'%i) for i in range(6)] 290 | for bm, bv, bf in zip(boundary_mes, boundary_verts, boundary_faces): 291 | bm.from_pydata(bv, [], bf) 292 | 293 | vol_me=bpy.data.meshes.new('internal') 294 | vol_me.from_pydata(verts, edges,faces) 295 | vol_me.update() 296 | 297 | ob = bpy.data.objects.new(objName,vol_me) 298 | bpy.context.collection.objects.link(ob) 299 | boundary_obs = [] 300 | for i, bm in enumerate(boundary_mes): 301 | boundary_ob = bpy.data.objects.new(objName+ '_{}'.format(i), bm) 302 | boundary_ob.parent = ob 303 | # boundary_ob.show_all_edges = True 304 | # boundary_ob.show_wire = True 305 | bpy.context.collection.objects.link(boundary_ob) 306 | boundary_obs.append(boundary_ob) 307 | return ob 308 | 309 | 310 | def getBlockFaces(verts): 311 | fids = [(0,1,5,4),(0,3,2,1),(3,7,6,2),(4,5,6,7),(0,4,7,3),(1,2,6,5)] 312 | faces = [(verts[f[0]],verts[f[1]],verts[f[2]],verts[f[3]]) for f in fids] 313 | return faces 314 | 315 | 316 | # Utility functions 317 | # ----------------- 318 | 319 | def collectEdges(bob, lengths): 320 | bob.select_set(True) 321 | bpy.context.view_layer.objects.active = bob 322 | bpy.ops.object.mode_set(mode='EDIT') 323 | # snap_vertices = get_snap_vertices(bob) 324 | bm = bmesh.from_edit_mesh(bob.data) 325 | layers = bm.edges.layers 326 | # snapIdl = layers.string.get('snapId') 327 | block_edges = dict() 328 | 329 | timel = layers.int.get('modtime') 330 | groupl = layers.int.get('groupid') 331 | x1l = layers.float.get('x1') 332 | x2l = layers.float.get('x2') 333 | r1l = layers.float.get('r1') 334 | r2l = layers.float.get('r2') 335 | cellsl = layers.int.get('cells') 336 | ratiol = layers.float.get("ratio") 337 | 338 | ncells = dict() 339 | times = dict() 340 | for e in bm.edges: 341 | if e[groupl] not in ncells: 342 | ncells[e[groupl]] = e[cellsl] 343 | times[e[groupl]] = e[timel] 344 | elif e[timel] > times[e[groupl]]: 345 | ncells[e[groupl]] = e[cellsl] 346 | times[e[groupl]] = e[timel] 347 | 348 | for e in bm.edges: 349 | be = dict() 350 | ev = list([e.verts[0].index,e.verts[1].index]) 351 | if ev in lengths[0]: 352 | ind = lengths[0].index(ev) 353 | L = lengths[1][ind] 354 | else: 355 | L = (e.verts[0].co-e.verts[1].co).length 356 | be["type"] = bob.swiftBlock_MappingType 357 | be["x1"] = e[x1l] 358 | be["x2"] = e[x2l] 359 | be["r1"] = e[r1l] 360 | be["r2"] = e[r2l] 361 | be["N"] = ncells[e[groupl]] 362 | be["ratio"] = e[ratiol] 363 | be["L"] = L 364 | if not be["N"]: 365 | be["N"] = 10 366 | if not be["r1"]: 367 | be["r1"] = 1. 368 | if not be["r2"]: 369 | be["r2"] = 1. 370 | if not be["ratio"]: 371 | be["ratio"] = 1 372 | be = edgeMapping(be) 373 | block_edges[(e.verts[1].index,e.verts[0].index)] = be 374 | be = dict(be) 375 | be["x1"],be["x2"] = be["x2"],be["x1"] 376 | be["r1"],be["r2"] = be["r2"],be["r1"] 377 | be["ratio"] = 1./be["ratio"] 378 | be = edgeMapping(be) 379 | 380 | block_edges[(e.verts[0].index,e.verts[1].index)] = be 381 | return block_edges 382 | 383 | 384 | # Build the mesh from already existing blocking 385 | def writeMesh(ob, folder = ''): 386 | if not ob.swiftBlock_blocks: 387 | bpy.ops.swift_block.build_blocking('INVOKE_DEFAULT') 388 | 389 | verts = list(blender_utils.vertices_from_mesh(ob)) 390 | bm = bmesh.from_edit_mesh(ob.data) 391 | 392 | # do not write polylines for hidden edges 393 | edges = [] 394 | for e in bm.edges: 395 | if not e.hide: 396 | edges.append((e.verts[0].index, e.verts[1].index)) 397 | 398 | bpy.ops.object.mode_set(mode='OBJECT') 399 | 400 | 401 | ob.select_set(False) 402 | if ob.swiftBlock_Autosnap and ob.swiftBlock_EdgeSnapObject: 403 | polyLines, polyLinesPoints, lengths = getPolyLines(verts, edges, ob) 404 | else: 405 | polyLines = [] 406 | lengths = [[]] 407 | verts = [] 408 | matrix = ob.matrix_world.copy() 409 | for v in ob.data.vertices: 410 | verts.append(matrix @ v.co) 411 | 412 | blocks = [] 413 | block_names = [] 414 | for b in ob.swiftBlock_blocks: 415 | if b.enabled: 416 | blocks.append(list(b.verts)) 417 | if b.namedRegion: 418 | block_names.append(b.name) 419 | else: 420 | block_names.append('') 421 | 422 | edgeInfo = collectEdges(ob,lengths) 423 | 424 | bm = bmesh.from_edit_mesh(ob.data) 425 | detemp = [] 426 | groupl = bm.edges.layers.int.get('groupid') 427 | ngroups = 0 428 | for e in bm.edges: 429 | detemp.append((e[groupl],e.verts[0].index,e.verts[1].index)) 430 | ngroups = max(ngroups,e[groupl]) 431 | 432 | block_edges = [[] for i in range(ngroups+1)] 433 | for e in detemp: 434 | block_edges[e[0]].append([e[1],e[2]]) 435 | 436 | enabledl = bm.faces.layers.int.get('enabled') 437 | bm.verts.ensure_lookup_table() 438 | bm.edges.ensure_lookup_table() 439 | bm.faces.ensure_lookup_table() 440 | 441 | projections = {'vert2surf':dict(),'edge2surf':dict(),'face2surf':dict(), 'geo':dict()} 442 | for p in ob.swiftBlock_projections: 443 | if p.type == 'vert2surf' and any([f[enabledl] for f in bm.verts[p.id].link_faces]): 444 | key = bm.verts[p.id].index 445 | if key in projections[p.type]: 446 | projections[p.type][key] += " {}".format(p.ob) 447 | else: 448 | projections[p.type][key] = p.ob 449 | elif p.type == 'edge2surf' and any([f[enabledl] for f in bm.edges[p.id].link_faces]): 450 | key = tuple(v.index for v in bm.edges[p.id].verts) 451 | if key in projections[p.type]: 452 | projections[p.type][key] += " {}".format(p.ob) 453 | else: 454 | projections[p.type][key] = p.ob 455 | elif p.type == 'face2surf' and bm.faces[p.id][enabledl]: 456 | key = tuple(v.index for v in bm.faces[p.id].verts) 457 | projections[p.type][key] = p.ob 458 | 459 | selected_edges = [e.select for e in ob.data.edges] 460 | 461 | boundaries = [{'name':mat.name, 'type':mat.boundary_type, 'faceVerts':[]} for mat in ob.data.materials] 462 | for f in bm.faces: 463 | if f[enabledl] == 1: 464 | boundaries[f.material_index]['faceVerts'].append([v.index for v in f.verts]) 465 | for b in boundaries: 466 | if not b['faceVerts']: 467 | boundaries.remove(b) 468 | 469 | # return edge selection 470 | bpy.ops.mesh.select_all(action='DESELECT') 471 | bpy.ops.object.mode_set(mode='OBJECT') 472 | for e,sel in zip(ob.data.edges,selected_edges): 473 | e.select = sel 474 | 475 | ### This is everything that is related to blockMesh so a new multiblock mesher could be introduced easily just by creating new preview file ### 476 | if ob.swiftBlock_Mesher == 'blockMeshMG': 477 | from . import blockMeshMG 478 | importlib.reload(blockMeshMG) 479 | if folder: 480 | mesh = blockMeshMG.PreviewMesh(folder) 481 | else: 482 | mesh = blockMeshMG.PreviewMesh() 483 | # projection_tris = writeProjectionObjects(project_verts,project_edges,project_faces, mesh.geomPath) 484 | if ob.swiftBlock_projections: 485 | geos = writeProjectionObjects(ob, mesh.geomPath) 486 | projections['geo'] = geos 487 | 488 | cells = mesh.writeBlockMeshDict(verts, 1, boundaries, polyLines, edgeInfo, block_names, blocks, block_edges, projections) 489 | ############################################################### 490 | elif ob.swiftBlock_Mesher == 'blockMeshBodyFit': 491 | from . import blockMeshBodyFit 492 | importlib.reload(blockMeshBodyFit) 493 | if folder: 494 | mesh = blockMeshBodyFit.PreviewMesh(folder) 495 | else: 496 | mesh = blockMeshBodyFit.PreviewMesh() 497 | writeProjectionObjects(ob, mesh.triSurfacePath, onlyFaces = True) 498 | cells = mesh.writeBlockMeshDict(verts, 1, boundaries, polyLines, edgeInfo, block_names, blocks, block_edges, projections, ob.swiftBlock_SearchLength) 499 | bpy.ops.wm.context_set_value(data_path="tool_settings.mesh_select_mode", value="(False,True,False)") 500 | return mesh, cells 501 | 502 | 503 | def changeMesher(self, context): 504 | ob = context.active_object 505 | if ob.swiftBlock_Mesher == "blockMeshMG": 506 | ob.swiftBlock_MappingType = "Geometric MG" 507 | elif ob.swiftBlock_Mesher == "blockMeshBodyFit": 508 | ob.swiftBlock_MappingType = "Geometric" 509 | 510 | def showInternalFaces(self, context): 511 | ob = context.active_object 512 | hideFacesEdges(ob, ob.swiftBlock_ShowInternalFaces) 513 | 514 | def hideFacesEdges(ob, showInternal = False): 515 | ob.data.update() 516 | bm = bmesh.from_edit_mesh(ob.data) 517 | bm.verts.ensure_lookup_table() 518 | 519 | negl = bm.faces.layers.int.get('neg') 520 | posl = bm.faces.layers.int.get('pos') 521 | enabledl = bm.faces.layers.int.get('enabled') 522 | 523 | for f in bm.faces: 524 | if f[negl] != -1 and f[posl] != -1: 525 | if (not ob.swiftBlock_blocks[f[posl]].enabled and ob.swiftBlock_blocks[f[negl]].enabled) \ 526 | or (ob.swiftBlock_blocks[f[posl]].enabled and not ob.swiftBlock_blocks[f[negl]].enabled): 527 | # boundary face 528 | f.hide = False # = False 529 | f[enabledl] = 1 530 | elif not ob.swiftBlock_blocks[f[posl]].enabled and not ob.swiftBlock_blocks[f[negl]].enabled: 531 | # both blocks disabled 532 | f[enabledl] = False 533 | f.hide = True 534 | elif showInternal: 535 | # internal face 536 | f[enabledl] = 2 537 | f.hide = False 538 | else: 539 | # internal face 540 | f[enabledl] = 2 541 | f.hide = True 542 | elif (f[posl] == -1 and f[negl] != -1): #boundary face 543 | if ob.swiftBlock_blocks[f[negl]].enabled: 544 | # boundary face 545 | f.hide = False # = False 546 | f[enabledl] = 1 547 | else: 548 | # boundary block disabled 549 | f.hide = True 550 | f[enabledl] = False 551 | elif (f[posl] != -1 and f[negl] == -1): #boundary face 552 | if ob.swiftBlock_blocks[f[posl]].enabled: 553 | # boundary face 554 | f.hide = False 555 | f[enabledl] = 1 556 | else: 557 | # boundary block disabled 558 | f.hide = True 559 | f[enabledl] = False 560 | 561 | for e in bm.edges: 562 | edge_found = False 563 | for b in ob.swiftBlock_blocks: 564 | if b.enabled and e.verts[0].index in b.verts and e.verts[1].index in b.verts: 565 | edge_found = True 566 | e.hide = False 567 | continue 568 | if not edge_found: 569 | e.hide = True 570 | 571 | bpy.ops.swift_block.draw_edge_directions('INVOKE_DEFAULT',show=False) 572 | ob.data.update() 573 | 574 | # Get all objects in current context 575 | def getProjectionObjects(self, context): 576 | obs = [] 577 | for ob in bpy.data.objects: 578 | if ob.type == "MESH" and not ob.swiftBlock_isblockingObject and not ob.swiftBlock_ispreviewObject and not ob.swiftBlock_isdirectionObject: 579 | obs.append((ob.name, ob.name, '')) 580 | return obs 581 | 582 | def updateBoundaryColor(self, context): 583 | ob = context.active_object 584 | mat = bpy.data.materials[self.name] 585 | mat.diffuse_color = self.color 586 | 587 | def updateBoundaryName(self, context): 588 | ob = context.active_object 589 | mat = bpy.data.materials[self.oldName] 590 | mat.name = self.name 591 | self.oldName = mat.name 592 | 593 | 594 | # This function checks that the vert, edge or face is still there. 595 | # Unfortunately, the projection ids might be wrong if verts, edges 596 | # or faces have been deleted. 597 | def updateProjections(ob): 598 | bm = bmesh.from_edit_mesh(ob.data) 599 | bm.verts.ensure_lookup_table() 600 | bm.edges.ensure_lookup_table() 601 | bm.faces.ensure_lookup_table() 602 | remove_projections = [] 603 | for i, p in enumerate(ob.swiftBlock_projections): 604 | try: 605 | if p.type == 'vert2surf': 606 | bm.verts[p.id] 607 | elif p.type == 'edge2surf': 608 | bm.edges[p.id] 609 | elif p.type == 'face2surf': 610 | bm.faces[p.id] 611 | except IndexError: 612 | remove_projections.append(i) 613 | for pid in reversed(sorted(remove_projections)): 614 | ob.swiftBlock_projections.remove(pid) 615 | 616 | # Boundary condition operators 617 | def selectActiveBoundary(self, context): 618 | ob = context.active_object 619 | ob.active_material_index = ob.swiftBlock_boundary_index 620 | 621 | bm = bmesh.from_edit_mesh(ob.data) 622 | bpy.ops.mesh.select_all(action='DESELECT') 623 | 624 | for f in bm.faces: 625 | if f.material_index == ob.swiftBlock_boundary_index: 626 | f.select=True 627 | 628 | def patchColor(patch_no): 629 | color = [(0.25,0.25,0.25,1), (1.0,0.,0.,1), (0.0,1.,0.,1), (0.0,0.,1.,1), \ 630 | (0.707,0.707,0,1), (0,0.707,0.707,1), (0.707,0,0.707,1)] 631 | return color[patch_no % len(color)] 632 | 633 | def writeProjectionObjects(ob, path, onlyFaces = False): 634 | blender_version = bpy.app.version[1] 635 | objects = [] 636 | for p in ob.swiftBlock_projections: 637 | if onlyFaces and not p.type == 'face2surf': 638 | continue 639 | else: 640 | objects.append(p.ob) 641 | objects = set(objects) 642 | for o in objects: 643 | sob = bpy.data.objects[o] 644 | hide = sob.hide_get() 645 | blender_utils.activateObject(sob) 646 | 647 | filepath = path + '/{}.stl'.format(o) 648 | if "export_mesh" in dir(bpy.ops): 649 | # Blender 3.6 and earlier 650 | bpy.ops.export_mesh.stl(filepath=filepath, use_selection=True) 651 | elif "stl_export" in dir(bpy.ops.wm): 652 | # Blender 4.2 and later 653 | bpy.ops.wm.stl_export(filepath=filepath, export_selected_objects=True) 654 | else: 655 | raise Exception("No known STL exporters found") 656 | 657 | sob.hide_set(hide) 658 | blender_utils.activateObject(ob) 659 | return objects 660 | 661 | 662 | # Kalle's implementation 663 | def getPolyLines(verts, edges, bob): 664 | polyLinesPoints = [] 665 | polyLines = '' 666 | polyLinesLengths = [[], []] 667 | tol = 1e-6 668 | 669 | def isPointOnEdge(point, A, B): 670 | eps = (((A - B).magnitude - (point-B).magnitude) - (A-point).magnitude) 671 | return True if (abs(eps) < tol) else False 672 | 673 | # nosnap= [False for i in range(len(edges))] 674 | # for eid, e in enumerate(obj.data.edges): 675 | # nosnap[eid] = e.use_edge_sharp 676 | 677 | bpy.ops.wm.context_set_value(data_path="tool_settings.mesh_select_mode", value="(True,False,False)") 678 | geoobj = bpy.data.objects[bob.swiftBlock_EdgeSnapObject] 679 | geo_verts = list(blender_utils.vertices_from_mesh(geoobj)) 680 | geo_edges = list(blender_utils.edges_from_mesh(geoobj)) 681 | geoobj.select_set(False) # avoid deletion 682 | 683 | # First go through all vertices in the block structure and find vertices snapped to edges 684 | # When found, add a vertex at that location to the polyLine object by splitting the edge 685 | # Create a new Blender object containing the newly inserted verts. Then use Blender's 686 | # shortest path algo to find polyLines. 687 | 688 | for vid, v in enumerate(verts): 689 | found = False 690 | for gvid, gv in enumerate(geo_verts): 691 | mag = (v-gv).magnitude 692 | if mag < tol: 693 | found = True 694 | break # We have found a vertex co-located, continue with next block vertex 695 | if not found: 696 | for geid, ge in enumerate(geo_edges): 697 | if (isPointOnEdge(v, geo_verts[ge[0]], geo_verts[ge[1]])): 698 | geo_verts.append(v) 699 | geo_edges.append([geo_edges[geid][1],len(geo_verts)-1]) # Putting the vert on the edge, by splitting it in two. 700 | geo_edges[geid][1] = len(geo_verts)-1 701 | break # No more iteration, go to next block vertex 702 | 703 | mesh_data = bpy.data.meshes.new("deleteme") 704 | mesh_data.from_pydata(geo_verts, geo_edges, []) 705 | mesh_data.update() 706 | geoobj = bpy.data.objects.new('deleteme', mesh_data) 707 | bpy.context.collection.objects.link(geoobj) 708 | geo_verts = list(blender_utils.vertices_from_mesh(geoobj)) 709 | geo_edges = list(blender_utils.edges_from_mesh(geoobj)) 710 | bpy.context.view_layer.objects.active=geoobj 711 | 712 | # Now start the search over again on the new object with more verts 713 | snapped_verts = {} 714 | for vid, v in enumerate(verts): 715 | for gvid, gv in enumerate(geo_verts): 716 | mag = (v-gv).magnitude 717 | if mag < tol: 718 | snapped_verts[vid] = gvid 719 | break # We have found a vertex co-located, continue with next block vertex 720 | 721 | bpy.ops.wm.context_set_value(data_path="tool_settings.mesh_select_mode", value="(True,False,False)") 722 | for edid, ed in enumerate(edges): 723 | if ed[0] in snapped_verts and ed[1] in snapped_verts:# and not nosnap[edid]: 724 | geoobj.hide_set(False) 725 | bpy.ops.object.mode_set(mode='EDIT') 726 | bpy.ops.mesh.select_all(action='DESELECT') 727 | bpy.ops.object.mode_set(mode='OBJECT') 728 | geoobj.data.vertices[snapped_verts[ed[0]]].select=True 729 | geoobj.data.vertices[snapped_verts[ed[1]]].select=True 730 | bpy.ops.object.mode_set(mode='EDIT') 731 | try: 732 | bpy.ops.mesh.select_vertex_path(type='EDGE_LENGTH') 733 | except: 734 | bpy.ops.mesh.shortest_path_select() 735 | bpy.ops.object.mode_set(mode='OBJECT') 736 | bpy.ops.object.mode_set(mode='EDIT') 737 | bpy.ops.mesh.duplicate() 738 | bpy.ops.object.mode_set(mode='OBJECT') 739 | bpy.ops.object.mode_set(mode='EDIT') 740 | bpy.ops.mesh.separate(type='SELECTED') 741 | bpy.ops.object.mode_set(mode='OBJECT') 742 | polyLineobj = bpy.data.objects['deleteme.001'] 743 | if len(polyLineobj.data.vertices) > 2: 744 | polyLineverts = list(blender_utils.vertices_from_mesh(polyLineobj)) 745 | polyLineedges = list(blender_utils.edges_from_mesh(polyLineobj)) 746 | for vid, v in enumerate(polyLineverts): 747 | mag = (v-verts[ed[0]]).magnitude 748 | if mag < tol: 749 | startVertex = vid 750 | break 751 | polyLineStr, vectors, length = sortedVertices(polyLineverts,polyLineedges,startVertex) 752 | polyLinesPoints.append([ed[0],ed[1],vectors]) 753 | polyLinesLengths[0].append([min(ed[0],ed[1]), max(ed[0],ed[1])]) # write out sorted 754 | polyLinesLengths[1].append(length) 755 | polyLine = 'polyLine {} {} ('.format(*ed) 756 | polyLine += polyLineStr 757 | polyLine += ')\n' 758 | polyLines += polyLine 759 | 760 | geoobj.select_set(False) 761 | polyLineobj.select_set(True) 762 | bpy.ops.object.delete() 763 | geoobj.select_set(True) 764 | bpy.ops.object.delete() 765 | return polyLines, polyLinesPoints, polyLinesLengths 766 | 767 | def sortedVertices(verts,edges,startVert): 768 | sorted = [] 769 | vectors = [] 770 | sorted.append(startVert) 771 | vert = startVert 772 | length = len(edges)+1 773 | for i in range(len(verts)): 774 | for eid, e in enumerate(edges): 775 | if vert in e: 776 | if e[0] == vert: 777 | sorted.append(e[1]) 778 | else: 779 | sorted.append(e[0]) 780 | edges.pop(eid) 781 | vert = sorted[-1] 782 | break 783 | 784 | polyLine = '' 785 | length = 0. 786 | for vid, v in enumerate(sorted): 787 | polyLine += '({} {} {})'.format(*verts[v]) 788 | vectors.append(verts[v]) 789 | if vid>=1: 790 | length += (vectors[vid] - vectors[vid-1]).magnitude 791 | return polyLine, vectors, length 792 | --------------------------------------------------------------------------------