├── .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 | [](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 |
--------------------------------------------------------------------------------