├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── bake_operators.py ├── car_rig.py └── widgets.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /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 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 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 | {project} Copyright (C) {year} {fullname} 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 | Rigacar is a free addon for Blender. It is designed to fulfill the following goals: 2 | 3 | * generate a complete rig as quickly as possible (actually few seconds) for standard car models 4 | * provide tools to automate wheels animation 5 | * allow efficient animation baking to be able to export animated models into real time renderers 6 | 7 | Please read [full documentation](http://digicreatures.net/articles/rigacar.html) on my website. 8 | 9 | You can also watch the series of videotutorials: 10 | 11 | [![Rigacar Part 1](http://img.youtube.com/vi/D3XQxA_-TzY/0.jpg)](https://www.youtube.com/watch?v=D3XQxA_-TzY&list=PLH_mmrv8SfPFiEj93RJt3sBvHCnipI9qK "Rigacar videotutorials") 12 | -------------------------------------------------------------------------------- /__init__.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 3 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 | # 20 | 21 | bl_info = { 22 | "name": "Rigacar (Generates Car Rig)", 23 | "author": "David Gayerie", 24 | "version": (7, 1), 25 | "blender": (2, 83, 0), 26 | "location": "View3D > Add > Armature", 27 | "description": "Adds a deformation rig for vehicules, generates animation rig and bake wheels animation.", 28 | "wiki_url": "http://digicreatures.net/articles/rigacar.html", 29 | "tracker_url": "https://github.com/digicreatures/rigacar/issues", 30 | "category": "Rigging"} 31 | 32 | 33 | if "bpy" in locals(): 34 | import importlib 35 | if "bake_operators" in locals(): 36 | importlib.reload(bake_operators) 37 | if "car_rig" in locals(): 38 | importlib.reload(car_rig) 39 | if "widgets" in locals(): 40 | importlib.reload(widgets) 41 | else: 42 | import bpy 43 | from . import bake_operators 44 | from . import car_rig 45 | 46 | 47 | def enumerate_ground_sensors(bones): 48 | bone = bones.get('GroundSensor.Axle.Ft') 49 | if bone is not None: 50 | yield bone 51 | for bone in bones: 52 | if bone.name.startswith('GroundSensor.Ft'): 53 | yield bone 54 | bone = bones.get('GroundSensor.Axle.Bk') 55 | if bone is not None: 56 | yield bone 57 | for bone in bones: 58 | if bone.name.startswith('GroundSensor.Bk'): 59 | yield bone 60 | 61 | 62 | class RIGACAR_PT_mixin: 63 | 64 | def __init__(self): 65 | self.layout.use_property_split = True 66 | self.layout.use_property_decorate = False 67 | 68 | @classmethod 69 | def is_car_rig(cls, context): 70 | return context.object is not None and context.object.data is not None and 'Car Rig' in context.object.data 71 | 72 | @classmethod 73 | def is_car_rig_generated(cls, context): 74 | return cls.is_car_rig(context) and context.object.data['Car Rig'] 75 | 76 | def display_generate_section(self, context): 77 | self.layout.operator(car_rig.POSE_OT_carAnimationRigGenerate.bl_idname, text='Generate') 78 | 79 | def display_bake_section(self, context): 80 | self.layout.operator(bake_operators.ANIM_OT_carSteeringBake.bl_idname) 81 | self.layout.operator(bake_operators.ANIM_OT_carWheelsRotationBake.bl_idname) 82 | self.layout.operator(bake_operators.ANIM_OT_carClearSteeringWheelsRotation.bl_idname) 83 | 84 | def display_rig_props_section(self, context): 85 | layout = self.layout.column() 86 | layout.prop(context.object, '["wheels_on_y_axis"]', text="Wheels on Y axis") 87 | layout.prop(context.object, '["suspension_factor"]', text="Pitch factor") 88 | layout.prop(context.object, '["suspension_rolling_factor"]', text="Roll factor") 89 | 90 | def display_ground_sensors_section(self, context): 91 | for ground_sensor in enumerate_ground_sensors(context.object.pose.bones): 92 | ground_projection_constraint = ground_sensor.constraints.get('Ground projection') 93 | self.layout.label(text=ground_sensor.name, icon='BONE_DATA') 94 | if ground_projection_constraint is not None: 95 | self.layout.prop(ground_projection_constraint, 'target', text='Ground') 96 | if ground_projection_constraint.target is not None: 97 | self.layout.prop(ground_projection_constraint, 'shrinkwrap_type') 98 | if ground_projection_constraint.shrinkwrap_type == 'PROJECT': 99 | self.layout.prop(ground_projection_constraint, 'project_limit') 100 | self.layout.prop(ground_projection_constraint, 'influence') 101 | ground_projection_limit_constraint = ground_sensor.constraints.get('Ground projection limitation') 102 | if ground_projection_limit_constraint is not None: 103 | self.layout.prop(ground_projection_limit_constraint, 'min_z', text='Min local Z') 104 | self.layout.prop(ground_projection_limit_constraint, 'max_z', text='Max local Z') 105 | self.layout.separator() 106 | 107 | 108 | class RIGACAR_PT_rigProperties(bpy.types.Panel, RIGACAR_PT_mixin): 109 | bl_label = "Rigacar" 110 | bl_space_type = "PROPERTIES" 111 | bl_region_type = "WINDOW" 112 | bl_context = "data" 113 | bl_options = {'DEFAULT_CLOSED'} 114 | 115 | @classmethod 116 | def poll(cls, context): 117 | return RIGACAR_PT_mixin.is_car_rig(context) 118 | 119 | def draw(self, context): 120 | if RIGACAR_PT_mixin.is_car_rig_generated(context): 121 | self.display_rig_props_section(context) 122 | self.layout.separator() 123 | self.display_bake_section(context) 124 | else: 125 | self.display_generate_section(context) 126 | 127 | 128 | class RIGACAR_PT_groundSensorsProperties(bpy.types.Panel, RIGACAR_PT_mixin): 129 | bl_label = "Ground Sensors" 130 | bl_parent_id = "RIGACAR_PT_rigProperties" 131 | bl_space_type = "PROPERTIES" 132 | bl_region_type = "WINDOW" 133 | bl_context = "data" 134 | bl_options = {'DEFAULT_CLOSED'} 135 | 136 | @classmethod 137 | def poll(cls, context): 138 | return RIGACAR_PT_mixin.is_car_rig_generated(context) 139 | 140 | def draw(self, context): 141 | self.display_ground_sensors_section(context) 142 | 143 | 144 | class RIGACAR_PT_animationRigView(bpy.types.Panel, RIGACAR_PT_mixin): 145 | bl_category = "Rigacar" 146 | bl_label = "Animation Rig" 147 | bl_space_type = "VIEW_3D" 148 | bl_region_type = "UI" 149 | 150 | @classmethod 151 | def poll(cls, context): 152 | return RIGACAR_PT_mixin.is_car_rig(context) 153 | 154 | def draw(self, context): 155 | if RIGACAR_PT_mixin.is_car_rig_generated(context): 156 | self.display_rig_props_section(context) 157 | else: 158 | self.display_generate_section(context) 159 | 160 | 161 | class RIGACAR_PT_wheelsAnimationView(bpy.types.Panel, RIGACAR_PT_mixin): 162 | bl_category = "Rigacar" 163 | bl_label = "Wheels animation" 164 | bl_space_type = "VIEW_3D" 165 | bl_region_type = "UI" 166 | 167 | @classmethod 168 | def poll(cls, context): 169 | return RIGACAR_PT_mixin.is_car_rig_generated(context) 170 | 171 | def draw(self, context): 172 | self.display_bake_section(context) 173 | 174 | 175 | class RIGACAR_PT_groundSensorsView(bpy.types.Panel, RIGACAR_PT_mixin): 176 | bl_category = "Rigacar" 177 | bl_label = "Ground Sensors" 178 | bl_space_type = "VIEW_3D" 179 | bl_region_type = "UI" 180 | bl_options = {'DEFAULT_CLOSED'} 181 | 182 | @classmethod 183 | def poll(cls, context): 184 | return RIGACAR_PT_mixin.is_car_rig_generated(context) 185 | 186 | def draw(self, context): 187 | self.display_ground_sensors_section(context) 188 | 189 | 190 | def menu_entries(menu, context): 191 | menu.layout.operator(car_rig.OBJECT_OT_armatureCarDeformationRig.bl_idname, text="Car (deformation rig)", icon='AUTO') 192 | 193 | 194 | classes = ( 195 | RIGACAR_PT_rigProperties, 196 | RIGACAR_PT_groundSensorsProperties, 197 | RIGACAR_PT_animationRigView, 198 | RIGACAR_PT_wheelsAnimationView, 199 | RIGACAR_PT_groundSensorsView, 200 | ) 201 | 202 | 203 | def register(): 204 | bpy.types.VIEW3D_MT_armature_add.append(menu_entries) 205 | for c in classes: 206 | bpy.utils.register_class(c) 207 | car_rig.register() 208 | bake_operators.register() 209 | 210 | 211 | def unregister(): 212 | bake_operators.unregister() 213 | car_rig.unregister() 214 | for c in classes: 215 | bpy.utils.unregister_class(c) 216 | bpy.types.VIEW3D_MT_armature_add.remove(menu_entries) 217 | 218 | 219 | if __name__ == "__main__": 220 | register() 221 | -------------------------------------------------------------------------------- /bake_operators.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 3 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 | # 20 | 21 | import bpy 22 | import mathutils 23 | import math 24 | import itertools 25 | import re 26 | 27 | 28 | def cursor(cursor_mode): 29 | def cursor_decorator(func): 30 | def wrapper(self, context, *args, **kwargs): 31 | context.window.cursor_modal_set(cursor_mode) 32 | try: 33 | return func(self, context, *args, **kwargs) 34 | finally: 35 | context.window.cursor_modal_restore() 36 | return wrapper 37 | return cursor_decorator 38 | 39 | 40 | def bone_name(prefix, position, side, index=0): 41 | if index == 0: 42 | return '%s.%s.%s' % (prefix, position, side) 43 | else: 44 | return '%s.%s.%s.%03d' % (prefix, position, side, index) 45 | 46 | 47 | def bone_range(bones, name_prefix, position, side): 48 | for index in itertools.count(): 49 | name = bone_name(name_prefix, position, side, index) 50 | if name in bones: 51 | yield bones[name] 52 | else: 53 | break 54 | 55 | 56 | def find_wheelbrake_bone(bones, position, side, index): 57 | other_side = 'R' if side == 'L' else 'L' 58 | name_prefix = 'WheelBrake' 59 | bone = bones.get(bone_name(name_prefix, position, side, index)) 60 | if bone: 61 | return bone 62 | bone = bones.get(bone_name(name_prefix, position, other_side, index)) 63 | if bone: 64 | return bone 65 | if index > 0: 66 | bone = bones.get(bone_name(name_prefix, position, side)) 67 | if bone: 68 | return bone 69 | bone = bones.get(bone_name(name_prefix, position, other_side)) 70 | if bone: 71 | return bone 72 | backward_compatible_bone_name = '%s Wheels' % ('Front' if position == 'Ft' else 'Back') 73 | return bones.get(backward_compatible_bone_name) 74 | 75 | 76 | def clear_property_animation(context, property_name, remove_keyframes=True): 77 | if remove_keyframes and context.object.animation_data and context.object.animation_data.action: 78 | fcurve_datapath = '["%s"]' % property_name 79 | action = context.object.animation_data.action 80 | fcurve = action.fcurves.find(fcurve_datapath) 81 | if fcurve is not None: 82 | action.fcurves.remove(fcurve) 83 | context.object[property_name] = .0 84 | 85 | 86 | def create_property_animation(context, property_name): 87 | action = context.object.animation_data.action 88 | fcurve_datapath = '["%s"]' % property_name 89 | return action.fcurves.new(fcurve_datapath, index=0, action_group='Wheels rotation') 90 | 91 | 92 | class FCurvesEvaluator(object): 93 | """Encapsulates a bunch of FCurves for vector animations.""" 94 | 95 | def __init__(self, fcurves, default_value): 96 | self.default_value = default_value 97 | self.fcurves = fcurves 98 | 99 | def evaluate(self, f): 100 | result = [] 101 | for fcurve, value in zip(self.fcurves, self.default_value): 102 | if fcurve is not None: 103 | result.append(fcurve.evaluate(f)) 104 | else: 105 | result.append(value) 106 | return result 107 | 108 | 109 | class VectorFCurvesEvaluator(object): 110 | 111 | def __init__(self, fcurves_evaluator): 112 | self.fcurves_evaluator = fcurves_evaluator 113 | 114 | def evaluate(self, f): 115 | return mathutils.Vector(self.fcurves_evaluator.evaluate(f)) 116 | 117 | 118 | class EulerToQuaternionFCurvesEvaluator(object): 119 | 120 | def __init__(self, fcurves_evaluator): 121 | self.fcurves_evaluator = fcurves_evaluator 122 | 123 | def evaluate(self, f): 124 | return mathutils.Euler(self.fcurves_evaluator.evaluate(f)).to_quaternion() 125 | 126 | 127 | class QuaternionFCurvesEvaluator(object): 128 | 129 | def __init__(self, fcurves_evaluator): 130 | self.fcurves_evaluator = fcurves_evaluator 131 | 132 | def evaluate(self, f): 133 | return mathutils.Quaternion(self.fcurves_evaluator.evaluate(f)) 134 | 135 | 136 | def fix_old_steering_rotation(rig_object): 137 | """ 138 | Fix armature generated with rigacar version < 6.0 139 | """ 140 | if rig_object.pose and rig_object.pose.bones: 141 | if 'MCH-Steering.rotation' in rig_object.pose.bones: 142 | rig_object.pose.bones['MCH-Steering.rotation'].rotation_mode = 'QUATERNION' 143 | 144 | 145 | class BakingOperator(object): 146 | frame_start: bpy.props.IntProperty(name='Start Frame', min=1) 147 | frame_end: bpy.props.IntProperty(name='End Frame', min=1) 148 | keyframe_tolerance: bpy.props.FloatProperty(name='Keyframe tolerance', min=0, default=.01) 149 | 150 | @classmethod 151 | def poll(cls, context): 152 | return ('Car Rig' in context.object.data and 153 | context.object.data['Car Rig'] and 154 | context.object.mode in ('POSE', 'OBJECT')) 155 | 156 | def invoke(self, context, event): 157 | if context.object.animation_data is None: 158 | context.object.animation_data_create() 159 | if context.object.animation_data.action is None: 160 | context.object.animation_data.action = bpy.data.actions.new("%sAction" % context.object.name) 161 | 162 | action = context.object.animation_data.action 163 | self.frame_start = int(action.frame_range[0]) 164 | self.frame_end = int(action.frame_range[1]) 165 | 166 | return context.window_manager.invoke_props_dialog(self) 167 | 168 | def draw(self, context): 169 | self.layout.use_property_split = True 170 | self.layout.use_property_decorate = False 171 | self.layout.prop(self, 'frame_start') 172 | self.layout.prop(self, 'frame_end') 173 | self.layout.prop(self, 'keyframe_tolerance') 174 | 175 | def _create_euler_evaluator(self, action, source_bone): 176 | fcurve_name = 'pose.bones["%s"].rotation_euler' % source_bone.name 177 | fc_root_rot = [action.fcurves.find(fcurve_name, index=i) for i in range(3)] 178 | return EulerToQuaternionFCurvesEvaluator(FCurvesEvaluator(fc_root_rot, default_value=(.0, .0, .0))) 179 | 180 | def _create_quaternion_evaluator(self, action, source_bone): 181 | fcurve_name = 'pose.bones["%s"].rotation_quaternion' % source_bone.name 182 | fc_root_rot = [action.fcurves.find(fcurve_name, index=i) for i in range(4)] 183 | return QuaternionFCurvesEvaluator(FCurvesEvaluator(fc_root_rot, default_value=(1.0, .0, .0, .0))) 184 | 185 | def _create_location_evaluator(self, action, source_bone): 186 | fcurve_name = 'pose.bones["%s"].location' % source_bone.name 187 | fc_root_loc = [action.fcurves.find(fcurve_name, index=i) for i in range(3)] 188 | return VectorFCurvesEvaluator(FCurvesEvaluator(fc_root_loc, default_value=(.0, .0, .0))) 189 | 190 | def _create_scale_evaluator(self, action, source_bone): 191 | fcurve_name = 'pose.bones["%s"].scale' % source_bone.name 192 | fc_root_loc = [action.fcurves.find(fcurve_name, index=i) for i in range(3)] 193 | return VectorFCurvesEvaluator(FCurvesEvaluator(fc_root_loc, default_value=(1.0, 1.0, 1.0))) 194 | 195 | def _bake_action(self, context, *source_bones): 196 | action = context.object.animation_data.action 197 | nla_tweak_mode = context.object.animation_data.use_tweak_mode if hasattr(context.object.animation_data, 'use_tweak_mode') else False 198 | 199 | # saving context 200 | selected_bones = [b for b in context.object.data.bones if b.select] 201 | mode = context.object.mode 202 | for b in selected_bones: 203 | b.select = False 204 | 205 | bpy.ops.object.mode_set(mode='OBJECT') 206 | source_bones_matrix_basis = [] 207 | for source_bone in source_bones: 208 | source_bones_matrix_basis.append(context.object.pose.bones[source_bone.name].matrix_basis.copy()) 209 | source_bone.select = True 210 | 211 | # Blender 2.81 : Another hack for another bug in the bake operator 212 | # removing from the selection objects which are not the current one 213 | for obj in context.selected_objects: 214 | if obj is not context.object: 215 | obj.select_set(state=False) 216 | 217 | bpy.ops.nla.bake(frame_start=self.frame_start, frame_end=self.frame_end, only_selected=True, bake_types={'POSE'}, visual_keying=True) 218 | baked_action = context.object.animation_data.action 219 | 220 | # restoring context 221 | for source_bone, matrix_basis in zip(source_bones, source_bones_matrix_basis): 222 | context.object.pose.bones[source_bone.name].matrix_basis = matrix_basis 223 | source_bone.select = False 224 | for b in selected_bones: 225 | b.select = True 226 | 227 | bpy.ops.object.mode_set(mode=mode) 228 | 229 | if nla_tweak_mode: 230 | context.object.animation_data.use_tweak_mode = nla_tweak_mode 231 | else: 232 | context.object.animation_data.action = action 233 | 234 | return baked_action 235 | 236 | 237 | class ANIM_OT_carWheelsRotationBake(bpy.types.Operator, BakingOperator): 238 | bl_idname = 'anim.car_wheels_rotation_bake' 239 | bl_label = 'Bake wheels rotation' 240 | bl_description = 'Automatically generates wheels animation based on Root bone animation.' 241 | bl_options = {'REGISTER', 'UNDO'} 242 | 243 | def execute(self, context): 244 | context.object['wheels_on_y_axis'] = False 245 | self._bake_wheels_rotation(context) 246 | return {'FINISHED'} 247 | 248 | @cursor('WAIT') 249 | def _bake_wheels_rotation(self, context): 250 | bones = context.object.data.bones 251 | 252 | wheel_bones = [] 253 | brake_bones = [] 254 | for position, side in itertools.product(('Ft', 'Bk'), ('L', 'R')): 255 | for index, wheel_bone in enumerate(bone_range(bones, 'MCH-Wheel.rotation', position, side)): 256 | wheel_bones.append(wheel_bone) 257 | brake_bones.append(find_wheelbrake_bone(bones, position, side, index) or wheel_bone) 258 | 259 | for property_name in map(lambda wheel_bone: wheel_bone.name.replace('MCH-', ''), wheel_bones): 260 | clear_property_animation(context, property_name) 261 | 262 | bones = set(wheel_bones + brake_bones) 263 | baked_action = self._bake_action(context, *bones) 264 | 265 | try: 266 | for wheel_bone, brake_bone in zip(wheel_bones, brake_bones): 267 | self._bake_wheel_rotation(context, baked_action, wheel_bone, brake_bone) 268 | finally: 269 | bpy.data.actions.remove(baked_action) 270 | 271 | def _evaluate_distance_per_frame(self, action, bone, brake_bone): 272 | loc_evaluator = self._create_location_evaluator(action, bone) 273 | rot_evaluator = self._create_euler_evaluator(action, bone) 274 | brake_evaluator = self._create_scale_evaluator(action, brake_bone) 275 | 276 | radius = bone.length if bone.length > .0 else 1.0 277 | bone_init_vector = (bone.head_local - bone.tail_local).normalized() 278 | prev_pos = loc_evaluator.evaluate(self.frame_start) 279 | prev_speed = 0 280 | distance = 0 281 | yield self.frame_start, distance 282 | for f in range(self.frame_start + 1, self.frame_end): 283 | pos = loc_evaluator.evaluate(f) 284 | speed_vector = pos - prev_pos 285 | speed_vector *= 2 * brake_evaluator.evaluate(f).y - 1 286 | rotation_quaternion = rot_evaluator.evaluate(f) 287 | bone_orientation = rotation_quaternion @ bone_init_vector 288 | speed = math.copysign(speed_vector.magnitude, bone_orientation.dot(speed_vector)) 289 | speed /= radius 290 | drop_keyframe = False 291 | if speed == .0: 292 | drop_keyframe = prev_speed == speed 293 | elif prev_speed != .0: 294 | drop_keyframe = abs(1 - prev_speed / speed) < self.keyframe_tolerance / 10 295 | if not drop_keyframe: 296 | prev_speed = speed 297 | yield f - 1, distance 298 | distance += speed 299 | prev_pos = pos 300 | yield self.frame_end, distance 301 | 302 | def _bake_wheel_rotation(self, context, baked_action, bone, brake_bone): 303 | fc_rot = create_property_animation(context, bone.name.replace('MCH-', '')) 304 | 305 | for f, distance in self._evaluate_distance_per_frame(baked_action, bone, brake_bone): 306 | kf = fc_rot.keyframe_points.insert(f, distance) 307 | kf.interpolation = 'LINEAR' 308 | kf.type = 'JITTER' 309 | 310 | 311 | class ANIM_OT_carSteeringBake(bpy.types.Operator, BakingOperator): 312 | bl_idname = 'anim.car_steering_bake' 313 | bl_label = 'Bake car steering' 314 | bl_description = 'Automatically generates steering animation based on Root bone animation.' 315 | bl_options = {'REGISTER', 'UNDO'} 316 | 317 | rotation_factor: bpy.props.FloatProperty(name='Rotation factor', min=.1, default=1) 318 | 319 | def draw(self, context): 320 | self.layout.use_property_split = True 321 | self.layout.use_property_decorate = False 322 | self.layout.prop(self, 'frame_start') 323 | self.layout.prop(self, 'frame_end') 324 | self.layout.prop(self, 'rotation_factor') 325 | self.layout.prop(self, 'keyframe_tolerance') 326 | 327 | def execute(self, context): 328 | if self.frame_end > self.frame_start: 329 | if 'Steering' in context.object.data.bones and 'MCH-Steering.rotation' in context.object.data.bones: 330 | steering = context.object.data.bones['Steering'] 331 | mch_steering_rotation = context.object.data.bones['MCH-Steering.rotation'] 332 | bone_offset = abs(steering.head_local.y - mch_steering_rotation.head_local.y) 333 | self._bake_steering_rotation(context, bone_offset, mch_steering_rotation) 334 | return {'FINISHED'} 335 | 336 | def _evaluate_rotation_per_frame(self, action, bone_offset, bone): 337 | loc_evaluator = self._create_location_evaluator(action, bone) 338 | rot_evaluator = self._create_quaternion_evaluator(action, bone) 339 | 340 | distance_threshold = pow(bone_offset * max(self.keyframe_tolerance, .001), 2) 341 | steering_threshold = bone_offset * self.keyframe_tolerance * .1 342 | bone_direction_vector = (bone.head_local - bone.tail_local).normalized() 343 | bone_normal_vector = mathutils.Vector((1, 0, 0)) 344 | 345 | current_pos = loc_evaluator.evaluate(self.frame_start) 346 | previous_steering_position = None 347 | for f in range(self.frame_start, self.frame_end - 1): 348 | next_pos = loc_evaluator.evaluate(f + 1) 349 | steering_direction_vector = next_pos - current_pos 350 | 351 | if steering_direction_vector.length_squared < distance_threshold: 352 | continue 353 | 354 | rotation_quaternion = rot_evaluator.evaluate(f) 355 | world_space_bone_direction_vector = rotation_quaternion @ bone_direction_vector 356 | world_space_bone_normal_vector = rotation_quaternion @ bone_normal_vector 357 | 358 | projected_steering_direction = steering_direction_vector.dot(world_space_bone_direction_vector) 359 | if projected_steering_direction == 0: 360 | continue 361 | 362 | length_ratio = bone_offset * self.rotation_factor / projected_steering_direction 363 | steering_direction_vector *= length_ratio 364 | 365 | steering_position = mathutils.geometry.distance_point_to_plane(steering_direction_vector, world_space_bone_direction_vector, world_space_bone_normal_vector) 366 | 367 | if previous_steering_position is not None \ 368 | and abs(steering_position - previous_steering_position) < steering_threshold: 369 | continue 370 | 371 | yield f, steering_position 372 | current_pos = next_pos 373 | previous_steering_position = steering_position 374 | 375 | @cursor('WAIT') 376 | def _bake_steering_rotation(self, context, bone_offset, bone): 377 | clear_property_animation(context, 'Steering.rotation') 378 | fix_old_steering_rotation(context.object) 379 | fc_rot = create_property_animation(context, 'Steering.rotation') 380 | action = self._bake_action(context, bone) 381 | 382 | try: 383 | for f, steering_pos in self._evaluate_rotation_per_frame(action, bone_offset, bone): 384 | kf = fc_rot.keyframe_points.insert(f, steering_pos) 385 | kf.type = 'JITTER' 386 | kf.interpolation = 'LINEAR' 387 | finally: 388 | bpy.data.actions.remove(action) 389 | 390 | 391 | class ANIM_OT_carClearSteeringWheelsRotation(bpy.types.Operator): 392 | bl_idname = "anim.car_clear_steering_wheels_rotation" 393 | bl_label = "Clear baked animation" 394 | bl_description = "Clear generated rotation for steering and wheels" 395 | bl_options = {'REGISTER', 'UNDO'} 396 | 397 | clear_steering: bpy.props.BoolProperty(name="Steering", description="Clear generated animation for steering", default=True) 398 | clear_wheels: bpy.props.BoolProperty(name="Wheels", description="Clear generated animation for wheels", default=True) 399 | 400 | def draw(self, context): 401 | self.layout.use_property_decorate = False 402 | self.layout.label(text='Clear generated keyframes for') 403 | self.layout.prop(self, property='clear_steering') 404 | self.layout.prop(self, property='clear_wheels') 405 | 406 | @classmethod 407 | def poll(cls, context): 408 | return context.object is not None and context.object.data is not None and context.object.data.get('Car Rig') 409 | 410 | def execute(self, context): 411 | re_wheel_propname = re.compile(r'^Wheel\.rotation\.(Ft|Bk)\.[LR](\.\d+)?$') 412 | for prop in context.object.keys(): 413 | if prop == 'Steering.rotation': 414 | clear_property_animation(context, prop, remove_keyframes=self.clear_steering) 415 | elif re_wheel_propname.match(prop): 416 | clear_property_animation(context, prop, remove_keyframes=self.clear_wheels) 417 | # this is a hack to force Blender to take into account the modification 418 | # of the properties by changing the object mode. 419 | # Don't know yet if it is specific to blender 2.80 420 | mode = context.object.mode 421 | bpy.ops.object.mode_set(mode='OBJECT' if mode == 'POSE' else 'POSE') 422 | bpy.ops.object.mode_set(mode=mode) 423 | return {'FINISHED'} 424 | 425 | 426 | def register(): 427 | bpy.utils.register_class(ANIM_OT_carWheelsRotationBake) 428 | bpy.utils.register_class(ANIM_OT_carSteeringBake) 429 | bpy.utils.register_class(ANIM_OT_carClearSteeringWheelsRotation) 430 | 431 | 432 | def unregister(): 433 | bpy.utils.unregister_class(ANIM_OT_carClearSteeringWheelsRotation) 434 | bpy.utils.unregister_class(ANIM_OT_carSteeringBake) 435 | bpy.utils.unregister_class(ANIM_OT_carWheelsRotationBake) 436 | 437 | 438 | if __name__ == "__main__": 439 | register() 440 | -------------------------------------------------------------------------------- /car_rig.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 3 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 | # 20 | 21 | import bpy 22 | import math 23 | import bpy_extras 24 | import mathutils 25 | import re 26 | from math import inf 27 | from rna_prop_ui import rna_idprop_ui_create 28 | 29 | CUSTOM_SHAPE_LAYER = 13 30 | MCH_BONE_EXTENSION_LAYER = 14 31 | DEF_BONE_LAYER = 15 32 | MCH_BONE_LAYER = 31 33 | 34 | 35 | def deselect_edit_bones(ob): 36 | for b in ob.data.edit_bones: 37 | b.select = False 38 | b.select_head = False 39 | b.select_tail = False 40 | 41 | 42 | def create_constraint_influence_driver(ob, cns, driver_data_path, base_influence=1.0): 43 | fcurve = cns.driver_add('influence') 44 | drv = fcurve.driver 45 | drv.type = 'AVERAGE' 46 | var = drv.variables.new() 47 | var.name = 'influence' 48 | var.type = 'SINGLE_PROP' 49 | 50 | targ = var.targets[0] 51 | targ.id_type = 'OBJECT' 52 | targ.id = ob 53 | targ.data_path = driver_data_path 54 | 55 | if base_influence != 1.0: 56 | fmod = fcurve.modifiers[0] 57 | fmod.mode = 'POLYNOMIAL' 58 | fmod.poly_order = 1 59 | fmod.coefficients = (0, base_influence) 60 | 61 | 62 | def create_rotation_euler_x_driver(ob, bone, driver_data_path): 63 | fcurve = bone.driver_add('rotation_euler', 0) 64 | drv = fcurve.driver 65 | drv.type = 'AVERAGE' 66 | var = drv.variables.new() 67 | var.name = 'rotationAngle' 68 | var.type = 'SINGLE_PROP' 69 | 70 | targ = var.targets[0] 71 | targ.id_type = 'OBJECT' 72 | targ.id = ob 73 | targ.data_path = driver_data_path 74 | 75 | 76 | def create_translation_x_driver(ob, bone, driver_data_path): 77 | fcurve = bone.driver_add('location', 0) 78 | drv = fcurve.driver 79 | drv.type = 'AVERAGE' 80 | var = drv.variables.new() 81 | var.name = 'rotationAngle' 82 | var.type = 'SINGLE_PROP' 83 | 84 | targ = var.targets[0] 85 | targ.id_type = 'OBJECT' 86 | targ.id = ob 87 | targ.data_path = driver_data_path 88 | 89 | 90 | def create_bone_group(pose, group_name, color_set, bone_names): 91 | group = pose.bone_groups.new(name=group_name) 92 | group.color_set = color_set 93 | for bone_name in bone_names: 94 | bone = pose.bones.get(bone_name) 95 | if bone is not None: 96 | bone.bone_group = group 97 | 98 | 99 | def name_range(prefix, nb=1000): 100 | if nb > 0: 101 | yield prefix 102 | for i in range(1, nb): 103 | yield '%s.%03d' % (prefix, i) 104 | 105 | 106 | def get_widget(name): 107 | widget = bpy.data.objects.get(name) 108 | if widget is None: 109 | from . import widgets 110 | widgets.create() 111 | widget = bpy.data.objects.get(name) 112 | return widget 113 | 114 | 115 | def define_custom_property(target, name, value, description=None, overridable=True): 116 | rna_idprop_ui_create(target, name, default=value, description=description, overridable=overridable, min=-inf, max=inf) 117 | 118 | 119 | def dispatch_bones_to_armature_layers(ob): 120 | re_mch_bone = re.compile(r'^MCH-Wheel(Brake)?\.(Ft|Bk)\.[LR](\.\d+)?$') 121 | default_visible_layers = [False] * 32 122 | 123 | for b in ob.data.bones: 124 | layers = [False] * 32 125 | if b.name.startswith('DEF-'): 126 | layers[DEF_BONE_LAYER] = True 127 | elif b.name.startswith('MCH-'): 128 | layers[MCH_BONE_LAYER] = True 129 | if b.name in ('MCH-Body', 'MCH-Steering') or re_mch_bone.match(b.name): 130 | layers[MCH_BONE_EXTENSION_LAYER] = True 131 | else: 132 | layer_num = ob.pose.bones[b.name].bone_group_index 133 | layers[layer_num] = True 134 | default_visible_layers[layer_num] = True 135 | b.layers = layers 136 | 137 | ob.data.layers = default_visible_layers 138 | 139 | shape_bone_layers = [False] * 32 140 | shape_bone_layers[CUSTOM_SHAPE_LAYER] = True 141 | for b in ob.pose.bones: 142 | if b.custom_shape: 143 | if b.custom_shape_transform: 144 | ob.pose.bones[b.custom_shape_transform.name].custom_shape = b.custom_shape 145 | ob.data.bones[b.custom_shape_transform.name].layers = shape_bone_layers 146 | else: 147 | ob.data.bones[b.name].layers[CUSTOM_SHAPE_LAYER] = True 148 | 149 | 150 | class NameSuffix(object): 151 | 152 | def __init__(self, position, side, index=0): 153 | self.position = position 154 | self.side = side 155 | self.index = index 156 | if index == 0: 157 | self.value = '%s.%s' % (position, side) 158 | else: 159 | self.value = '%s.%s.%03d' % (position, side, index) 160 | 161 | def name(self, base_name=None): 162 | return '%s.%s' % (base_name, self.value) if base_name else self.value 163 | 164 | @property 165 | def is_front(self): 166 | return self.position == 'Ft' 167 | 168 | @property 169 | def is_left(self): 170 | return self.side == 'L' 171 | 172 | @property 173 | def is_first(self): 174 | return self.index == 0 175 | 176 | def __str__(self): 177 | return self.value 178 | 179 | 180 | class BoundingBox(object): 181 | 182 | def __init__(self, armature, bone_name): 183 | objs = [o for o in armature.children if o.parent_bone == bone_name] 184 | bone = armature.data.bones[bone_name] 185 | self.__center = bone.head.copy() 186 | if not objs: 187 | self.__xyz = [bone.head.x - bone.length / 2, bone.head.x + bone.length / 2, bone.head.y - bone.length, bone.head.y + bone.length, .0, bone.head.z * 2] 188 | else: 189 | self.__xyz = [inf, -inf, inf, -inf, inf, -inf] 190 | self.__compute(mathutils.Matrix(), *objs) 191 | 192 | def __compute(self, pmatrix, *objs): 193 | for obj in objs: 194 | omatrix = pmatrix @ obj.matrix_world 195 | if obj.instance_type == 'COLLECTION': 196 | self.__compute(omatrix, *obj.instance_collection.all_objects) 197 | elif obj.bound_box: 198 | for p in obj.bound_box: 199 | world_p = omatrix @ mathutils.Vector(p) 200 | self.__xyz[0] = min(world_p.x, self.__xyz[0]) 201 | self.__xyz[1] = max(world_p.x, self.__xyz[1]) 202 | self.__xyz[2] = min(world_p.y, self.__xyz[2]) 203 | self.__xyz[3] = max(world_p.y, self.__xyz[3]) 204 | self.__xyz[4] = min(world_p.z, self.__xyz[4]) 205 | self.__xyz[5] = max(world_p.z, self.__xyz[5]) 206 | self.__compute(pmatrix, *obj.children) 207 | 208 | @property 209 | def center(self): 210 | return self.__center 211 | 212 | @property 213 | def box_center(self): 214 | return mathutils.Vector((self.max_x + self.min_x, self.max_y + self.min_y, self.max_z + self.min_z)) / 2 215 | 216 | @property 217 | def min_x(self): 218 | return self.__xyz[0] 219 | 220 | @property 221 | def max_x(self): 222 | return self.__xyz[1] 223 | 224 | @property 225 | def min_y(self): 226 | return self.__xyz[2] 227 | 228 | @property 229 | def max_y(self): 230 | return self.__xyz[3] 231 | 232 | @property 233 | def min_z(self): 234 | return self.__xyz[4] 235 | 236 | @property 237 | def max_z(self): 238 | return self.__xyz[5] 239 | 240 | @property 241 | def width(self): 242 | return abs(self.__xyz[0] - self.__xyz[1]) 243 | 244 | @property 245 | def length(self): 246 | return abs(self.__xyz[2] - self.__xyz[3]) 247 | 248 | @property 249 | def height(self): 250 | return abs(self.__xyz[4] - self.__xyz[5]) 251 | 252 | 253 | class WheelBoundingBox(BoundingBox): 254 | 255 | def __init__(self, armature, bone_name, side): 256 | super().__init__(armature, bone_name) 257 | self.side = side 258 | 259 | def compute_outer_x(self, delta=0): 260 | if self.side == 'L': 261 | return self.max_x + delta 262 | else: 263 | return self.min_x - delta 264 | 265 | 266 | class WheelsDimension(object): 267 | 268 | def __init__(self, armature, position, side_position, default): 269 | self.default = default 270 | self.position = position 271 | self.side_position = side_position 272 | self.wheels = [] 273 | wheel_bones = (armature.data.edit_bones.get(name) for name in name_range('DEF-Wheel.%s.%s' % (self.position, self.side_position))) 274 | for wheel_bone in wheel_bones: 275 | if wheel_bone is None: 276 | break 277 | self.wheels.append(WheelBoundingBox(armature, wheel_bone.name, side_position)) 278 | 279 | def name_suffixes(self): 280 | for i in range(len(self.wheels)): 281 | yield NameSuffix(self.position, self.side_position, i) 282 | 283 | def names(self, base_name=None): 284 | for name_suffix in name_range('%s.%s' % (self.position, self.side_position), self.nb): 285 | yield '%s.%s' % (base_name, name_suffix) if base_name else name_suffix 286 | 287 | def name(self, base_name=None): 288 | suffix = '%s.%s' % (self.position, self.side_position) 289 | return '%s.%s' % (base_name, suffix) if base_name else suffix 290 | 291 | @property 292 | def nb(self): 293 | return len(self.wheels) 294 | 295 | @property 296 | def min_position(self): 297 | if self.nb == 0: 298 | return self.default 299 | return min(self.wheels, key=lambda w: w.center.y).center 300 | 301 | @property 302 | def max_position(self): 303 | if self.nb == 0: 304 | return self.default 305 | return max(self.wheels, key=lambda w: w.center.y).center 306 | 307 | @property 308 | def medium_position(self): 309 | if self.nb == 0: 310 | return self.min_position 311 | return (self.min_position + self.max_position) / 2.0 312 | 313 | def compute_outer_x(self, delta=0): 314 | if self.side_position == 'L': 315 | x = max(map(lambda w: w.max_x, self.wheels)) 316 | x += delta 317 | else: 318 | x = min(map(lambda w: w.min_x, self.wheels)) 319 | x -= delta 320 | return x 321 | 322 | @property 323 | def outer_z(self): 324 | return max(map(lambda w: w.max_z, self.wheels)) 325 | 326 | @property 327 | def outer_front(self): 328 | return min(map(lambda w: w.min_y, self.wheels)) 329 | 330 | @property 331 | def outer_back(self): 332 | return max(map(lambda w: w.max_y, self.wheels)) 333 | 334 | 335 | class CarDimension(object): 336 | 337 | def __init__(self, armature): 338 | body = armature.data.edit_bones['DEF-Body'] 339 | self.bb_body = BoundingBox(armature, 'DEF-Body') 340 | self.wheels_front_left = WheelsDimension(armature, 'Ft', 'L', default=body.head) 341 | self.wheels_front_right = WheelsDimension(armature, 'Ft', 'R', default=body.head) 342 | self.wheels_back_left = WheelsDimension(armature, 'Bk', 'L', default=body.tail) 343 | self.wheels_back_right = WheelsDimension(armature, 'Bk', 'R', default=body.tail) 344 | 345 | @property 346 | def body_center(self): 347 | return self.bb_body.center 348 | 349 | @property 350 | def car_center(self): 351 | center = self.bb_body.box_center.copy() 352 | center.y = (self.max_y + self.min_y) / 2 353 | return center 354 | 355 | @property 356 | def width(self): 357 | return max([self.bb_body.width] + [abs(w.compute_outer_x() - self.bb_body.center.x) * 2 for w in self.wheels_dimensions]) 358 | 359 | @property 360 | def height(self): 361 | return max([self.bb_body.max_z] + [w.outer_z for w in self.wheels_dimensions]) 362 | 363 | @property 364 | def length(self): 365 | return abs(self.max_y - self.min_y) 366 | 367 | @property 368 | def min_y(self): 369 | return min([self.bb_body.min_y] + [w.outer_front for w in self.wheels_dimensions]) 370 | 371 | @property 372 | def max_y(self): 373 | return max([self.bb_body.max_y] + [w.outer_back for w in self.wheels_dimensions]) 374 | 375 | @property 376 | def wheels_front_position(self): 377 | position = (self.wheels_front_left.min_position + self.wheels_front_right.min_position) / 2 378 | position.x = self.bb_body.center.x 379 | return position 380 | 381 | @property 382 | def wheels_back_position(self): 383 | position = (self.wheels_back_left.max_position + self.wheels_back_right.max_position) / 2 384 | position.x = self.bb_body.center.x 385 | return position 386 | 387 | @property 388 | def suspension_front_position(self): 389 | position = (self.wheels_front_left.medium_position + self.wheels_front_right.medium_position) / 2 390 | position.x = self.bb_body.center.x 391 | return position 392 | 393 | @property 394 | def suspension_back_position(self): 395 | position = (self.wheels_back_left.medium_position + self.wheels_back_right.medium_position) / 2 396 | position.x = self.bb_body.center.x 397 | return position 398 | 399 | @property 400 | def has_wheels(self): 401 | return self.has_front_wheels or self.has_back_wheels 402 | 403 | @property 404 | def has_front_wheels(self): 405 | return self.nb_front_wheels > 0 406 | 407 | @property 408 | def has_back_wheels(self): 409 | return self.nb_back_wheels > 0 410 | 411 | @property 412 | def nb_front_wheels(self): 413 | return max(self.wheels_front_left.nb, self.wheels_front_right.nb) 414 | 415 | @property 416 | def nb_back_wheels(self): 417 | return max(self.wheels_back_left.nb, self.wheels_back_right.nb) 418 | 419 | @property 420 | def wheels_dimensions(self): 421 | return filter(lambda w: w.nb, (self.wheels_front_left, self.wheels_front_right, self.wheels_back_left, self.wheels_back_right)) 422 | 423 | 424 | def create_wheel_brake_bone(wheel_brake, parent_bone, wheel_bone): 425 | wheel_brake.use_deform = False 426 | wheel_brake.parent = parent_bone 427 | wheel_brake.head = wheel_bone.head 428 | wheel_brake.tail = wheel_bone.tail 429 | 430 | 431 | def generate_constraint_on_wheel_brake_bone(wheel_brake_pose_bone, wheel_pose_bone): 432 | wheel_brake_pose_bone.lock_location = (True, True, True) 433 | wheel_brake_pose_bone.lock_rotation = (True, True, True) 434 | wheel_brake_pose_bone.lock_rotation_w = True 435 | wheel_brake_pose_bone.lock_scale = (True, False, False) 436 | wheel_brake_pose_bone.custom_shape = get_widget('WGT-CarRig.WheelBrake') 437 | wheel_brake_pose_bone.bone.show_wire = True 438 | wheel_brake_pose_bone.bone_group = wheel_pose_bone.bone_group 439 | wheel_brake_pose_bone.bone.layers = wheel_pose_bone.bone.layers 440 | 441 | cns = wheel_brake_pose_bone.constraints.new('LIMIT_SCALE') 442 | cns.name = 'Brakes' 443 | cns.use_transform_limit = True 444 | cns.owner_space = 'LOCAL' 445 | cns.use_max_x = True 446 | cns.use_min_x = True 447 | cns.min_x = 1.0 448 | cns.max_x = 1.0 449 | cns.use_max_y = True 450 | cns.use_min_y = True 451 | cns.min_y = .5 452 | cns.max_y = 1.0 453 | cns.use_max_z = True 454 | cns.use_min_z = True 455 | cns.min_z = .5 456 | cns.max_z = 1.0 457 | 458 | 459 | class ArmatureGenerator(object): 460 | 461 | def __init__(self, ob): 462 | self.ob = ob 463 | 464 | def generate(self, scene, adjust_origin): 465 | define_custom_property(self.ob, 466 | name='wheels_on_y_axis', 467 | value=False, 468 | description="Activate wheels rotation when moving the root bone along the Y axis") 469 | define_custom_property(self.ob, 470 | name='suspension_factor', 471 | value=.5, 472 | description="Influence of the dampers over the pitch of the body") 473 | define_custom_property(self.ob, 474 | name='suspension_rolling_factor', 475 | value=.5, 476 | description="Influence of the dampers over the roll of the body") 477 | 478 | location = self.ob.location.copy() 479 | self.ob.location = (0, 0, 0) 480 | try: 481 | bpy.ops.object.mode_set(mode='EDIT') 482 | self.dimension = CarDimension(self.ob) 483 | self.generate_animation_rig() 484 | self.ob.data['Car Rig'] = True 485 | deselect_edit_bones(self.ob) 486 | 487 | if adjust_origin: 488 | bpy.ops.object.mode_set(mode='OBJECT') 489 | self.set_origin(scene) 490 | 491 | bpy.ops.object.mode_set(mode='POSE') 492 | self.generate_constraints_on_rig() 493 | self.ob.display_type = 'WIRE' 494 | 495 | self.generate_bone_groups() 496 | dispatch_bones_to_armature_layers(self.ob) 497 | finally: 498 | self.ob.location += location 499 | 500 | def generate_animation_rig(self): 501 | amt = self.ob.data 502 | 503 | body = amt.edit_bones['DEF-Body'] 504 | root = amt.edit_bones.new('Root') 505 | if self.dimension.has_back_wheels: 506 | root.head = self.dimension.wheels_back_position 507 | elif self.dimension.has_front_wheels: 508 | root.head = self.dimension.wheels_front_position 509 | else: 510 | root.head = self.dimension.body_center 511 | root.head.z = 0 512 | root.tail = root.head 513 | root.tail.y += max(self.dimension.length / 1.95, self.dimension.width * 1.1) 514 | root.use_deform = False 515 | 516 | shape_root = amt.edit_bones.new('SHP-Root') 517 | shape_root.head = self.dimension.car_center 518 | shape_root.head.z = 0.01 519 | shape_root.tail = shape_root.head 520 | shape_root.tail.y += root.length 521 | shape_root.use_deform = False 522 | shape_root.parent = root 523 | 524 | drift = amt.edit_bones.new('Drift') 525 | drift.head = self.dimension.wheels_front_position 526 | drift.head.z = self.dimension.wheels_back_position.z 527 | drift.tail = drift.head 528 | drift.tail.y -= self.dimension.width * .95 529 | drift.roll = math.pi 530 | drift.use_deform = False 531 | drift.parent = root 532 | base_bone_parent = drift 533 | 534 | if self.dimension.has_front_wheels: 535 | groundsensor_axle_front = amt.edit_bones.new('GroundSensor.Axle.Ft') 536 | groundsensor_axle_front.head = self.dimension.wheels_front_position 537 | groundsensor_axle_front.tail = groundsensor_axle_front.head 538 | groundsensor_axle_front.tail.y += self.dimension.length / 16 539 | groundsensor_axle_front.parent = root 540 | 541 | shp_groundsensor_axle_front = amt.edit_bones.new('SHP-GroundSensor.Axle.Ft') 542 | shp_groundsensor_axle_front.head = groundsensor_axle_front.head 543 | shp_groundsensor_axle_front.tail = groundsensor_axle_front.tail 544 | shp_groundsensor_axle_front.head.z = shp_groundsensor_axle_front.tail.z = 0.001 545 | shp_groundsensor_axle_front.parent = groundsensor_axle_front 546 | 547 | mch_root_axle_front = amt.edit_bones.new('MCH-Root.Axle.Ft') 548 | mch_root_axle_front.head = self.dimension.wheels_front_position 549 | mch_root_axle_front.head.z = 0.001 550 | mch_root_axle_front.tail = mch_root_axle_front.head 551 | mch_root_axle_front.tail.y += self.dimension.length / 6 552 | mch_root_axle_front.parent = groundsensor_axle_front 553 | if not self.dimension.has_back_wheels: 554 | drift.parent = mch_root_axle_front 555 | 556 | if self.dimension.has_back_wheels: 557 | groundsensor_axle_back = amt.edit_bones.new('GroundSensor.Axle.Bk') 558 | groundsensor_axle_back.head = self.dimension.wheels_back_position 559 | groundsensor_axle_back.tail = groundsensor_axle_back.head 560 | groundsensor_axle_back.tail.y += self.dimension.length / 16 561 | groundsensor_axle_back.parent = drift 562 | 563 | shp_groundsensor_axle_back = amt.edit_bones.new('SHP-GroundSensor.Axle.Bk') 564 | shp_groundsensor_axle_back.head = groundsensor_axle_back.head 565 | shp_groundsensor_axle_back.tail = groundsensor_axle_back.tail 566 | shp_groundsensor_axle_back.head.z = shp_groundsensor_axle_back.tail.z = 0.001 567 | shp_groundsensor_axle_back.parent = groundsensor_axle_back 568 | 569 | mch_root_axle_back = amt.edit_bones.new('MCH-Root.Axle.Bk') 570 | mch_root_axle_back.head = self.dimension.wheels_back_position 571 | mch_root_axle_back.head.z = 0 572 | mch_root_axle_back.tail = mch_root_axle_back.head 573 | mch_root_axle_back.tail.y += self.dimension.length / 6 574 | mch_root_axle_back.parent = groundsensor_axle_back 575 | base_bone_parent = mch_root_axle_back 576 | 577 | shape_drift = amt.edit_bones.new('SHP-Drift') 578 | shape_drift.head = self.dimension.body_center 579 | shape_drift.head.y = self.dimension.max_y + drift.length * .2 580 | shape_drift.head.z = self.dimension.wheels_back_position.z 581 | shape_drift.tail = shape_drift.head 582 | shape_drift.tail.y += drift.length 583 | shape_drift.use_deform = False 584 | shape_drift.parent = base_bone_parent 585 | 586 | for wheel_dimension in self.dimension.wheels_dimensions: 587 | for name_suffix, wheel_bounding_box in zip(wheel_dimension.name_suffixes(), wheel_dimension.wheels): 588 | self.generate_animation_wheel_bones(name_suffix, wheel_bounding_box, base_bone_parent) 589 | self.generate_wheel_damper(wheel_dimension, base_bone_parent) 590 | 591 | if self.dimension.has_front_wheels: 592 | wheel_ft_r = amt.edit_bones.get('DEF-Wheel.Ft.R') 593 | wheelFtL = amt.edit_bones.get('DEF-Wheel.Ft.L') 594 | 595 | axis_ft = amt.edit_bones.new('MCH-Axis.Ft') 596 | axis_ft.head = wheel_ft_r.head 597 | axis_ft.tail = wheelFtL.head 598 | axis_ft.use_deform = False 599 | axis_ft.parent = base_bone_parent 600 | 601 | mch_steering = amt.edit_bones.new('MCH-Steering') 602 | mch_steering.head = self.dimension.wheels_front_position 603 | mch_steering.tail = self.dimension.wheels_front_position 604 | mch_steering.tail.y += self.dimension.width / 2 605 | mch_steering.use_deform = False 606 | mch_steering.parent = groundsensor_axle_front if groundsensor_axle_front else root 607 | 608 | steering_rotation = amt.edit_bones.new('MCH-Steering.rotation') 609 | steering_rotation.head = mch_steering.head 610 | steering_rotation.tail = mch_steering.tail 611 | steering_rotation.tail.y += 1 612 | steering_rotation.use_deform = False 613 | 614 | steering = amt.edit_bones.new('Steering') 615 | steering.head = steering_rotation.head 616 | steering.head.y = self.dimension.min_y - 4 * wheelFtL.length 617 | steering.tail = steering.head 618 | steering.tail.y -= self.dimension.width / 2 619 | steering.use_deform = False 620 | steering.parent = steering_rotation 621 | 622 | if self.dimension.has_back_wheels: 623 | wheel_bk_r = amt.edit_bones.get('DEF-Wheel.Bk.R') 624 | wheel_bk_l = amt.edit_bones.get('DEF-Wheel.Bk.L') 625 | 626 | axisBk = amt.edit_bones.new('MCH-Axis.Bk') 627 | axisBk.head = wheel_bk_r.head 628 | axisBk.tail = wheel_bk_l.head 629 | axisBk.use_deform = False 630 | axisBk.parent = base_bone_parent 631 | 632 | suspension_bk = amt.edit_bones.new('MCH-Suspension.Bk') 633 | suspension_bk.head = self.dimension.suspension_back_position 634 | suspension_bk.tail = self.dimension.suspension_back_position 635 | suspension_bk.tail.y += 2 636 | suspension_bk.use_deform = False 637 | suspension_bk.parent = base_bone_parent 638 | 639 | suspension_ft = amt.edit_bones.new('MCH-Suspension.Ft') 640 | suspension_ft.head = self.dimension.suspension_front_position 641 | align_vector = suspension_bk.head - suspension_ft.head 642 | align_vector.magnitude = 2 643 | suspension_ft.tail = self.dimension.suspension_front_position + align_vector 644 | suspension_ft.use_deform = False 645 | suspension_ft.parent = base_bone_parent 646 | 647 | axis = amt.edit_bones.new('MCH-Axis') 648 | axis.head = suspension_ft.head 649 | axis.tail = suspension_bk.head 650 | axis.use_deform = False 651 | axis.parent = suspension_ft 652 | 653 | mch_body = amt.edit_bones.new('MCH-Body') 654 | mch_body.head = body.head 655 | mch_body.tail = body.tail 656 | mch_body.tail.y += 1 657 | mch_body.use_deform = False 658 | mch_body.parent = axis 659 | 660 | suspension = amt.edit_bones.new('Suspension') 661 | suspension.head = self.dimension.body_center 662 | suspension.head.z = self.dimension.height + self.dimension.width * .25 663 | suspension.tail = suspension.head 664 | suspension.tail.y += root.length * .5 665 | suspension.use_deform = False 666 | suspension.parent = axis 667 | 668 | def generate_animation_wheel_bones(self, name_suffix, wheel_bounding_box, parent_bone): 669 | amt = self.ob.data 670 | 671 | def_wheel_bone = amt.edit_bones.get(name_suffix.name('DEF-Wheel')) 672 | 673 | if def_wheel_bone is None: 674 | return 675 | 676 | ground_sensor = amt.edit_bones.new(name_suffix.name('GroundSensor')) 677 | ground_sensor.head = wheel_bounding_box.box_center 678 | ground_sensor.head.z = def_wheel_bone.head.z 679 | ground_sensor.tail = ground_sensor.head 680 | ground_sensor.tail.y += max(max(wheel_bounding_box.height, ground_sensor.head.z) / 2.5, wheel_bounding_box.width * 1.02) 681 | ground_sensor.use_deform = False 682 | ground_sensor.parent = parent_bone 683 | 684 | shp_ground_sensor = amt.edit_bones.new(name_suffix.name('SHP-GroundSensor')) 685 | shp_ground_sensor.head = ground_sensor.head 686 | shp_ground_sensor.tail = ground_sensor.tail 687 | shp_ground_sensor.head.z = shp_ground_sensor.tail.z = .001 688 | shp_ground_sensor.use_deform = False 689 | shp_ground_sensor.parent = ground_sensor 690 | 691 | mch_wheel = amt.edit_bones.new(name_suffix.name('MCH-Wheel')) 692 | mch_wheel.head = def_wheel_bone.head 693 | mch_wheel.tail = def_wheel_bone.tail 694 | mch_wheel.tail.y += .5 695 | mch_wheel.use_deform = False 696 | mch_wheel.parent = ground_sensor 697 | 698 | define_custom_property(self.ob, 699 | name=name_suffix.name('Wheel.rotation'), 700 | value=.0, 701 | description="Animation property for wheel spinning") 702 | mch_wheel_rotation = amt.edit_bones.new(name_suffix.name('MCH-Wheel.rotation')) 703 | mch_wheel_rotation.head = def_wheel_bone.head 704 | mch_wheel_rotation.tail = def_wheel_bone.head 705 | mch_wheel_rotation.tail.y += mch_wheel_rotation.tail.z 706 | mch_wheel_rotation.use_deform = False 707 | 708 | def_wheel_brake_bone = amt.edit_bones.get(name_suffix.name('DEF-WheelBrake')) 709 | if def_wheel_brake_bone is not None: 710 | mch_wheel = amt.edit_bones.new(name_suffix.name('MCH-WheelBrake')) 711 | mch_wheel.head = def_wheel_brake_bone.head 712 | mch_wheel.tail = def_wheel_brake_bone.tail 713 | mch_wheel.tail.y += .5 714 | mch_wheel.use_deform = False 715 | mch_wheel.parent = ground_sensor 716 | 717 | wheel = amt.edit_bones.new(name_suffix.name('Wheel')) 718 | wheel.use_deform = False 719 | wheel.parent = ground_sensor 720 | wheel.head = def_wheel_bone.head 721 | wheel.head.x = wheel_bounding_box.compute_outer_x(wheel_bounding_box.length * .05) 722 | wheel.tail = wheel.head 723 | wheel.tail.y += wheel.tail.z * .9 724 | 725 | if name_suffix.is_left and name_suffix.is_first: 726 | wheel_brake = amt.edit_bones.new(name_suffix.name('WheelBrake')) 727 | create_wheel_brake_bone(wheel_brake, mch_wheel, wheel) 728 | 729 | def generate_wheel_damper(self, wheel_dimension, parent_bone): 730 | amt = self.ob.data 731 | 732 | if wheel_dimension.nb == 1: 733 | wheel_damper_parent = amt.edit_bones[wheel_dimension.name('GroundSensor')] 734 | else: 735 | wheel_damper_parent = amt.edit_bones.new(wheel_dimension.name('MCH-GroundSensor')) 736 | wheel_damper_parent.head = wheel_dimension.medium_position 737 | wheel_damper_parent.tail = wheel_dimension.medium_position 738 | wheel_damper_parent.tail.y += 1.0 739 | wheel_damper_parent.head.z = 0 740 | wheel_damper_parent.tail.z = 0 741 | wheel_damper_parent.use_deform = False 742 | wheel_damper_parent.parent = parent_bone 743 | 744 | wheel_damper = amt.edit_bones.new(wheel_dimension.name('WheelDamper')) 745 | wheel_damper.head = wheel_dimension.medium_position 746 | wheel_damper_scale_ratio = abs(wheel_damper.head.z) 747 | wheel_damper.head.x = wheel_dimension.compute_outer_x(wheel_damper_scale_ratio * .25) 748 | wheel_damper.head.z *= 1.5 749 | wheel_damper.tail = wheel_damper.head 750 | wheel_damper.tail.y += wheel_damper_scale_ratio 751 | wheel_damper.use_deform = False 752 | wheel_damper.parent = wheel_damper_parent 753 | 754 | mch_wheel_damper = amt.edit_bones.new(wheel_dimension.name('MCH-WheelDamper')) 755 | mch_wheel_damper.head = wheel_dimension.medium_position 756 | mch_wheel_damper.tail = wheel_dimension.medium_position 757 | mch_wheel_damper.tail.y += 2 758 | mch_wheel_damper.use_deform = False 759 | mch_wheel_damper.parent = wheel_damper 760 | 761 | def generate_constraints_on_rig(self): 762 | pose = self.ob.pose 763 | amt = self.ob.data 764 | 765 | for b in pose.bones: 766 | if b.name.startswith('DEF-') or b.name.startswith('MCH-') or b.name.startswith('SHP-'): 767 | b.lock_location = (True, True, True) 768 | b.lock_rotation = (True, True, True) 769 | b.lock_scale = (True, True, True) 770 | b.lock_rotation_w = True 771 | 772 | for wheel_dimension in self.dimension.wheels_dimensions: 773 | for name_suffix in wheel_dimension.name_suffixes(): 774 | self.generate_constraints_on_wheel_bones(name_suffix) 775 | self.generate_constraints_on_wheel_damper(wheel_dimension) 776 | 777 | self.generate_constraints_on_axle_bones('Ft') 778 | self.generate_constraints_on_axle_bones('Bk') 779 | 780 | mch_axis = pose.bones.get('MCH-Axis') 781 | if mch_axis is not None: 782 | for axis_pos, influence in (('Ft', 1), ('Bk', .5)): 783 | subtarget = 'MCH-Axis.%s' % axis_pos 784 | if subtarget in pose.bones: 785 | cns = mch_axis.constraints.new('TRANSFORM') 786 | cns.name = 'Rotation from %s' % subtarget 787 | cns.target = self.ob 788 | cns.subtarget = subtarget 789 | cns.map_from = 'ROTATION' 790 | cns.from_min_x_rot = math.radians(-180) 791 | cns.from_max_x_rot = math.radians(180) 792 | cns.map_to_y_from = 'X' 793 | cns.map_to = 'ROTATION' 794 | cns.to_min_y_rot = math.radians(180) 795 | cns.to_max_y_rot = math.radians(-180) 796 | cns.owner_space = 'LOCAL' 797 | cns.target_space = 'LOCAL' 798 | create_constraint_influence_driver(self.ob, cns, '["suspension_rolling_factor"]', base_influence=influence) 799 | 800 | root = pose.bones['Root'] 801 | root.lock_scale = (True, True, True) 802 | root.custom_shape = get_widget('WGT-CarRig.Root') 803 | root.custom_shape_transform = pose.bones['SHP-Root'] 804 | root.bone.show_wire = True 805 | 806 | for ground_sensor_axle_name in ('GroundSensor.Axle.Ft', 'GroundSensor.Axle.Bk'): 807 | groundsensor_axle = pose.bones.get(ground_sensor_axle_name) 808 | if groundsensor_axle: 809 | groundsensor_axle.lock_location = (True, True, False) 810 | groundsensor_axle.lock_rotation = (True, True, True) 811 | groundsensor_axle.lock_scale = (True, True, True) 812 | groundsensor_axle.custom_shape = get_widget('WGT-CarRig.GroundSensor.Axle') 813 | groundsensor_axle.lock_rotation_w = True 814 | groundsensor_axle.custom_shape_transform = pose.bones['SHP-%s' % groundsensor_axle.name] 815 | groundsensor_axle.bone.show_wire = True 816 | self.generate_ground_projection_constraint(groundsensor_axle) 817 | 818 | if groundsensor_axle.name == 'GroundSensor.Axle.Ft' and 'GroundSensor.Axle.Bk' in pose.bones: 819 | cns = groundsensor_axle.constraints.new('LIMIT_DISTANCE') 820 | cns.name = 'Limit distance from Root' 821 | cns.limit_mode = 'LIMITDIST_ONSURFACE' 822 | cns.target = self.ob 823 | cns.subtarget = 'GroundSensor.Axle.Bk' 824 | cns.use_transform_limit = True 825 | cns.owner_space = 'POSE' 826 | cns.target_space = 'POSE' 827 | 828 | mch_root_axle_front = pose.bones.get('MCH-Root.Axle.Ft') 829 | mch_root_axle_back = pose.bones.get('MCH-Root.Axle.Bk') 830 | if mch_root_axle_front and mch_root_axle_back: 831 | cns = mch_root_axle_back.constraints.new('DAMPED_TRACK') 832 | cns.name = 'Track front axle' 833 | cns.target = self.ob 834 | cns.subtarget = mch_root_axle_front.name 835 | cns.track_axis = 'TRACK_NEGATIVE_Y' 836 | 837 | drift = pose.bones['Drift'] 838 | drift.lock_location = (True, True, True) 839 | drift.lock_rotation = (True, True, False) 840 | drift.lock_scale = (True, True, True) 841 | drift.rotation_mode = 'ZYX' 842 | drift.custom_shape = get_widget('WGT-CarRig.DriftHandle') 843 | drift.custom_shape_transform = pose.bones['SHP-Drift'] 844 | drift.bone.show_wire = True 845 | 846 | suspension = pose.bones['Suspension'] 847 | suspension.lock_rotation = (True, True, True) 848 | suspension.lock_scale = (True, True, True) 849 | suspension.lock_rotation_w = True 850 | suspension.custom_shape = get_widget('WGT-CarRig.Suspension') 851 | suspension.bone.show_wire = True 852 | 853 | steering = pose.bones.get('Steering') 854 | if steering is not None: 855 | steering.lock_location = (False, True, True) 856 | steering.lock_rotation = (True, True, True) 857 | steering.lock_scale = (True, True, True) 858 | steering.lock_rotation_w = True 859 | steering.custom_shape = get_widget('WGT-CarRig.Steering') 860 | steering.bone.show_wire = True 861 | 862 | mch_steering_rotation = pose.bones['MCH-Steering.rotation'] 863 | mch_steering_rotation.rotation_mode = 'QUATERNION' 864 | define_custom_property(self.ob, 865 | name='Steering.rotation', 866 | value=.0, 867 | description="Animation property for steering") 868 | create_translation_x_driver(self.ob, mch_steering_rotation, '["Steering.rotation"]') 869 | 870 | if mch_root_axle_back: 871 | cns = mch_steering_rotation.constraints.new('COPY_ROTATION') 872 | cns.name = 'Copy back axle rotation' 873 | cns.target = self.ob 874 | cns.subtarget = mch_root_axle_back.name 875 | cns.use_x = True 876 | cns.use_y = False 877 | cns.use_z = False 878 | cns.owner_space = 'LOCAL' 879 | cns.target_space = 'LOCAL' 880 | 881 | self.generate_childof_constraint(mch_steering_rotation, mch_root_axle_front if mch_root_axle_front else root) 882 | 883 | mch_steering = pose.bones['MCH-Steering'] 884 | cns = mch_steering.constraints.new('DAMPED_TRACK') 885 | cns.name = 'Track steering bone' 886 | cns.target = self.ob 887 | cns.subtarget = 'Steering' 888 | cns.track_axis = 'TRACK_NEGATIVE_Y' 889 | 890 | cns = mch_steering.constraints.new('COPY_ROTATION') 891 | cns.name = 'Drift counter animation' 892 | cns.target = self.ob 893 | cns.subtarget = 'Drift' 894 | cns.use_x = False 895 | cns.use_y = False 896 | cns.use_z = True 897 | cns.use_offset = True 898 | cns.owner_space = 'LOCAL' 899 | cns.target_space = 'LOCAL' 900 | 901 | mch_body = self.ob.pose.bones['MCH-Body'] 902 | cns = mch_body.constraints.new('TRANSFORM') 903 | cns.name = 'Suspension on rollover' 904 | cns.target = self.ob 905 | cns.subtarget = 'Suspension' 906 | cns.map_from = 'LOCATION' 907 | cns.from_min_x = -2 908 | cns.from_max_x = 2 909 | cns.from_min_y = -2 910 | cns.from_max_y = 2 911 | cns.map_to_x_from = 'Y' 912 | cns.map_to_y_from = 'X' 913 | cns.map_to = 'ROTATION' 914 | cns.to_min_x_rot = math.radians(6) 915 | cns.to_max_x_rot = math.radians(-6) 916 | cns.to_min_y_rot = math.radians(-7) 917 | cns.to_max_y_rot = math.radians(7) 918 | cns.owner_space = 'LOCAL' 919 | cns.target_space = 'LOCAL' 920 | 921 | cns = mch_body.constraints.new('TRANSFORM') 922 | cns.name = 'Suspension on vertical' 923 | cns.target = self.ob 924 | cns.subtarget = 'Suspension' 925 | cns.map_from = 'LOCATION' 926 | cns.from_min_z = -0.5 927 | cns.from_max_z = 0.5 928 | cns.map_to_z_from = 'Z' 929 | cns.map_to = 'LOCATION' 930 | cns.to_min_z = -0.1 931 | cns.to_max_z = 0.1 932 | cns.owner_space = 'LOCAL' 933 | cns.target_space = 'LOCAL' 934 | 935 | body = self.ob.pose.bones['DEF-Body'] 936 | cns = body.constraints.new('COPY_TRANSFORMS') 937 | cns.target = self.ob 938 | cns.subtarget = 'MCH-Body' 939 | 940 | def generate_ground_projection_constraint(self, bone): 941 | cns = bone.constraints.new('SHRINKWRAP') 942 | cns.name = 'Ground projection' 943 | cns.shrinkwrap_type = 'NEAREST_SURFACE' 944 | cns.project_axis_space = 'LOCAL' 945 | cns.project_axis = 'NEG_Z' 946 | cns.distance = abs(bone.head.z) 947 | 948 | def generate_childof_constraint(self, child, parent): 949 | cns = child.constraints.new('CHILD_OF') 950 | cns.target = self.ob 951 | cns.subtarget = parent.name 952 | cns.inverse_matrix = self.ob.data.bones[parent.name].matrix_local.inverted() 953 | cns.use_location_x = True 954 | cns.use_location_y = True 955 | cns.use_location_z = True 956 | cns.use_rotation_x = True 957 | cns.use_rotation_y = True 958 | cns.use_rotation_z = True 959 | return cns 960 | 961 | def generate_constraints_on_axle_bones(self, position): 962 | pose = self.ob.pose 963 | 964 | subtarget = 'MCH-Axis.%s' % position 965 | if subtarget in pose.bones: 966 | mch_suspension = pose.bones['MCH-Suspension.%s' % position] 967 | cns = mch_suspension.constraints.new('COPY_LOCATION') 968 | cns.name = 'Location from %s' % subtarget 969 | cns.target = self.ob 970 | cns.subtarget = subtarget 971 | cns.head_tail = .5 972 | cns.use_x = False 973 | cns.use_y = False 974 | cns.use_z = True 975 | cns.owner_space = 'WORLD' 976 | cns.target_space = 'WORLD' 977 | create_constraint_influence_driver(self.ob, cns, '["suspension_factor"]') 978 | 979 | if position == 'Ft': 980 | cns = mch_suspension.constraints.new('DAMPED_TRACK') 981 | cns.name = 'Track suspension back' 982 | cns.target = self.ob 983 | cns.subtarget = 'MCH-Suspension.Bk' 984 | cns.track_axis = 'TRACK_Y' 985 | 986 | mch_axis = pose.bones.get('MCH-Axis.%s' % position) 987 | if mch_axis is not None: 988 | cns = mch_axis.constraints.new('COPY_LOCATION') 989 | cns.name = 'Copy location from right wheel' 990 | cns.target = self.ob 991 | cns.subtarget = 'MCH-WheelDamper.%s.R' % position 992 | cns.use_x = True 993 | cns.use_y = True 994 | cns.use_z = True 995 | cns.owner_space = 'WORLD' 996 | cns.target_space = 'WORLD' 997 | 998 | mch_axis = pose.bones['MCH-Axis.%s' % position] 999 | cns = mch_axis.constraints.new('DAMPED_TRACK') 1000 | cns.name = 'Track Left Wheel' 1001 | cns.target = self.ob 1002 | cns.subtarget = 'MCH-WheelDamper.%s.L' % position 1003 | cns.track_axis = 'TRACK_Y' 1004 | 1005 | def generate_constraints_on_wheel_bones(self, name_suffix): 1006 | pose = self.ob.pose 1007 | 1008 | def_wheel = pose.bones.get(name_suffix.name('DEF-Wheel')) 1009 | if def_wheel is None: 1010 | return 1011 | 1012 | cns = def_wheel.constraints.new('COPY_TRANSFORMS') 1013 | cns.target = self.ob 1014 | cns.subtarget = name_suffix.name('MCH-Wheel') 1015 | 1016 | def_wheel_brake = pose.bones.get(name_suffix.name('DEF-WheelBrake')) 1017 | if def_wheel_brake is not None: 1018 | cns = def_wheel_brake.constraints.new('COPY_TRANSFORMS') 1019 | cns.target = self.ob 1020 | cns.subtarget = name_suffix.name('MCH-WheelBrake') 1021 | 1022 | ground_sensor = pose.bones[name_suffix.name('GroundSensor')] 1023 | ground_sensor.lock_location = (True, True, False) 1024 | ground_sensor.lock_rotation = (True, True, True) 1025 | ground_sensor.lock_rotation_w = True 1026 | ground_sensor.lock_scale = (True, True, True) 1027 | ground_sensor.custom_shape = get_widget('WGT-CarRig.GroundSensor') 1028 | ground_sensor.custom_shape_transform = pose.bones['SHP-%s' % ground_sensor.name] 1029 | ground_sensor.bone.show_wire = True 1030 | 1031 | if name_suffix.is_front: 1032 | cns = ground_sensor.constraints.new('COPY_ROTATION') 1033 | cns.name = 'Steering rotation' 1034 | cns.target = self.ob 1035 | cns.subtarget = 'MCH-Steering' 1036 | cns.use_x = False 1037 | cns.use_y = False 1038 | cns.use_z = True 1039 | cns.owner_space = 'LOCAL' 1040 | cns.target_space = 'LOCAL' 1041 | 1042 | self.generate_ground_projection_constraint(ground_sensor) 1043 | 1044 | cns = ground_sensor.constraints.new('LIMIT_LOCATION') 1045 | cns.name = 'Ground projection limitation' 1046 | cns.use_transform_limit = True 1047 | cns.owner_space = 'LOCAL' 1048 | cns.use_max_x = True 1049 | cns.use_min_x = True 1050 | cns.min_x = 0 1051 | cns.max_x = 0 1052 | cns.use_max_y = True 1053 | cns.use_min_y = True 1054 | cns.min_y = 0 1055 | cns.max_y = 0 1056 | cns.use_max_z = True 1057 | cns.use_min_z = True 1058 | cns.min_z = -.2 1059 | cns.max_z = .2 1060 | 1061 | wheel = pose.bones.get(name_suffix.name('Wheel')) 1062 | wheel.rotation_mode = "XYZ" 1063 | wheel.lock_location = (True, True, True) 1064 | wheel.lock_rotation = (False, True, True) 1065 | wheel.lock_scale = (True, True, True) 1066 | wheel.custom_shape = get_widget('WGT-CarRig.Wheel') 1067 | wheel.bone.show_wire = True 1068 | 1069 | wheel_brake = pose.bones.get(name_suffix.name('WheelBrake')) 1070 | if wheel_brake: 1071 | generate_constraint_on_wheel_brake_bone(wheel_brake, wheel) 1072 | 1073 | mch_wheel = pose.bones[name_suffix.name('MCH-Wheel')] 1074 | mch_wheel.rotation_mode = "XYZ" 1075 | 1076 | cns = mch_wheel.constraints.new('COPY_ROTATION') 1077 | cns.name = 'Bake animation wheels' 1078 | cns.target = self.ob 1079 | cns.subtarget = name_suffix.name('MCH-Wheel.rotation') 1080 | cns.use_x = True 1081 | cns.use_y = False 1082 | cns.use_z = False 1083 | cns.use_offset = False 1084 | cns.owner_space = 'POSE' 1085 | cns.target_space = 'POSE' 1086 | 1087 | cns = mch_wheel.constraints.new('TRANSFORM') 1088 | cns.name = 'Wheel rotation along Y axis' 1089 | cns.target = self.ob 1090 | cns.subtarget = 'Root' 1091 | cns.use_motion_extrapolate = True 1092 | cns.map_from = 'LOCATION' 1093 | cns.from_min_y = - math.pi * abs(mch_wheel.head.z if mch_wheel.head.z != 0 else 1) 1094 | cns.from_max_y = - cns.from_min_y 1095 | cns.map_to_x_from = 'Y' 1096 | cns.map_to = 'ROTATION' 1097 | cns.to_min_x_rot = math.pi 1098 | cns.to_max_x_rot = -math.pi 1099 | cns.owner_space = 'LOCAL' 1100 | cns.target_space = 'LOCAL' 1101 | 1102 | create_constraint_influence_driver(self.ob, cns, '["wheels_on_y_axis"]') 1103 | 1104 | cns = mch_wheel.constraints.new('COPY_ROTATION') 1105 | cns.name = 'Animation wheels' 1106 | cns.target = self.ob 1107 | cns.subtarget = wheel.name 1108 | cns.use_x = True 1109 | cns.use_y = False 1110 | cns.use_z = False 1111 | cns.use_offset = True 1112 | cns.owner_space = 'LOCAL' 1113 | cns.target_space = 'LOCAL' 1114 | 1115 | mch_wheel_rotation = pose.bones[name_suffix.name('MCH-Wheel.rotation')] 1116 | mch_wheel_rotation.rotation_mode = "XYZ" 1117 | self.generate_childof_constraint(mch_wheel_rotation, ground_sensor) 1118 | create_rotation_euler_x_driver(self.ob, mch_wheel_rotation, '["%s"]' % name_suffix.name('Wheel.rotation')) 1119 | 1120 | def generate_constraints_on_wheel_damper(self, wheel_dimension): 1121 | pose = self.ob.pose 1122 | 1123 | wheel_damper = pose.bones.get(wheel_dimension.name('WheelDamper')) 1124 | if wheel_damper is not None: 1125 | wheel_damper.lock_location = (True, True, False) 1126 | wheel_damper.lock_rotation = (True, True, True) 1127 | wheel_damper.lock_rotation_w = True 1128 | wheel_damper.lock_scale = (True, True, True) 1129 | wheel_damper.custom_shape = get_widget('WGT-CarRig.WheelDamper') 1130 | wheel_damper.bone.show_wire = True 1131 | 1132 | mch_ground_sensor = pose.bones.get(wheel_dimension.name('MCH-GroundSensor')) 1133 | if mch_ground_sensor is not None: 1134 | fcurve = mch_ground_sensor.driver_add('location', 2) 1135 | drv = fcurve.driver 1136 | drv.type = 'MAX' 1137 | 1138 | for i, ground_sensor_name in enumerate(wheel_dimension.names('GroundSensor')): 1139 | if ground_sensor_name in pose.bones: 1140 | var = drv.variables.new() 1141 | var.name = 'groundSensor%03d' % i 1142 | var.type = 'TRANSFORMS' 1143 | 1144 | targ = var.targets[0] 1145 | targ.id = self.ob 1146 | targ.bone_target = ground_sensor_name 1147 | targ.transform_space = 'LOCAL_SPACE' 1148 | targ.transform_type = 'LOC_Z' 1149 | 1150 | def generate_bone_groups(self): 1151 | pose = self.ob.pose 1152 | create_bone_group(pose, 'Direction', color_set='THEME04', bone_names=('Root', 'Drift', 'SHP-Root', 'SHP-Drift')) 1153 | create_bone_group(pose, 'Suspension', color_set='THEME09', bone_names=('Suspension', 'WheelDamper.Ft.L', 'WheelDamper.Ft.R', 'WheelDamper.Bk.L', 'WheelDamper.Bk.R')) 1154 | 1155 | wheel_widgets = ('Steering',) 1156 | for wheel_dimension in self.dimension.wheels_dimensions: 1157 | wheel_widgets += tuple(wheel_dimension.names('Wheel')) 1158 | wheel_widgets += tuple(wheel_dimension.names('WheelBrake')) 1159 | create_bone_group(pose, 'Wheel', color_set='THEME03', bone_names=wheel_widgets) 1160 | 1161 | ground_sensor_names = ('GroundSensor.Axle.Ft', 'GroundSensor.Axle.Bk', 'SHP-GroundSensor.Axle.Ft', 'SHP-GroundSensor.Axle.Bk') 1162 | for wheel_dimension in self.dimension.wheels_dimensions: 1163 | ground_sensor_names += tuple(wheel_dimension.names('GroundSensor')) 1164 | ground_sensor_names += tuple("SHP-%s" % i for i in ground_sensor_names) 1165 | create_bone_group(pose, 'GroundSensor', color_set='THEME02', bone_names=ground_sensor_names) 1166 | 1167 | def set_origin(self, scene): 1168 | object_location = self.ob.location[:] 1169 | root = self.ob.data.bones.get('Root') 1170 | if root: 1171 | cursor_location = scene.cursor.location[:] 1172 | scene.cursor.location = root.head 1173 | try: 1174 | bpy.ops.object.origin_set(type='ORIGIN_CURSOR') 1175 | finally: 1176 | scene.cursor.location = cursor_location 1177 | self.ob.location = object_location 1178 | 1179 | 1180 | class OBJECT_OT_armatureCarDeformationRig(bpy.types.Operator): 1181 | bl_idname = "object.armature_car_deformation_rig" 1182 | bl_label = "Add car deformation rig" 1183 | bl_description = "Creates the base rig for a car." 1184 | bl_options = {'REGISTER', 'UNDO'} 1185 | 1186 | body_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location', 1187 | description='Extra translation added to location of the car body', 1188 | size=3, 1189 | default=(0, 0, 0), 1190 | subtype='TRANSLATION') 1191 | 1192 | nb_front_wheels_pairs: bpy.props.IntProperty(name='Pairs', 1193 | description='Number of front wheels pairs', 1194 | default=1, 1195 | min=0) 1196 | 1197 | front_wheel_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location', 1198 | description='Extra translation added to location of the front wheels', 1199 | size=3, 1200 | default=(0, 0, 0), 1201 | subtype='TRANSLATION') 1202 | 1203 | nb_back_wheels_pairs: bpy.props.IntProperty(name='Pairs', 1204 | description='Number of back wheels pairs', 1205 | default=1, 1206 | min=0) 1207 | 1208 | back_wheel_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location', 1209 | description='Extra translation added to location of the back wheels', 1210 | size=3, 1211 | default=(0, 0, 0), 1212 | subtype='TRANSLATION') 1213 | 1214 | nb_front_wheel_brakes_pairs: bpy.props.IntProperty(name='Front Pairs', 1215 | description='Number of front wheel brakes pairs', 1216 | default=0, 1217 | min=0) 1218 | 1219 | front_wheel_brakes_pos_delta: bpy.props.FloatProperty(name='Front Delta Location', 1220 | description='Extra translation added to location of the front brakes', 1221 | default=0) 1222 | 1223 | nb_back_wheel_brakes_pairs: bpy.props.IntProperty(name='Back Pairs', 1224 | description='Number of back wheel brakes pairs', 1225 | default=0, 1226 | min=0) 1227 | 1228 | back_wheel_brakes_pos_delta: bpy.props.FloatProperty(name='Back Delta Location', 1229 | description='Extra translation added to location of the back brakes', 1230 | default=0) 1231 | 1232 | def draw(self, context): 1233 | self.layout.use_property_split = True 1234 | self.layout.use_property_decorate = False 1235 | self.layout.label(text='Body') 1236 | layout = self.layout.box() 1237 | layout.prop(self, 'body_pos_delta') 1238 | self.layout.label(text='Front wheels') 1239 | layout = self.layout.box() 1240 | layout.prop(self, 'nb_front_wheels_pairs') 1241 | layout.prop(self, 'front_wheel_pos_delta') 1242 | self.layout.label(text='Back wheels') 1243 | layout = self.layout.box() 1244 | layout.prop(self, 'nb_back_wheels_pairs') 1245 | layout.prop(self, 'back_wheel_pos_delta') 1246 | self.layout.label(text='Brakes') 1247 | layout = self.layout.box() 1248 | layout.prop(self, 'nb_front_wheel_brakes_pairs') 1249 | layout.prop(self, 'front_wheel_brakes_pos_delta') 1250 | layout.prop(self, 'nb_back_wheel_brakes_pairs') 1251 | layout.prop(self, 'back_wheel_brakes_pos_delta') 1252 | 1253 | def invoke(self, context, event): 1254 | self.bones_position = { 1255 | 'Body': mathutils.Vector((0.0, 0, .8)), 1256 | 'Wheel.Ft.L': mathutils.Vector((0.9, -2, .5)), 1257 | 'Wheel.Ft.R': mathutils.Vector((-.9, -2, .5)), 1258 | 'Wheel.Bk.L': mathutils.Vector((0.9, 2, .5)), 1259 | 'Wheel.Bk.R': mathutils.Vector((-.9, 2, .5)), 1260 | 'WheelBrake.Ft.L': mathutils.Vector((0.8, -2, .5)), 1261 | 'WheelBrake.Ft.R': mathutils.Vector((-.8, -2, .5)), 1262 | 'WheelBrake.Bk.L': mathutils.Vector((0.8, 2, .5)), 1263 | 'WheelBrake.Bk.R': mathutils.Vector((-.8, 2, .5)) 1264 | } 1265 | self.target_objects_name = {} 1266 | 1267 | has_body_target = self._find_target_object(context, 'Body') 1268 | 1269 | nb_wheels_ft_l = self._find_target_object_for_wheels(context, 'Wheel.Ft.L') 1270 | nb_wheels_ft_r = self._find_target_object_for_wheels(context, 'Wheel.Ft.R') 1271 | nb_wheels_bk_l = self._find_target_object_for_wheels(context, 'Wheel.Bk.L') 1272 | nb_wheels_bk_r = self._find_target_object_for_wheels(context, 'Wheel.Bk.R') 1273 | 1274 | nb_wheel_brakes_ft_l = self._find_target_object_for_wheels(context, 'WheelBrake.Ft.L') 1275 | nb_wheel_brakes_ft_r = self._find_target_object_for_wheels(context, 'WheelBrake.Ft.R') 1276 | nb_wheel_brakes_bk_l = self._find_target_object_for_wheels(context, 'WheelBrake.Bk.L') 1277 | nb_wheel_brakes_bk_r = self._find_target_object_for_wheels(context, 'WheelBrake.Bk.R') 1278 | 1279 | self.nb_front_wheels_pairs = max(nb_wheels_ft_l, nb_wheels_ft_r) 1280 | self.nb_back_wheels_pairs = max(nb_wheels_bk_l, nb_wheels_bk_r) 1281 | self.nb_front_wheel_brakes_pairs = max(nb_wheel_brakes_ft_l, nb_wheel_brakes_ft_r) 1282 | self.nb_back_wheel_brakes_pairs = max(nb_wheel_brakes_bk_l, nb_wheel_brakes_bk_r) 1283 | 1284 | # if no target object has been found for body, we assume it may have no 1285 | # target object for front and back wheels either. 1286 | if not has_body_target: 1287 | self.nb_front_wheels_pairs = max(1, self.nb_front_wheels_pairs) 1288 | self.nb_back_wheels_pairs = max(1, self.nb_back_wheels_pairs) 1289 | 1290 | return self.execute(context) 1291 | 1292 | def _find_target_object_for_wheels(self, context, suffix_name): 1293 | for count, name in enumerate(name_range(suffix_name)): 1294 | if not self._find_target_object(context, name): 1295 | return count 1296 | 1297 | def _find_target_object(self, context, name): 1298 | escaped_name = re.escape(name).replace(r'\.', r'[\.-_ ]') 1299 | pattern = re.compile(f"^.*{escaped_name}$", re.IGNORECASE) 1300 | for obj in context.selected_objects: 1301 | if pattern.match(obj.name): 1302 | self.target_objects_name[name] = obj.name 1303 | self.bones_position[name] = obj.location.copy() 1304 | return True 1305 | return False 1306 | 1307 | def execute(self, context): 1308 | """Creates the meta rig with basic bones""" 1309 | amt = bpy.data.armatures.new('Car Rig Data') 1310 | amt['Car Rig'] = False 1311 | 1312 | rig = bpy_extras.object_utils.object_data_add(context, amt, name='Car Rig') 1313 | 1314 | # TODO: cannot edit new object added to a hidden collection 1315 | # Could be a better fix (steal code from other addons). 1316 | try: 1317 | bpy.ops.object.mode_set(mode='EDIT') 1318 | except TypeError: 1319 | self.report({'ERROR'}, "Cannot edit the new armature! Please make sure the active collection is visible and editable") 1320 | return {'CANCELLED'} 1321 | 1322 | self._create_bone(rig, 'Body', delta_pos=self.body_pos_delta) 1323 | 1324 | self._create_wheel_bones(rig, 'Wheel.Ft.L', self.nb_front_wheels_pairs, self.front_wheel_pos_delta) 1325 | self._create_wheel_bones(rig, 'Wheel.Ft.R', self.nb_front_wheels_pairs, self.front_wheel_pos_delta.reflect(mathutils.Vector((1, 0, 0)))) 1326 | self._create_wheel_bones(rig, 'Wheel.Bk.L', self.nb_back_wheels_pairs, self.back_wheel_pos_delta) 1327 | self._create_wheel_bones(rig, 'Wheel.Bk.R', self.nb_back_wheels_pairs, self.back_wheel_pos_delta.reflect(mathutils.Vector((1, 0, 0)))) 1328 | 1329 | front_wheel_brakes_delta_pos = self.front_wheel_pos_delta.copy() 1330 | front_wheel_brakes_delta_pos.x = self.front_wheel_brakes_pos_delta 1331 | self._create_wheel_bones(rig, 'WheelBrake.Ft.L', self.nb_front_wheel_brakes_pairs, front_wheel_brakes_delta_pos) 1332 | self._create_wheel_bones(rig, 'WheelBrake.Ft.R', self.nb_front_wheel_brakes_pairs, front_wheel_brakes_delta_pos.reflect(mathutils.Vector((1, 0, 0)))) 1333 | back_wheel_brakes_delta_pos = self.back_wheel_pos_delta.copy() 1334 | back_wheel_brakes_delta_pos.x = self.back_wheel_brakes_pos_delta 1335 | self._create_wheel_bones(rig, 'WheelBrake.Bk.L', self.nb_back_wheel_brakes_pairs, back_wheel_brakes_delta_pos) 1336 | self._create_wheel_bones(rig, 'WheelBrake.Bk.R', self.nb_back_wheel_brakes_pairs, back_wheel_brakes_delta_pos.reflect(mathutils.Vector((1, 0, 0)))) 1337 | 1338 | deselect_edit_bones(rig) 1339 | 1340 | bpy.ops.object.mode_set(mode='OBJECT') 1341 | 1342 | return {'FINISHED'} 1343 | 1344 | def _create_bone(self, rig, name, delta_pos): 1345 | b = rig.data.edit_bones.new('DEF-' + name) 1346 | 1347 | b.head = self.bones_position[name] + delta_pos 1348 | b.tail = b.head 1349 | if name == 'Body': 1350 | b.tail.y += b.tail.z * 4 1351 | else: 1352 | b.tail.y += b.tail.z 1353 | 1354 | target_obj_name = self.target_objects_name.get(name) 1355 | if target_obj_name is not None and target_obj_name in bpy.context.scene.objects: 1356 | target_obj = bpy.context.scene.objects[target_obj_name] 1357 | if name == 'Body': 1358 | b.tail = b.head 1359 | b.tail.y += target_obj.dimensions[1] / 2 if target_obj.dimensions and target_obj.dimensions[0] != 0 else 1 1360 | target_obj.parent = rig 1361 | target_obj.parent_bone = b.name 1362 | target_obj.parent_type = 'BONE' 1363 | target_obj.location += rig.matrix_world.to_translation() 1364 | target_obj.matrix_parent_inverse = (rig.matrix_world @ mathutils.Matrix.Translation(b.tail)).inverted() 1365 | 1366 | return b 1367 | 1368 | def _create_wheel_bones(self, rig, base_wheel_name, nb_wheels, delta_pos): 1369 | for wheel_name in name_range(base_wheel_name, nb_wheels): 1370 | if wheel_name not in self.bones_position: 1371 | wheel_position = previous_wheel_default_pos.copy() 1372 | wheel_position.y += abs(previous_wheel.head.z * 2.2) 1373 | self.bones_position[wheel_name] = wheel_position 1374 | previous_wheel = self._create_bone(rig, wheel_name, delta_pos) 1375 | previous_wheel_default_pos = self.bones_position[wheel_name] 1376 | 1377 | 1378 | class POSE_OT_carAnimationRigGenerate(bpy.types.Operator): 1379 | bl_idname = "pose.car_animation_rig_generate" 1380 | bl_label = "Generate car animation rig" 1381 | bl_description = "Creates the complete armature for animating the car." 1382 | bl_options = {'REGISTER', 'UNDO'} 1383 | 1384 | adjust_origin: bpy.props.BoolProperty(name='Move origin', 1385 | description='Set origin of the armature at the same location as the root bone', 1386 | default=True) 1387 | 1388 | @classmethod 1389 | def poll(cls, context): 1390 | return context.object is not None and context.object.data is not None and 'Car Rig' in context.object.data 1391 | 1392 | def draw(self, context): 1393 | self.layout.use_property_split = True 1394 | self.layout.use_property_decorate = False 1395 | self.layout.prop(self, 'adjust_origin') 1396 | 1397 | def execute(self, context): 1398 | if context.object.data['Car Rig']: 1399 | self.report({'INFO'}, 'Rig already generated') 1400 | return {"CANCELLED"} 1401 | 1402 | if 'DEF-Body' not in context.object.data.bones: 1403 | self.report({'ERROR'}, 'No bone named DEF-Body. This is not a valid armature!') 1404 | return {"CANCELLED"} 1405 | 1406 | armature_generator = ArmatureGenerator(context.object) 1407 | armature_generator.generate(context.scene, self.adjust_origin) 1408 | return {"FINISHED"} 1409 | 1410 | 1411 | class POSE_OT_carAnimationAddBrakeWheelBones(bpy.types.Operator): 1412 | bl_idname = "pose.car_animation_add_brake_wheel_bones" 1413 | bl_label = "Add missing brake wheel bones" 1414 | bl_description = "Generates missing brake wheel bones for each selected wheel widget." 1415 | bl_options = {'UNDO'} 1416 | 1417 | @classmethod 1418 | def poll(cls, context): 1419 | return context.object.mode == 'POSE' and\ 1420 | context.object is not None and context.object.data is not None and\ 1421 | context.object.data.get('Car Rig') 1422 | 1423 | def execute(self, context): 1424 | mode = context.object.mode 1425 | re_wheel_bone_name = re.compile(r'^Wheel\.(Ft|Bk)\.([LR])(\.\d+)?$') 1426 | for pose_bone in context.selected_pose_bones: 1427 | matcher = re_wheel_bone_name.match(pose_bone.name) 1428 | if matcher: 1429 | wheelbrake_name = 'WheelBrake.%s.%s%s' % matcher.groups(default='') 1430 | parent_name = 'MCH-Wheel.%s.%s%s' % matcher.groups(default='') 1431 | self.create_wheelbrake_bone(context, pose_bone, wheelbrake_name, parent_name) 1432 | bpy.ops.object.mode_set(mode=mode) 1433 | return {"FINISHED"} 1434 | 1435 | def create_wheelbrake_bone(self, context, wheel_pose_bone, name, parent_name): 1436 | obj = context.object 1437 | amt = context.object.data 1438 | if name not in amt.bones and parent_name in amt.bones: 1439 | bpy.ops.object.mode_set(mode='EDIT') 1440 | create_wheel_brake_bone(amt.edit_bones.new(name), amt.edit_bones[parent_name], amt.edit_bones[wheel_pose_bone.name]) 1441 | bpy.ops.object.mode_set(mode='POSE') 1442 | generate_constraint_on_wheel_brake_bone(obj.pose.bones[name], wheel_pose_bone) 1443 | 1444 | 1445 | def register(): 1446 | bpy.utils.register_class(POSE_OT_carAnimationRigGenerate) 1447 | bpy.utils.register_class(OBJECT_OT_armatureCarDeformationRig) 1448 | bpy.utils.register_class(POSE_OT_carAnimationAddBrakeWheelBones) 1449 | 1450 | 1451 | def unregister(): 1452 | bpy.utils.unregister_class(POSE_OT_carAnimationAddBrakeWheelBones) 1453 | bpy.utils.unregister_class(OBJECT_OT_armatureCarDeformationRig) 1454 | bpy.utils.unregister_class(POSE_OT_carAnimationRigGenerate) 1455 | 1456 | 1457 | if __name__ == "__main__": 1458 | register() 1459 | -------------------------------------------------------------------------------- /widgets.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 3 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 | # 20 | 21 | import bpy 22 | 23 | COLLECTION_NAME = 'Rigacar widgets' 24 | 25 | 26 | def create(): 27 | if COLLECTION_NAME not in bpy.data.collections: 28 | c = bpy.data.collections.new(COLLECTION_NAME) 29 | c.hide_viewport = True 30 | c.hide_render = True 31 | c.hide_select = True 32 | 33 | widgets_collection = bpy.data.collections[COLLECTION_NAME] 34 | 35 | if COLLECTION_NAME not in bpy.context.scene.collection.children: 36 | bpy.context.scene.collection.children.link(widgets_collection) 37 | 38 | for name, widget in get_widgets().items(): 39 | object_name = 'WGT-CarRig.%s' % name 40 | if object_name not in bpy.data.objects: 41 | m = bpy.data.meshes.new(object_name) 42 | m.from_pydata(widget['vertices'], widget['edges'], []) 43 | o = bpy.data.objects.new(object_name, m) 44 | else: 45 | o = bpy.data.objects[object_name] 46 | 47 | if object_name not in widgets_collection.objects: 48 | widgets_collection.objects.link(o) 49 | 50 | 51 | def get_widgets(): 52 | """ 53 | Return array of data compatible with method Mesh.from_pydata. 54 | To get data in Blender from the selected object, use: 55 | {'vertices': [v.co[:] for v in bpy.context.object.data.vertices], 'edges': [e.vertices[:] for e in bpy.context.object.data.edges]} 56 | """ 57 | widgets = {} 58 | widgets['DriftHandle'] = { 59 | 'vertices': [(0.560016930103302, 3.5614462490229926e-07, 0.0), (0.5472726821899414, 0.051212746649980545, 0.0), 60 | (0.5173879265785217, 0.09973714500665665, 0.0), (0.4643173813819885, 0.14344893395900726, 0.0), 61 | (0.3959912657737732, 0.1842898577451706, 0.0), (0.3111281096935272, 0.21670186519622803, 0.0), 62 | (0.21430844068527222, 0.2407861351966858, 0.0), (0.10925304144620895, 0.2556171119213104, 0.0), 63 | (0.5551204085350037, 0.025475479662418365, 0.0), (0.5348702073097229, 0.07644488662481308, 0.0), 64 | (0.4915122985839844, 0.12226644158363342, 0.0), (0.4308139681816101, 0.1645427942276001, 0.0), 65 | (0.3535597026348114, 0.2004958689212799, 0.0), (0.2627182900905609, 0.22874398529529572, 0.0), 66 | (0.16178074479103088, 0.2482016384601593, 0.0), (0.05462652072310448, 0.2581210136413574, 0.0), 67 | (0.4683051109313965, 2.978201791847823e-07, 0.0), (0.46016228199005127, 0.03240064159035683, 0.0), 68 | (0.43346360325813293, 0.06059274449944496, 0.0), (0.38938117027282715, 0.08542850613594055, 0.0), 69 | (0.33114129304885864, 0.110313281416893, 0.0), (0.2601758539676666, 0.13103415071964264, 0.0), 70 | (0.17921197414398193, 0.1465274840593338, 0.0), (0.09136109799146652, 0.15609556436538696, 0.0), 71 | (0.4649912118911743, 0.015658844262361526, 0.0), (0.450432151556015, 0.04798557236790657, 0.0), 72 | (0.41178521513938904, 0.07347165793180466, 0.0), (0.3602612316608429, 0.09748400002717972, 0.0), 73 | (0.29565858840942383, 0.12036669254302979, 0.0), (0.21969391405582428, 0.13851231336593628, 0.0), 74 | (0.13528653979301453, 0.15106302499771118, 0.0), (0.04568054899573326, 0.15747304260730743, 0.0), 75 | (-0.560016930103302, 3.5614462490229926e-07, 0.0), (-0.5472726821899414, 0.051212746649980545, 0.0), 76 | (-0.5173879265785217, 0.09973714500665665, 0.0), (-0.46497008204460144, 0.14364132285118103, 0.0), 77 | (-0.3959912657737732, 0.1842898577451706, 0.0), (-0.3111281096935272, 0.21670186519622803, 0.0), 78 | (-0.21430844068527222, 0.2407861351966858, 0.0), (-0.10925304144620895, 0.2556171119213104, 0.0), 79 | (-0.5551204085350037, 0.025475479662418365, 0.0), (-0.5348702073097229, 0.07644488662481308, 0.0), 80 | (-0.4915122985839844, 0.12226644158363342, 0.0), (-0.4308139681816101, 0.1645427942276001, 0.0), 81 | (-0.3535597026348114, 0.2004958689212799, 0.0), (-0.2627182900905609, 0.22874398529529572, 0.0), 82 | (-0.16178074479103088, 0.2482016384601593, 0.0), (-0.05462652072310448, 0.2581210136413574, 0.0), 83 | (-0.4683051109313965, 2.978201791847823e-07, 0.0), (-0.46016228199005127, 0.03240064159035683, 0.0), 84 | (-0.43346360325813293, 0.06059274449944496, 0.0), (-0.38938117027282715, 0.08542850613594055, 0.0), 85 | (-0.33114129304885864, 0.110313281416893, 0.0), (-0.2601758539676666, 0.13103415071964264, 0.0), 86 | (-0.17921197414398193, 0.1465274840593338, 0.0), (-0.09136109799146652, 0.15609556436538696, 0.0), 87 | (-0.4649912118911743, 0.015658844262361526, 0.0), (-0.450432151556015, 0.04798557236790657, 0.0), 88 | (-0.41178521513938904, 0.07347165793180466, 0.0), (-0.3602612316608429, 0.09748400002717972, 0.0), 89 | (-0.29565858840942383, 0.12036669254302979, 0.0), (-0.21969391405582428, 0.13851231336593628, 0.0), 90 | (-0.13528653979301453, 0.15106302499771118, 0.0), (-0.04568054899573326, 0.15747304260730743, 0.0), 91 | (0.0, 0.26062488555908203, 0.0), (0.0, 0.15932999551296234, 0.0)], 92 | 'edges': [(8, 0), (9, 1), (10, 2), (11, 3), (12, 4), (13, 5), (14, 6), (15, 7), (1, 8), (2, 9), (3, 10), (4, 11), 93 | (5, 12), (6, 13), (7, 14), (64, 15), (24, 16), (25, 17), (26, 18), (27, 19), (28, 20), (29, 21), 94 | (30, 22), (31, 23), (17, 24), (18, 25), (19, 26), (20, 27), (21, 28), (22, 29), (23, 30), (65, 31), 95 | (0, 16), (24, 8), (1, 17), (25, 9), (40, 32), (41, 33), (42, 34), (43, 35), (44, 36), (45, 37), (46, 38), 96 | (47, 39), (33, 40), (34, 41), (35, 42), (36, 43), (37, 44), (38, 45), (39, 46), (64, 47), (56, 48), 97 | (57, 49), (58, 50), (59, 51), (60, 52), (61, 53), (62, 54), (63, 55), (49, 56), (50, 57), (51, 58), 98 | (52, 59), (53, 60), (54, 61), (55, 62), (65, 63), (32, 48), (56, 40), (33, 49), (57, 41)] 99 | } 100 | 101 | widgets['Root'] = { 102 | 'vertices': [(-0.5, -0.8844379782676697, 0.0), (-0.3844379782676697, -1.0, 0.0), (-0.4912033677101135, -0.9286617040634155, 0.0), 103 | (-0.4661526679992676, -0.9661527276039124, 0.0), (-0.42866164445877075, -0.9912034273147583, 0.0), (0.3844379782676697, -1.0, 0.0), 104 | (0.5, -0.8844379782676697, 0.0), (0.42866164445877075, -0.9912034273147583, 0.0), (0.4661526679992676, -0.9661527276039124, 0.0), 105 | (0.4912033677101135, -0.9286617040634155, 0.0), (-0.5, 0.8844379782676697, 0.0), (-0.3844379782676697, 1.0, 0.0), 106 | (-0.4912033677101135, 0.9286617040634155, 0.0), (-0.4661526679992676, 0.9661527276039124, 0.0), (-0.42866164445877075, 0.9912034273147583, 0.0), 107 | (0.5, 0.8844379782676697, 0.0), (0.3844379782676697, 1.0, 0.0), (0.4912033677101135, 0.9286617040634155, 0.0), 108 | (0.4661526679992676, 0.9661527276039124, 0.0), (0.42866164445877075, 0.9912034273147583, 0.0), (0.1234154999256134, -1.0, 0.0), 109 | (-0.1234154999256134, -1.0, 0.0), (0.1234154999256134, -1.0971899032592773, 0.0), (-0.1234154999256134, -1.0971899032592773, 0.0), 110 | (0.3031257688999176, -1.0971899032592773, 0.0), (-0.3031257688999176, -1.0971899032592773, 0.0), (0.0, -1.25, 0.0)], 111 | 'edges': [(0, 2), (2, 3), (3, 4), (4, 1), (5, 7), (7, 8), (8, 9), (9, 6), (10, 12), (12, 13), (13, 14), (14, 11), (15, 17), (17, 18), (18, 19), 112 | (19, 16), (0, 10), (6, 15), (11, 16), (20, 5), (21, 1), (22, 20), (23, 21), (24, 22), (25, 23), (26, 24), (26, 25)] 113 | } 114 | 115 | widgets['GroundSensor'] = { 116 | 'vertices': [(-0.5, -0.822191596031189, 0.0), (-0.32219159603118896, -1.0, 0.0), (-0.4761781692504883, -0.9110957980155945, 0.0), 117 | (-0.4110957980155945, -0.9761781692504883, 0.0), (0.32219159603118896, -1.0, 0.0), (0.5, -0.822191596031189, 0.0), 118 | (0.4110957980155945, -0.9761781692504883, 0.0), (0.47617819905281067, -0.9110957980155945, 0.0), (-0.5, 0.822191596031189, 0.0), 119 | (-0.32219159603118896, 1.0, 0.0), (-0.4761781692504883, 0.9110957980155945, 0.0), (-0.4110957980155945, 0.9761781692504883, 0.0), 120 | (0.5, 0.822191596031189, 0.0), (0.32219159603118896, 1.0, 0.0), (0.4761781692504883, 0.9110957980155945, 0.0), 121 | (0.4110957980155945, 0.9761781692504883, 0.0)], 122 | 'edges': [(0, 2), (2, 3), (3, 1), (4, 6), (6, 7), (7, 5), (8, 10), (10, 11), (11, 9), (12, 14), (14, 15), (15, 13), (0, 8), (1, 4), (5, 12), (9, 13)] 123 | } 124 | 125 | widgets['GroundSensor.Axle'] = { 126 | 'vertices': [(0.0, 0.5, 0.0), (-0.19134172797203064, 0.4619397521018982, 0.0), (-0.3535533845424652, 0.3535533845424652, 0.0), 127 | (-0.4619397521018982, 0.19134171307086945, 0.0), (-0.5, -2.1855694143368964e-08, 0.0), (-0.4619397521018982, -0.19134175777435303, 0.0), 128 | (-0.3535533845424652, -0.3535533845424652, 0.0), (-0.19134174287319183, -0.4619397521018982, 0.0), (-7.549790126404332e-08, -0.5, 0.0), 129 | (0.1913416087627411, -0.46193981170654297, 0.0), (0.35355329513549805, -0.35355350375175476, 0.0), (0.4619397521018982, -0.19134178757667542, 0.0), 130 | (0.5, 5.962440319251527e-09, 0.0), (0.4619397222995758, 0.1913418024778366, 0.0), (0.35355326533317566, 0.35355350375175476, 0.0), 131 | (0.19134148955345154, 0.46193987131118774, 0.0), (0.0, 0.2866188585758209, 0.0), (-0.1096842959523201, 0.2648012936115265, 0.0), 132 | (-0.2026701420545578, 0.2026701420545578, 0.0), (-0.2648012936115265, 0.1096842885017395, 0.0), (-0.2866188585758209, -1.2528508008813333e-08, 0.0), 133 | (-0.2648012936115265, -0.10968431085348129, 0.0), (-0.2026701420545578, -0.2026701420545578, 0.0), (-0.1096843034029007, -0.2648012936115265, 0.0), 134 | (-4.327824498773225e-08, -0.2866188585758209, 0.0), (0.10968422889709473, -0.2648013234138489, 0.0), 135 | (0.20267008244991302, -0.20267020165920258, 0.0), (0.2648012936115265, -0.10968433320522308, 0.0), 136 | (0.2866188585758209, 3.4178957442065894e-09, 0.0), (0.2648012638092041, 0.10968434065580368, 0.0), 137 | (0.20267006754875183, 0.20267020165920258, 0.0), (0.10968416184186935, 0.26480135321617126, 0.0), (-2.1639122493866125e-08, 0.0, 0.0)], 138 | 'edges': [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), (13, 12), (14, 13), (15, 14), (0, 15), 139 | (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), (23, 22), (24, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), 140 | (31, 30), (16, 31), (32, 24), (16, 32), (20, 32), (28, 32)] 141 | } 142 | 143 | widgets['Wheel'] = { 144 | 'vertices': [(0, 0.9999999403953552, -1.1874362826347351e-07), (0, 0.9807851910591125, 0.19509020447731018), 145 | (0, 0.9238794445991516, 0.38268333673477173), (0, 0.8314695358276367, 0.555570125579834), 146 | (0, 0.7071067094802856, 0.7071066498756409), (0, 0.555570125579834, 0.8314695358276367), 147 | (0, 0.3826833963394165, 0.9238793849945068), (0, 0.19509033858776093, 0.9807851314544678), 148 | (0, 7.549790836947068e-08, 0.9999998807907104), (0, -0.195090189576149, 0.9807851910591125), 149 | (0, -0.38268324732780457, 0.9238794445991516), (0, -0.555570125579834, 0.8314695358276367), 150 | (0, -0.7071067094802856, 0.7071066498756409), (0, -0.8314695954322815, 0.5555700659751892), 151 | (0, -0.9238795638084412, 0.3826831579208374), (0, -0.9807852506637573, 0.19508996605873108), 152 | (0, -0.9999998211860657, -4.445849981493666e-07), (0, -0.9807851314544678, -0.19509084522724152), 153 | (0, -0.9238792657852173, -0.38268399238586426), (0, -0.8314692378044128, -0.5555708408355713), 154 | (0, -0.7071062922477722, -0.7071073651313782), (0, -0.555569589138031, -0.8314701318740845), 155 | (0, -0.3826826512813568, -0.9238799810409546), (0, -0.1950894445180893, -0.9807855486869812), 156 | (0, 9.655991561885457e-07, -1.0000001192092896), (0, 0.1950913369655609, -0.9807851910591125), 157 | (0, 0.3826844394207001, -0.9238792061805725), (0, 0.5555711984634399, -0.8314690589904785), 158 | (0, 0.7071076035499573, -0.7071059942245483), (0, 0.8314703106880188, -0.5555692315101624), 159 | (0, 0.9238800406455994, -0.382682204246521), (0, 0.9807854890823364, -0.1950889378786087), 160 | (0, 0.9439931511878967, -1.099180622077256e-07), (0, 0.9258545637130737, 0.18416382372379303), 161 | (0, 0.8721359372138977, 0.36125046014785767), (0, 0.7849016189575195, 0.5244544148445129), 162 | (0, 0.6675039529800415, 0.667503833770752), (0, 0.5244544148445129, 0.7849015593528748), 163 | (0, 0.36125051975250244, 0.8721358180046082), (0, 0.18416395783424377, 0.9258544445037842), 164 | (0, 8.652769167838414e-08, 0.9439930319786072), (0, -0.18416379392147064, 0.925854504108429), 165 | (0, -0.3612503409385681, 0.8721358776092529), (0, -0.5244543552398682, 0.7849015593528748), 166 | (0, -0.6675038933753967, 0.667503833770752), (0, -0.7849015593528748, 0.5244543552398682), 167 | (0, -0.8721359372138977, 0.36125028133392334), (0, -0.9258545637130737, 0.18416360020637512), 168 | (0, -0.9439929723739624, -4.1751010826374113e-07), (0, -0.9258544445037842, -0.18416441977024078), 169 | (0, -0.8721356987953186, -0.3612510859966278), (0, -0.7849012613296509, -0.5244550704956055), 170 | (0, -0.6675034761428833, -0.6675045490264893), (0, -0.52445387840271, -0.7849021553993225), 171 | (0, -0.36124980449676514, -0.8721364140510559), (0, -0.18416307866573334, -0.9258548617362976), 172 | (0, 9.267772043131117e-07, -0.9439932703971863), (0, 0.18416491150856018, -0.925854504108429), 173 | (0, 0.36125150322914124, -0.8721356987953186), (0, 0.5244554281234741, -0.7849010825157166), 174 | (0, 0.6675047874450684, -0.6675032377243042), (0, 0.7849022746086121, -0.5244535803794861), 175 | (0, 0.8721364140510559, -0.3612493872642517), (0, 0.9258548021316528, -0.18416263163089752)], 176 | 'edges': [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), 177 | (13, 12), (14, 13), (15, 14), (16, 15), (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), 178 | (23, 22), (24, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), (31, 30), (0, 31), 179 | (33, 32), (34, 33), (35, 34), (36, 35), (37, 36), (38, 37), (39, 38), (40, 39), (41, 40), (42, 41), 180 | (43, 42), (44, 43), (45, 44), (46, 45), (47, 46), (48, 47), (49, 48), (50, 49), (51, 50), (52, 51), 181 | (53, 52), (54, 53), (55, 54), (56, 55), (57, 56), (58, 57), (59, 58), (60, 59), (61, 60), (62, 61), 182 | (63, 62), (32, 63)] 183 | } 184 | 185 | widgets['WheelBrake'] = { 186 | 'vertices': [(0, 0.8759009838104248, -1.0943974615429397e-07), (0, 0.8590707778930664, 0.17087969183921814), 187 | (0, 0.728285014629364, 0.48662444949150085), (0, 0.6193554997444153, 0.6193553805351257), 188 | (0, 0.48662441968917847, 0.7282849550247192), (0, 0.17087985575199127, 0.8590706586837769), 189 | (0, 1.0010809603500093e-07, 0.8759008646011353), (0, -0.17087966203689575, 0.8590707182884216), 190 | (0, -0.48662441968917847, 0.7282849550247192), (0, -0.6193554401397705, 0.6193553805351257), 191 | (0, -0.728285014629364, 0.4866243898868561), (0, -0.8590707182884216, 0.17087948322296143), 192 | (0, -0.8759008049964905, -3.948445055357297e-07), (0, -0.8590705990791321, -0.1708802729845047), 193 | (0, -0.7282846570014954, -0.4866250157356262), (0, -0.6193550825119019, -0.6193560361862183), 194 | (0, -0.48662394285202026, -0.7282854914665222), (0, -0.17087900638580322, -0.8590710163116455), 195 | (0, 8.797486543699051e-07, -0.8759011626243591), (0, 0.17088072001934052, -0.8590707182884216), 196 | (0, 0.4866253733634949, -0.7282845973968506), (0, 0.6193562746047974, -0.6193548440933228), 197 | (0, 0.7282857298851013, -0.486623615026474), (0, 0.8590710163116455, -0.170878604054451), 198 | (0, 0.7276514172554016, -1.0585000609353301e-07), (0, 0.7136697769165039, 0.14195764064788818), 199 | (0, 0.6050200462341309, 0.40426141023635864), (0, 0.5145272016525269, 0.5145270824432373), 200 | (0, 0.40426141023635864, 0.6050199270248413), (0, 0.14195780456066132, 0.7136696577072144), 201 | (0, 9.063110439910815e-08, 0.7276512980461121), (0, -0.141957625746727, 0.7136697173118591), 202 | (0, -0.40426141023635864, 0.6050199270248413), (0, -0.5145271420478821, 0.5145270824432373), 203 | (0, -0.6050199866294861, 0.40426138043403625), (0, -0.7136697173118591, 0.14195746183395386), 204 | (0, -0.7276512384414673, -3.429489368045324e-07), (0, -0.7136696577072144, -0.14195814728736877), 205 | (0, -0.6050196886062622, -0.40426191687583923), (0, -0.514526903629303, -0.5145276784896851), 206 | (0, -0.4042609930038452, -0.6050204634666443), (0, -0.141957089304924, -0.713670015335083), 207 | (0, 7.383145543826686e-07, -0.7276515960693359), (0, 0.14195851981639862, -0.7136697769165039), 208 | (0, 0.4042621850967407, -0.6050196886062622), (0, 0.5145279169082642, -0.5145267248153687), 209 | (0, 0.6050206422805786, -0.4042607545852661), (0, 0.713670015335083, -0.14195676147937775), 210 | (0, 0.695227324962616, -1.0113333814842917e-07), (0, 0.6818687319755554, 0.13563202321529388), 211 | (0, 0.5780603885650635, 0.38624757528305054), (0, 0.4915999174118042, 0.49159979820251465), 212 | (0, 0.38624757528305054, 0.5780603289604187), (0, 0.13563217222690582, 0.6818686127662659), 213 | (0, 8.659259265186847e-08, 0.6952272057533264), (0, -0.1356320083141327, 0.6818686723709106), 214 | (0, -0.38624757528305054, 0.5780603289604187), (0, -0.4915998578071594, 0.49159979820251465), 215 | (0, -0.5780603885650635, 0.38624754548072815), (0, -0.6818686723709106, 0.13563185930252075), 216 | (0, -0.6952271461486816, -3.276671804997022e-07), (0, -0.6818686127662659, -0.1356325000524521), 217 | (0, -0.5780600905418396, -0.38624805212020874), (0, -0.4915996491909027, -0.49160036444664), 218 | (0, -0.3862471580505371, -0.5780608057975769), (0, -0.1356315016746521, -0.6818689703941345), 219 | (0, 7.054153456920176e-07, -0.6952275037765503), (0, 0.13563285768032074, -0.6818687319755554), 220 | (0, 0.38624829053878784, -0.5780600905418396), (0, 0.4916006028652191, -0.4915994703769684), 221 | (0, 0.5780609846115112, -0.3862469494342804), (0, 0.6818689703941345, -0.13563118875026703), 222 | (0, 0.670251727104187, -9.750018392651327e-08), (0, 0.6573730111122131, 0.13075952231884003), 223 | (0, 0.5572939515113831, 0.3723718822002411), (0, 0.4739395081996918, 0.4739393889904022), 224 | (0, 0.3723718822002411, 0.5572938919067383), (0, 0.13075967133045197, 0.6573728919029236), 225 | (0, 8.348180813300132e-08, 0.6702516078948975), (0, -0.13075950741767883, 0.6573729515075684), 226 | (0, -0.3723718822002411, 0.5572938919067383), (0, -0.473939448595047, 0.4739393889904022), 227 | (0, -0.5572939515113831, 0.3723718523979187), (0, -0.6573729515075684, 0.1307593733072281), 228 | (0, -0.6702515482902527, -3.158959316351684e-07), (0, -0.6573728919029236, -0.13075998425483704), 229 | (0, -0.5572936534881592, -0.3723723292350769), (0, -0.4739392399787903, -0.4739399254322052), 230 | (0, -0.37237146496772766, -0.5572943091392517), (0, -0.13075903058052063, -0.6573732495307922), 231 | (0, 6.800737537560053e-07, -0.6702519059181213), (0, 0.1307603269815445, -0.6573730111122131), 232 | (0, 0.372372567653656, -0.5572936534881592), (0, 0.4739401638507843, -0.47393906116485596), 233 | (0, 0.557294487953186, -0.37237125635147095), (0, 0.6573732495307922, -0.13075871765613556)], 234 | 'edges': [(1, 0), (3, 2), (4, 3), (6, 5), (7, 6), (9, 8), (10, 9), (12, 11), (13, 12), (15, 14), (16, 15), 235 | (18, 17), (19, 18), (21, 20), (22, 21), (0, 23), (25, 24), (27, 26), (28, 27), (30, 29), (31, 30), 236 | (33, 32), (34, 33), (36, 35), (37, 36), (39, 38), (40, 39), (42, 41), (43, 42), (45, 44), (46, 45), 237 | (24, 47), (34, 10), (46, 22), (11, 35), (25, 1), (23, 47), (37, 13), (2, 26), (14, 38), (28, 4), 238 | (40, 16), (5, 29), (17, 41), (31, 7), (43, 19), (8, 32), (20, 44), (49, 48), (51, 50), (52, 51), 239 | (54, 53), (55, 54), (57, 56), (58, 57), (60, 59), (61, 60), (63, 62), (64, 63), (66, 65), (67, 66), 240 | (69, 68), (70, 69), (48, 71), (73, 72), (75, 74), (76, 75), (78, 77), (79, 78), (81, 80), (82, 81), 241 | (84, 83), (85, 84), (87, 86), (88, 87), (90, 89), (91, 90), (93, 92), (94, 93), (72, 95)] 242 | } 243 | 244 | widgets['Steering'] = { 245 | 'vertices': [(0.7296777367591858, 0.07034172862768173, 0.0), (0.057004380971193314, -0.07034172862768173, 0.0), (0.7296777367591858, -0.07034172862768173, 0.0), 246 | (0.057004380971193314, 0.07034172862768173, 0.0), (0.7296777367591858, 0.16664999723434448, 0.0), (0.7296777367591858, -0.16664999723434448, 0.0), 247 | (0.9998999834060669, 0.0, 0.0), (-0.7296777367591858, 0.07034172862768173, 0.0), (-0.057004380971193314, -0.07034172862768173, 0.0), 248 | (-0.7296777367591858, -0.07034172862768173, 0.0), (-0.057004380971193314, 0.07034172862768173, 0.0), (-0.7296777367591858, 0.16664999723434448, 0.0), 249 | (-0.7296777367591858, -0.16664999723434448, 0.0), (-0.9998999834060669, 0.0, 0.0)], 250 | 'edges': [(2, 1), (3, 0), (1, 3), (0, 4), (5, 2), (4, 6), (6, 5), (9, 8), (10, 7), (8, 10), (7, 11), (12, 9), (11, 13), (13, 12)] 251 | } 252 | 253 | widgets['Suspension'] = { 254 | 'vertices': [(-0.42728525400161743, -0.12928833067417145, -0.04404989629983902), (-0.06909304857254028, 0.2578587830066681, 0.0), 255 | (-0.13347753882408142, 0.23118986189365387, 0.0), (-0.1887657195329666, 0.1887657195329666, 0.0), 256 | (-0.23118992149829865, 0.13347753882408142, 0.0), (-0.2578587830066681, 0.06909307092428207, 0.0), 257 | (-0.42728525400161743, -0.06909316033124924, -0.025838270783424377), (-0.2578587830066681, -0.06909302622079849, 0.0), 258 | (-0.23118986189365387, -0.13347747921943665, 0.0), (-0.18876579403877258, -0.18876568973064423, 0.0), 259 | (-0.1334775984287262, -0.23118983209133148, 0.0), (-0.06909313797950745, -0.2578587532043457, 0.0), 260 | (-0.42728525400161743, 0.06909292191267014, -0.025838270783424377), (0.06909293681383133, -0.2578587830066681, 0.0), 261 | (0.13347743451595306, -0.23118995130062103, 0.0), (0.18876565992832184, -0.18876583874225616, 0.0), 262 | (0.23118983209133148, -0.1334775984287262, 0.0), (0.2578587532043457, -0.06909316033124924, 0.0), 263 | (0.42728525400161743, -0.06909316033124924, -0.025838270783424377), (0.2578587830066681, 0.06909292191267014, 0.0), 264 | (0.23118992149829865, 0.13347740471363068, 0.0), (0.18876585364341736, 0.18876561522483826, 0.0), 265 | (0.13347768783569336, 0.2311897873878479, 0.0), (0.0690932497382164, 0.2578587532043457, 0.0), 266 | (0.42728525400161743, 0.06909292191267014, -0.025838270783424377), (0.42728525400161743, -0.12928833067417145, -0.04404989629983902), 267 | (0.42728525400161743, 0.12928791344165802, -0.04404982924461365), (0.6011841893196106, -2.227646831443053e-07, -0.12004293501377106), 268 | (-0.42728525400161743, 0.12928791344165802, -0.04404982924461365), (-0.6011841893196106, -2.227646831443053e-07, -0.12004293501377106), 269 | (-0.06909316033124924, -0.42728525400161743, -0.025838270783424377), (0.06909292191267014, -0.42728525400161743, -0.025838270783424377), 270 | (-0.12928833067417145, -0.42728525400161743, -0.04404989629983902), (0.12928785383701324, -0.42728525400161743, -0.04404980689287186), 271 | (-2.545882011872891e-07, -0.6011841893196106, -0.12004290521144867), (-0.06909316033124924, 0.42728525400161743, -0.025838270783424377), 272 | (0.06909292191267014, 0.42728525400161743, -0.025838270783424377), (-0.12928833067417145, 0.42728525400161743, -0.04404989629983902), 273 | (0.12928785383701324, 0.42728525400161743, -0.04404982924461365), (-2.545882011872891e-07, 0.6011841893196106, -0.12004293501377106), 274 | (0.0, 0.1595769226551056, 0.0), (-0.0797884613275528, 0.13819767534732819, 0.0), (-0.13819767534732819, 0.079788438975811, 0.0), 275 | (-0.1595769226551056, -6.975329203129377e-09, 0.0), (-0.13819767534732819, -0.0797884613275528, 0.0), 276 | (-0.0797884613275528, -0.13819767534732819, 0.0), (-2.409544741510672e-08, -0.1595769226551056, 0.0), 277 | (0.07978841662406921, -0.13819767534732819, 0.0), (0.1381976306438446, -0.07978851348161697, 0.0), 278 | (0.1595769226551056, -7.418926628588451e-08, 0.0), (0.13819773495197296, 0.07978837937116623, 0.0), 279 | (0.07978855073451996, 0.1381976157426834, 0.0), (0.3002154231071472, 0.06909292191267014, -0.0018598437309265137), 280 | (0.34257203340530396, 0.06909292191267014, -0.007112756371498108), (0.3849286139011383, 0.06909292191267014, -0.015268802642822266), 281 | (0.3849286139011383, -0.06909316033124924, -0.015268847346305847), (0.34257200360298157, -0.06909316033124924, -0.00711272656917572), 282 | (0.30021539330482483, -0.06909316033124924, -0.0018598437309265137), (-0.3002154231071472, 0.06909303367137909, -0.0018598437309265137), 283 | (-0.34257203340530396, 0.0690929964184761, -0.007112756371498108), (-0.3849286139011383, 0.06909295171499252, -0.015268802642822266), 284 | (-0.3849286139011383, -0.06909313052892685, -0.015268802642822266), (-0.34257200360298157, -0.06909309327602386, -0.007112711668014526), 285 | (-0.30021539330482483, -0.06909305602312088, -0.0018598586320877075), (-0.06909314543008804, -0.30021539330482483, -0.0018598437309265137), 286 | (-0.06909314543008804, -0.34257200360298157, -0.00711272656917572), (-0.06909315288066864, -0.3849286139011383, -0.015268847346305847), 287 | (0.06909293681383133, -0.3002154231071472, -0.0018598437309265137), (0.06909292936325073, -0.34257203340530396, -0.007112756371498108), 288 | (0.06909292191267014, -0.3849286139011383, -0.015268802642822266), (-0.06909307837486267, 0.3002154231071472, -0.0018598437309265137), 289 | (-0.06909310817718506, 0.34257203340530396, -0.007112756371498108), (-0.06909313052892685, 0.3849286139011383, -0.015268802642822266), 290 | (0.06909316033124924, 0.30021539330482483, -0.0018598437309265137), (0.06909307837486267, 0.34257200360298157, -0.00711272656917572), 291 | (0.0690930038690567, 0.3849286139011383, -0.015268847346305847)], 292 | 'edges': [(2, 1), (3, 2), (4, 3), (5, 4), (12, 28), (0, 6), (8, 7), (9, 8), (10, 9), (11, 10), (28, 29), (29, 0), (14, 13), (15, 14), (16, 15), 293 | (17, 16), (54, 24), (57, 17), (20, 19), (21, 20), (22, 21), (23, 22), (60, 12), (25, 18), (24, 26), (27, 25), (26, 27), (63, 7), 294 | (32, 30), (31, 33), (34, 32), (33, 34), (66, 30), (69, 31), (37, 35), (36, 38), (39, 37), (38, 39), (72, 35), (75, 36), (41, 40), 295 | (42, 41), (43, 42), (44, 43), (45, 44), (46, 45), (47, 46), (48, 47), (49, 48), (50, 49), (51, 50), (40, 51), (19, 52), (52, 53), 296 | (53, 54), (18, 55), (55, 56), (56, 57), (5, 58), (58, 59), (59, 60), (6, 61), (61, 62), (62, 63), (11, 64), (64, 65), (65, 66), 297 | (13, 67), (67, 68), (68, 69), (1, 70), (70, 71), (71, 72), (23, 73), (73, 74), (74, 75)] 298 | } 299 | 300 | widgets['WheelDamper'] = { 301 | 'vertices': [(-0.17770397663116455, -0.09192684292793274, 0.14030149579048157), (-0.17770397663116455, -0.09192684292793274, 0.23383590579032898), 302 | (-0.10793274641036987, -0.16846297681331635, 0.13250692188739777), (-0.10793278366327286, -0.16846297681331635, 0.22604137659072876), 303 | (-0.009241040796041489, -0.1998595893383026, 0.12471239268779755), (-0.009241056628525257, -0.19985966384410858, 0.21824686229228973), 304 | (0.09192679077386856, -0.17770402133464813, 0.11691785603761673), (0.09192682057619095, -0.17770402133464813, 0.21045231819152832), 305 | (0.16846293210983276, -0.10793281346559525, 0.1091233491897583), (0.16846293210983276, -0.10793281346559525, 0.2026577889919281), 306 | (0.1998595893383026, -0.009241072461009026, 0.10132881999015808), (0.1998595893383026, -0.009241089224815369, 0.19486328959465027), 307 | (0.17770397663116455, 0.09192679077386856, 0.09353431314229965), (0.1777040809392929, 0.09192677587270737, 0.18706879019737244), 308 | (0.10793274641036987, 0.16846294701099396, 0.08573977649211884), (0.10793281346559525, 0.16846294701099396, 0.17927424609661102), 309 | (0.009240993298590183, 0.19985954463481903, 0.0779452696442604), (0.009241056628525257, 0.1998595893383026, 0.1714797168970108), 310 | (-0.09192684292793274, 0.17770391702651978, 0.07015073299407959), (-0.09192682057619095, 0.17770402133464813, 0.16368518769741058), 311 | (-0.16846294701099396, 0.10793264955282211, 0.06235618144273758), (-0.16846297681331635, 0.10793274641036987, 0.15589064359664917), 312 | (-0.19985954463481903, 0.009240960702300072, 0.05456162989139557), (-0.1998595893383026, 0.00924102496355772, 0.14809606969356537), 313 | (-0.17770391702651978, -0.09192687273025513, 0.04676707834005356), (-0.17770402133464813, -0.09192684292793274, 0.14030154049396515), 314 | (-0.1079326793551445, -0.16846294701099396, 0.03897252678871155), (-0.10793278366327286, -0.16846302151679993, 0.13250696659088135), 315 | (-0.00924097653478384, -0.19985954463481903, 0.03117799013853073), (-0.009241056628525257, -0.19985966384410858, 0.12471242249011993), 316 | (0.09192684292793274, -0.17770391702651978, 0.02338346280157566), (0.09192682057619095, -0.1777040809392929, 0.11691789329051971), 317 | (0.16846293210983276, -0.1079326942563057, 0.015588936395943165), (0.16846293210983276, -0.10793283581733704, 0.10912337899208069), 318 | (0.19985954463481903, -0.009240993298590183, 0.0077944230288267136), (0.1998595893383026, -0.009241105057299137, 0.10132886469364166), 319 | (0.17770391702651978, 0.09192683547735214, -9.42906623890849e-08), (0.1777040809392929, 0.09192677587270737, 0.09353433549404144), 320 | (0.1079326793551445, 0.16846290230751038, -0.007794610224664211), (0.10793281346559525, 0.16846294701099396, 0.08573982864618301), 321 | (0.00924097653478384, 0.19985945522785187, -0.015589137561619282), (0.009241056628525257, 0.1998595893383026, 0.0779452919960022), 322 | (-0.09192682057619095, 0.177703857421875, -0.023383673280477524), (-0.09192682057619095, 0.17770402133464813, 0.07015074789524078), 323 | (-0.16846290230751038, 0.10793262720108032, -0.031178224831819534), (-0.16846294701099396, 0.10793274641036987, 0.06235621124505997), 324 | (-0.19985945522785187, 0.009240944869816303, -0.03897276148200035), (-0.1998595893383026, 0.00924102496355772, 0.05456166714429855), 325 | (-0.177703857421875, -0.09192684292793274, -0.046767283231019974), (-0.17770402133464813, -0.09192684292793274, 0.04676711559295654), 326 | (-0.10793262720108032, -0.16846290230751038, -0.054561812430620193), (-0.10793278366327286, -0.16846302151679993, 0.038972560316324234), 327 | (-0.009240960702300072, -0.19985948503017426, -0.062356337904930115), (-0.00924102496355772, -0.19985966384410858, 0.031178021803498268), 328 | (0.09192684292793274, -0.177703857421875, -0.07015086710453033), (0.09192683547735214, -0.17770402133464813, 0.023383498191833496), 329 | (0.16846293210983276, -0.10793262720108032, -0.07794538885354996), (0.16846302151679993, -0.10793278366327286, 0.015588970854878426), 330 | (0.19985945522785187, -0.009240944869816303, -0.08573991060256958), (0.19985966384410858, -0.009241040796041489, 0.007794454228132963), 331 | (0.1777038276195526, 0.09192683547735214, -0.09353446215391159), (0.1777040809392929, 0.09192684292793274, -6.286042264491698e-08), 332 | (0.10793259739875793, 0.1684628427028656, -0.10132896900177002), (0.10793281346559525, 0.16846302151679993, -0.007794579025357962), 333 | (0.00924092996865511, 0.1998593956232071, -0.10912348330020905), (0.009241040796041489, 0.19985966384410858, -0.015589105896651745), 334 | (-0.09192682057619095, 0.17770376801490784, -0.11691801995038986), (-0.09192684292793274, 0.17770405113697052, -0.023383641615509987), 335 | (-0.1684628427028656, 0.10793255269527435, -0.12471257150173187), (-0.16846302151679993, 0.10793274641036987, -0.031178191304206848), 336 | (-0.1998593509197235, 0.009240913204848766, -0.1325071007013321), (-0.19985966384410858, 0.009240993298590183, -0.03897274285554886), 337 | (-0.17770376801490784, -0.09192683547735214, -0.1403016299009323), (-0.17770402133464813, -0.09192688763141632, -0.046767283231019974), 338 | (-0.10793255269527435, -0.1684628427028656, -0.14809612929821014), (-0.10793274641036987, -0.16846303641796112, -0.054561812430620193), 339 | (-0.009240896441042423, -0.1998593509197235, -0.15589067339897156), (-0.009240993298590183, -0.19985966384410858, -0.062356337904930115), 340 | (0.09192683547735214, -0.17770375311374664, -0.16368518769741058), (0.09192688763141632, -0.17770402133464813, -0.07015086710453033), 341 | (0.1684628129005432, -0.10793253034353256, -0.1714797168970108), (0.16846302151679993, -0.10793274641036987, -0.07794538885354996), 342 | (0.1998593509197235, -0.009240913204848766, -0.17927424609661102), (0.1998595893383026, -0.009241009131073952, -0.08573991060256958), 343 | (0.17770375311374664, 0.09192679077386856, -0.18706879019737244), (0.17770397663116455, 0.09192684292793274, -0.09353446215391159), 344 | (0.10793255269527435, 0.1684628278017044, -0.19486330449581146), (0.10793274641036987, 0.16846294701099396, -0.10132896900177002), 345 | (0.00924092996865511, 0.1998593509197235, -0.20265783369541168), (0.00924102496355772, 0.19985954463481903, -0.10912348330020905), 346 | (-0.09192678332328796, 0.17770370841026306, -0.2104523628950119), (-0.09192679077386856, 0.17770394682884216, -0.11691801995038986), 347 | (-0.1684628129005432, 0.10793255269527435, -0.21824689209461212), (-0.16846290230751038, 0.10793272405862808, -0.12471257150173187), 348 | (-0.19985929131507874, 0.00924092996865511, -0.22604137659072876), (-0.19985954463481903, 0.00924102496355772, -0.1325071007013321), 349 | (-0.17770370841026306, -0.09192678332328796, -0.23383590579032898), (-0.17770391702651978, -0.09192680567502975, -0.1403016299009323), 350 | (-0.21371114253997803, -0.21371114253997803, 0.24772925674915314), (0.21371114253997803, -0.21371114253997803, 0.24772925674915314), 351 | (-0.21371114253997803, 0.21371114253997803, 0.24772925674915314), (0.21371114253997803, 0.21371114253997803, 0.24772925674915314), 352 | (-0.21371114253997803, -0.21371114253997803, -0.24772925674915314), (0.21371114253997803, -0.21371114253997803, -0.24772925674915314), 353 | (-0.21371114253997803, 0.21371114253997803, -0.24772925674915314), (0.21371114253997803, 0.21371114253997803, -0.24772925674915314)], 354 | 'edges': [(1, 3), (2, 0), (3, 5), (4, 2), (5, 7), (6, 4), (7, 9), (8, 6), (9, 11), (10, 8), (11, 13), (12, 10), (13, 15), (14, 12), 355 | (15, 17), (16, 14), (17, 19), (18, 16), (19, 21), (20, 18), (21, 23), (22, 20), (23, 25), (24, 22), (25, 27), (26, 24), 356 | (27, 29), (28, 26), (29, 31), (30, 28), (31, 33), (32, 30), (33, 35), (34, 32), (35, 37), (36, 34), (37, 39), (38, 36), 357 | (39, 41), (40, 38), (41, 43), (42, 40), (43, 45), (44, 42), (45, 47), (46, 44), (47, 49), (48, 46), (49, 51), (50, 48), 358 | (51, 53), (52, 50), (53, 55), (54, 52), (55, 57), (56, 54), (57, 59), (58, 56), (59, 61), (60, 58), (61, 63), (62, 60), 359 | (63, 65), (64, 62), (65, 67), (66, 64), (67, 69), (68, 66), (69, 71), (70, 68), (71, 73), (72, 70), (73, 75), (74, 72), 360 | (75, 77), (76, 74), (77, 79), (78, 76), (79, 81), (80, 78), (81, 83), (82, 80), (83, 85), (84, 82), (85, 87), (86, 84), 361 | (87, 89), (88, 86), (89, 91), (90, 88), (91, 93), (92, 90), (93, 95), (94, 92), (95, 97), (96, 94), (100, 98), (98, 99), 362 | (99, 101), (101, 100), (104, 102), (102, 103), (103, 105), (105, 104)] 363 | } 364 | 365 | return widgets 366 | 367 | if __name__ == "__main__": 368 | create() 369 | --------------------------------------------------------------------------------