├── .github
└── workflows
│ └── BuildRelease.yml
├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── batch_render.py
├── blender_manifest.toml
├── camera_controlls.py
├── camera_gizmos.py
├── dolly_zoom_modal.py
├── keymap.py
├── pie_menu.py
├── preferences.py
├── ui.py
├── ui_helpers.py
└── uilist.py
/.github/workflows/BuildRelease.yml:
--------------------------------------------------------------------------------
1 | name: Build Release
2 |
3 | on:
4 | push:
5 | tags: [ "v[0-9]+.[0-9]+.[0-9]+" ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | path: ${{ github.event.repository.name }}
14 |
15 | - name: Zip Repository (excludes .git*)
16 | run: |
17 | version_with_underscores=$(echo ${{ github.ref_name }} | tr '.' '_')
18 | zip -r simple_camera_manager_${version_with_underscores}.zip \
19 | ${{ github.event.repository.name }} \
20 | -x "${{ github.event.repository.name }}/.git*"
21 |
22 | - name: Create versioned build with filtered zip file
23 | run: |
24 | version_with_underscores=$(echo ${{ github.ref_name }} | tr '.' '_')
25 | cd ${{ github.event.repository.name }}
26 | gh release create ${{ github.ref_name }} --generate-notes \
27 | ../simple_camera_manager_${version_with_underscores}.zip
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.pyc
3 | *.mp4
4 | *.json
5 |
6 | __pycache__/
7 | cameramanager_updater/
8 | cam-manager_updater/
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Camera Manager
2 |
3 | [](https://www.paypal.com/donate/?hosted_button_id=Q6PL92LJX7836)
4 |
5 |
6 | Cam-Manager is an add-on to improve efficiency and organization with multiple cameras. It empowers you to quickly present your work from product design to modeling/texturing work. It is a powerful tool to tweak the composition and framing of your artwork or when working with image planes. The addon includes different overview panels (3D view and Scene properties), a pie menu for quickly adjusting settings on the active camera, different tools to assign and adjust camera-specific resolution, world materials, render slots, and more. Further, it allows you to customize the shortcuts to your needs.
7 | >⚠️
8 | **This addon does not provide special tools for animation.** It is designed for static cameras, working with image planes, setting up the framing and composition as well as quickly presenting your work with static images.
9 | ## Getting the addon:
10 | Support me by purchasing the addon from [Superhive](https://superhivemarket.com/products/simple-camera-manager). You are permitted to get a copy from this GitHub page and everything specified in the attached license (GPL 3.0). Creating addons is a lot of effort and work. The licensing and contribution of my future addons will also depend on the fairness of all of you.
11 |
12 | ## Contribution
13 | The best and easiest way to contribute is to share your experiences and use cases with me. Seeing how you work, helps me to understand what you need.
14 |
15 | Pull requests are also very welcome. Feel free to contact me before working on your changes and making a pull request. Let's see if the proposed changes fit the overall design and purpose of this addon. I will be strict in keeping a consistent user experience and vision for this addon.
16 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # support reloading sub-modules
2 | if "bpy" in locals():
3 | import importlib
4 |
5 | importlib.reload(camera_controlls)
6 | importlib.reload(ui_helpers)
7 | importlib.reload(dolly_zoom_modal)
8 | importlib.reload(camera_gizmos)
9 | importlib.reload(batch_render)
10 | importlib.reload(keymap)
11 | importlib.reload(preferences)
12 | importlib.reload(ui)
13 | importlib.reload(uilist)
14 | importlib.reload(pie_menu)
15 |
16 |
17 | else:
18 | from . import camera_controlls
19 | from . import ui_helpers
20 | from . import dolly_zoom_modal
21 | from . import batch_render
22 | from . import camera_gizmos
23 | from . import ui
24 | from . import uilist
25 | from . import keymap
26 | from . import preferences
27 | from . import pie_menu
28 |
29 |
30 | files = [
31 | camera_controlls,
32 | ui_helpers,
33 | dolly_zoom_modal,
34 | batch_render,
35 | ui,
36 | uilist,
37 | pie_menu,
38 | camera_gizmos,
39 | # keymap and preferences should be last
40 | keymap,
41 | preferences
42 | ]
43 |
44 |
45 | def register():
46 | for file in files:
47 | file.register()
48 |
49 |
50 | def unregister():
51 | for file in reversed(files):
52 | file.unregister()
--------------------------------------------------------------------------------
/batch_render.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class CAM_MANAGER_BaseOperator:
4 | # Class variables to store the state of the operator
5 | _cameras = [] # List of cameras to render
6 | _original_camera = None # Original camera to restore after rendering
7 | _original_output_path = None # Original output path to restore after rendering
8 | _current_index = 0 # Index of the current camera being rendered
9 | _rendering = False # Flag to indicate if rendering is in progress
10 |
11 | def set_camera_settings(self, context, camera):
12 | """Set the active camera to the specified camera."""
13 | print(f"Setting camera settings for: {camera.name}")
14 | bpy.ops.cam_manager.change_scene_camera(camera_name=camera.name, switch_to_cam=True)
15 |
16 | def cleanup(self, context, aborted=False):
17 | """Restore the original camera and output path, and remove handlers."""
18 | scene = context.scene
19 | print("Cleanup called")
20 |
21 | # Restore the original camera and output path
22 | if self._original_camera:
23 | bpy.ops.cam_manager.change_scene_camera(camera_name=self._original_camera.name, switch_to_cam=True)
24 | if self._original_output_path:
25 | scene.render.filepath = self._original_output_path
26 |
27 | # Remove the render complete handler to avoid memory leaks or crashes
28 | if self.render_complete_handler in bpy.app.handlers.render_complete:
29 | bpy.app.handlers.render_complete.remove(self.render_complete_handler)
30 |
31 | # Report the status of the rendering process
32 | if aborted:
33 | self.report({'INFO'}, "Rendering aborted")
34 | else:
35 | self.report({'INFO'}, f"Rendering completed. {self._current_index} cameras rendered.")
36 |
37 | def initialize(self, context):
38 | """Initialize the operator by setting up the cameras and original settings."""
39 | scene = context.scene
40 | self._original_camera = scene.camera
41 | self._original_output_path = scene.render.filepath
42 | self._cameras = [obj for obj in bpy.data.objects if
43 | obj.type == 'CAMERA' and getattr(obj.data, "render_selected", False)]
44 |
45 | if not self._cameras:
46 | self.report({'ERROR'}, "No cameras selected for rendering")
47 | return False
48 |
49 | print(f"Cameras to render: {[cam.name for cam in self._cameras]}")
50 | self.report({'INFO'}, f"Cameras to render: {[cam.name for cam in self._cameras]}")
51 |
52 | return True
53 |
54 | class CAM_MANAGER_OT_multi_camera_rendering_modal(CAM_MANAGER_BaseOperator, bpy.types.Operator):
55 | """Render all selected cameras using modal operator"""
56 | bl_idname = "cam_manager.multi_camera_rendering_modal"
57 | bl_label = "Render All Selected Cameras (Modal)"
58 | bl_options = {'REGISTER', 'UNDO'}
59 |
60 | def modal(self, context, event):
61 | """Modal function to handle events and rendering process."""
62 | if event.type == 'TIMER':
63 | if not self._rendering:
64 | if self._current_index < len(self._cameras):
65 | # Set camera settings for the next render
66 | camera = self._cameras[self._current_index]
67 | self.set_camera_settings(context, camera)
68 | # Start the next render
69 | print(f"Starting render for camera: {camera.name}")
70 | self.report({'INFO'}, f"Starting render for camera: {camera.name}")
71 | self._rendering = True
72 | bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, use_viewport=False)
73 | else:
74 | self.cleanup(context)
75 | return {'FINISHED'}
76 | elif event.type == 'ESC':
77 | self.cleanup(context, aborted=True)
78 | return {'CANCELLED'}
79 |
80 | return {'PASS_THROUGH'}
81 |
82 | def execute(self, context):
83 | """Execute the operator and start the rendering process."""
84 | if not self.initialize(context):
85 | return {'CANCELLED'}
86 |
87 | # Register the render complete handler
88 | bpy.app.handlers.render_complete.append(self.render_complete_handler)
89 |
90 | # Start the modal operator
91 | context.window_manager.modal_handler_add(self)
92 | self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
93 |
94 | # Set camera settings for the first render
95 | if self._cameras:
96 | first_camera = self._cameras[0]
97 | self.set_camera_settings(context, first_camera)
98 |
99 | # Start the first render
100 | print(f"Starting first render for camera: {first_camera.name}")
101 | self.report({'INFO'}, f"Starting first render for camera: {first_camera.name}")
102 | self._rendering = True
103 | bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, use_viewport=False)
104 |
105 | # Instruct the user to open the console for detailed feedback
106 | self.report({'INFO'}, "Open the console (Window > Toggle System Console) to see detailed feedback.")
107 |
108 | return {'RUNNING_MODAL'}
109 |
110 | def render_complete_handler(self, scene, depsgraph):
111 | """Handler called when a render is complete."""
112 | print(f"Render complete for camera: {self._cameras[self._current_index].name}")
113 | self.report({'INFO'}, f"Render complete for camera: {self._cameras[self._current_index].name}")
114 | self._rendering = False
115 | self._current_index += 1
116 |
117 | if self._current_index < len(self._cameras):
118 | # Add a delay before starting the next render to ensure the previous render is fully completed
119 | bpy.app.timers.register(self.start_next_render, first_interval=1.0)
120 | else:
121 | # Add a delay before final cleanup to ensure the last render is fully completed
122 | bpy.app.timers.register(lambda: self.final_cleanup(bpy.context), first_interval=2.0)
123 |
124 | def start_next_render(self):
125 | """Start the next render after a delay."""
126 | if self._current_index < len(self._cameras):
127 | # Set camera settings for the next render
128 | camera = self._cameras[self._current_index]
129 | self.set_camera_settings(bpy.context, camera)
130 | print(f"Setting camera: {camera.name}")
131 | self.report({'INFO'}, f"Setting camera: {camera.name}")
132 | # Trigger the next render
133 | self._rendering = True
134 | bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, use_viewport=False)
135 |
136 | def final_cleanup(self, context):
137 | """Perform final cleanup after all renders are complete."""
138 | self.cleanup(context)
139 | # Ensure the handler is removed after the last render to avoid memory leaks or crashes
140 | if self.render_complete_handler in bpy.app.handlers.render_complete:
141 | bpy.app.handlers.render_complete.remove(self.render_complete_handler)
142 |
143 | class CAM_MANAGER_OT_multi_camera_rendering_handlers(CAM_MANAGER_BaseOperator, bpy.types.Operator):
144 | """Render all selected cameras using handlers"""
145 | bl_idname = "cam_manager.multi_camera_rendering_handlers"
146 | bl_label = "Render All Selected Cameras (Handlers)"
147 | bl_options = {'REGISTER', 'UNDO'}
148 |
149 | def render_complete_handler(self, scene, depsgraph):
150 | """Handler called when a render is complete."""
151 | print(f"Render complete for camera: {self._cameras[self._current_index].name}")
152 | self.report({'INFO'}, f"Render complete for camera: {self._cameras[self._current_index].name}")
153 | self._rendering = False
154 | self._current_index += 1
155 |
156 | if self._current_index < len(self._cameras):
157 | # Add a delay before starting the next render to ensure the previous render is fully completed
158 | bpy.app.timers.register(self.start_next_render, first_interval=1.0)
159 | else:
160 | # Add a delay before final cleanup to ensure the last render is fully completed
161 | bpy.app.timers.register(lambda: self.final_cleanup(bpy.context), first_interval=2.0)
162 |
163 | def start_next_render(self):
164 | """Start the next render after a delay."""
165 | if self._current_index < len(self._cameras):
166 | # Set camera settings for the next render
167 | camera = self._cameras[self._current_index]
168 | self.set_camera_settings(bpy.context, camera)
169 | print(f"Setting camera: {camera.name}")
170 | self.report({'INFO'}, f"Setting camera: {camera.name}")
171 | # Trigger the next render
172 | self._rendering = True
173 | bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, use_viewport=False)
174 |
175 | def final_cleanup(self, context):
176 | """Perform final cleanup after all renders are complete."""
177 | self.cleanup(context)
178 | # Ensure the handler is removed after the last render to avoid memory leaks or crashes
179 | if self.render_complete_handler in bpy.app.handlers.render_complete:
180 | bpy.app.handlers.render_complete.remove(self.render_complete_handler)
181 |
182 | def execute(self, context):
183 | """Execute the operator and start the rendering process."""
184 | if not self.initialize(context):
185 | return {'CANCELLED'}
186 |
187 | # Register the render complete handler
188 | bpy.app.handlers.render_complete.append(self.render_complete_handler)
189 |
190 | try:
191 | # Set camera settings for the first render
192 | if self._cameras:
193 | first_camera = self._cameras[0]
194 | self.set_camera_settings(context, first_camera)
195 |
196 | # Start the first render
197 | print(f"Starting first render for camera: {first_camera.name}")
198 | self.report({'INFO'}, f"Starting first render for camera: {first_camera.name}")
199 | self._rendering = True
200 | bpy.ops.render.render('INVOKE_DEFAULT', write_still=True, use_viewport=False)
201 | except Exception as e:
202 | self.report({'ERROR'}, f"Rendering failed: {e}")
203 | self.cleanup(context, aborted=True)
204 | return {'CANCELLED'}
205 |
206 | return {'RUNNING_MODAL'}
207 |
208 | classes = (
209 | CAM_MANAGER_OT_multi_camera_rendering_modal,
210 | CAM_MANAGER_OT_multi_camera_rendering_handlers,
211 | )
212 |
213 | def register():
214 | """Register the operators with Blender."""
215 | from bpy.utils import register_class
216 |
217 | for cls in classes:
218 | register_class(cls)
219 |
220 | def unregister():
221 | """Unregister the operators from Blender."""
222 | from bpy.utils import unregister_class
223 |
224 | for cls in reversed(classes):
225 | unregister_class(cls)
226 |
--------------------------------------------------------------------------------
/blender_manifest.toml:
--------------------------------------------------------------------------------
1 | schema_version = "1.0.0"
2 |
3 | id = "simple_camera_manager"
4 | version = "1.6.0"
5 | name = "Simple Camera Manager"
6 | tagline = "Simple Camera Manager is a simple tools for managing multiple cameras"
7 | maintainer = "Matthias Patscheider "
8 | type = "add-on"
9 |
10 | website = "https://weisl.github.io/camera_manager_Overview/"
11 |
12 | tags = ["3D View", "Camera", "User Interface"]
13 |
14 | blender_version_min = "4.2.0"
15 |
16 | license = [
17 | "SPDX:GPL-3.0-or-later",
18 | ]
19 |
20 | copyright = [
21 | "2023 Matthias Patscheider",
22 | ]
23 |
24 | [permissions]
25 | files = "Write Renders to disk"
26 |
27 |
28 | [build]
29 | paths_exclude_pattern = [
30 | "__pycache__/",
31 | "/.git/",
32 | "/*.zip",
33 | ]
34 |
--------------------------------------------------------------------------------
/camera_controlls.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 |
4 | cam_collection_name = 'Cameras'
5 |
6 |
7 | def make_collection(collection_name, parent_collection):
8 | """
9 | return existing collection if a collection with the according name exists, otherwise return a newly created one
10 |
11 | :param collection_name: name of the newly created collection
12 | :param parent_collection: parent collection of the newly created collection
13 | :return: the newly created collection
14 | """
15 |
16 | if collection_name in bpy.data.collections:
17 | col = bpy.data.collections[collection_name]
18 | else:
19 | col = bpy.data.collections.new(collection_name)
20 | parent_collection.children.link(col)
21 | return col
22 |
23 |
24 | def moveToCollection(ob, collection):
25 | """
26 | Move an object to another scene collection
27 | :param ob: object to move
28 | :param collection: collection the object is moved to
29 | :return: the input object
30 | """
31 | ob_old_coll = ob.users_collection # list of all collection the obj is in
32 | for col in ob_old_coll: # unlink from all precedent obj collections
33 | col.objects.unlink(ob)
34 | if ob.name not in collection.objects:
35 | collection.objects.link(ob)
36 | return ob
37 |
38 |
39 | def cycleCamera(context, direction):
40 | """
41 | Change the active camera to the previous or next camera one in the camera list
42 | :param context:
43 | :param direction: string with 'FORWARD' or 'BACKWARD' to define the direction
44 | :return: Bool for either successful or unsuccessful try
45 | """
46 |
47 | scene = context.scene
48 | cam_objects = [ob for ob in scene.objects if ob.type == 'CAMERA']
49 |
50 | if len(cam_objects) == 0:
51 | return False
52 |
53 | try:
54 | idx = cam_objects.index(scene.camera)
55 | new_idx = (idx + 1 if direction == 'FORWARD' else idx - 1) % len(cam_objects)
56 | except ValueError:
57 | new_idx = 0
58 |
59 | bpy.ops.cam_manager.change_scene_camera(camera_name=cam_objects[new_idx].name)
60 | # scene.camera = cam_objects[new_idx]
61 | return True
62 |
63 |
64 | def lock_camera(obj, lock):
65 | """ Locks or unlocks all transformation attributes of the camera. It further adds a custom property
66 | :param obj: object to lock/unlock
67 | :param lock: bool, defining if locking or unlocking
68 | :return: None
69 | """
70 | obj.lock_location[0] = lock
71 | obj.lock_location[1] = lock
72 | obj.lock_location[2] = lock
73 |
74 | obj.lock_rotation[0] = lock
75 | obj.lock_rotation[1] = lock
76 | obj.lock_rotation[2] = lock
77 |
78 | obj.lock_scale[0] = lock
79 | obj.lock_scale[1] = lock
80 | obj.lock_scale[2] = lock
81 | obj['lock'] = lock
82 |
83 |
84 | class OBJECT_OT_create_camera_from_view(bpy.types.Operator):
85 | """Create a new camera matching the current viewport view"""
86 | bl_idname = "camera.create_camera_from_view"
87 | bl_label = "Create Camera from View"
88 | bl_options = {'REGISTER', 'UNDO'}
89 |
90 | def execute(self, context):
91 | # Create a new camera
92 | bpy.ops.object.camera_add()
93 | camera = context.object # Ensure we get the newly created camera
94 | camera.name = 'ViewportCamera'
95 | print(f"New camera created: {camera.name} at location {camera.location}")
96 |
97 | # Store the original active camera
98 | original_camera = context.scene.camera
99 | print(f"Original active camera: {original_camera.name if original_camera else 'None'}")
100 |
101 | # Set the new camera as the active scene camera
102 | context.scene.camera = camera
103 | print(f"Set active scene camera to: {context.scene.camera.name}")
104 |
105 | # Get the 3D view area and region
106 | for area in bpy.context.screen.areas:
107 | if area.type == 'VIEW_3D':
108 | for region in area.regions:
109 | if region.type == 'WINDOW':
110 | # Align the new camera to the viewport view
111 | with context.temp_override(area=area, region=region, scene=context.scene):
112 | bpy.ops.view3d.camera_to_view()
113 | print(f"New camera aligned to viewport view. Location: {camera.location}")
114 | break
115 |
116 | # Optionally, set the camera's lens to match the viewport
117 | camera.data.lens = context.space_data.lens
118 | print(f"Camera lens set to: {camera.data.lens}")
119 |
120 | # Restore the original active camera
121 | # context.scene.camera = original_camera
122 | # print(f"Restored active scene camera to: {context.scene.camera.name if context.scene.camera else 'None'}")
123 |
124 | self.report({'INFO'}, f"Camera '{camera.name}' created from the viewport view.")
125 | return {'FINISHED'}
126 |
127 |
128 | class CAM_MANAGER_OT_lock_cameras(bpy.types.Operator):
129 | """Operator to lock and unlock all camera transforms"""
130 | bl_idname = "cam_manager.lock_unlock_camera"
131 | bl_label = "Lock/Unlock Camera"
132 | bl_description = "Lock/unlock location, rotation, and scaling of a camera"
133 |
134 | camera_name: bpy.props.StringProperty()
135 | cam_lock: bpy.props.BoolProperty(name="lock", default=True)
136 |
137 | def execute(self, context):
138 | if self.camera_name and bpy.data.objects[self.camera_name]:
139 | obj = bpy.data.objects[self.camera_name]
140 | lock_camera(obj, self.cam_lock)
141 |
142 | return {'FINISHED'}
143 |
144 |
145 | class CAM_MANAGER_OT_cycle_cameras_next(bpy.types.Operator):
146 | """Cycle through available cameras"""
147 | bl_idname = "cam_manager.cycle_cameras_next"
148 | bl_label = "Next Camera"
149 | bl_description = "Change the active camera to the next camera"
150 | bl_options = {'REGISTER'}
151 |
152 | direction: bpy.props.EnumProperty(
153 | name="Direction",
154 | items=(
155 | ('FORWARD', "Forward", "Next camera (alphabetically)"),
156 | ('BACKWARD', "Backward", "Previous camera (alphabetically)"),
157 | ),
158 | default='FORWARD'
159 | )
160 |
161 | def execute(self, context):
162 | if cycleCamera(context, self.direction):
163 | return {'FINISHED'}
164 | else:
165 | return {'CANCELLED'}
166 |
167 |
168 | class CAM_MANAGER_OT_cycle_cameras_backward(bpy.types.Operator):
169 | """Changes active camera to previous camera from Camera list"""
170 | bl_idname = "cam_manager.cycle_cameras_backward"
171 | bl_label = "Previous Cameras"
172 | bl_description = "Change the active camera to the previous camera"
173 | bl_options = {'REGISTER'}
174 |
175 | direction: bpy.props.EnumProperty(
176 | name="Direction",
177 | items=(
178 | ('FORWARD', "Forward", "Next camera (alphabetically)"),
179 | ('BACKWARD', "Backward", "Previous camera (alphabetically)"),
180 | ),
181 | default='BACKWARD'
182 | )
183 |
184 | def execute(self, context):
185 | if cycleCamera(context, self.direction):
186 | return {'FINISHED'}
187 | else:
188 | return {'CANCELLED'}
189 |
190 |
191 | class CAM_MANAGER_OT_create_collection(bpy.types.Operator):
192 | """Creates a new collection"""
193 | bl_idname = "camera.create_collection"
194 | bl_label = "Create Camera Collection"
195 | bl_description = "Create a cameras collection and add it to the scene"
196 | bl_options = {'REGISTER'}
197 |
198 | collection_name: bpy.props.StringProperty(name='Name', default='')
199 |
200 | def execute(self, context):
201 | global cam_collection_name
202 |
203 | 'use default from global variable defined at the top'
204 | if not self.collection_name:
205 | self.collection_name = cam_collection_name
206 |
207 | parent_collection = context.scene.collection
208 | collection_name = self.collection_name
209 | col = make_collection(collection_name, parent_collection)
210 |
211 | context.scene.cam_collection.collection = col
212 | return {'FINISHED'}
213 |
214 |
215 | class CAM_MANAGER_OT_resolution_from_img(bpy.types.Operator):
216 | """Sets camera resolution based on first background image assigned to the camera."""
217 | bl_idname = "cam_manager.camera_resolutio_from_image"
218 | bl_label = "Resolution from Background"
219 | bl_description = "Set the camera resolution to the camera background image"
220 |
221 | camera_name: bpy.props.StringProperty()
222 |
223 | def execute(self, context):
224 | if self.camera_name and bpy.data.cameras[self.camera_name]:
225 | camera = bpy.data.cameras[self.camera_name]
226 |
227 | if len(bpy.data.cameras[self.camera_name].background_images) > 0:
228 | resolution = camera.background_images[0].image.size
229 | camera.resolution = resolution
230 | return {'FINISHED'}
231 | else:
232 | self.report({'WARNING'}, 'Camera has no background image')
233 | return {'CANCELLED'}
234 |
235 | self.report({'WARNING'}, 'Not valid camera')
236 | return {'CANCELLED'}
237 |
238 |
239 | class CAM_MANAGER_OT_hide_unhide_camera(bpy.types.Operator):
240 | """Hides or unhides a camera"""
241 | bl_idname = "camera.hide_unhide"
242 | bl_label = "Hide/Unhide Camera"
243 | bl_description = "Hide or unhide the camera"
244 |
245 | camera_name: bpy.props.StringProperty()
246 | cam_hide: bpy.props.BoolProperty(name="hide", default=True)
247 |
248 | def execute(self, context):
249 | if self.camera_name and bpy.data.objects[self.camera_name]:
250 | obj = bpy.data.objects[self.camera_name]
251 | obj.hide_set(self.cam_hide)
252 | return {'FINISHED'}
253 |
254 |
255 | class CAM_MANAGER_OT_switch_camera(bpy.types.Operator):
256 | """Set camera as scene camera and update the resolution accordingly. The camera is set as active object and selected."""
257 | bl_idname = "cam_manager.change_scene_camera"
258 | bl_label = "Set active Camera"
259 | bl_description = "Set the active camera"
260 |
261 | camera_name: bpy.props.StringProperty()
262 | switch_to_cam: bpy.props.BoolProperty(default=False)
263 |
264 | def execute(self, context):
265 | scene = context.scene
266 | if self.camera_name and scene.objects.get(self.camera_name):
267 | camera = scene.objects[self.camera_name]
268 |
269 | if camera.data.resolution:
270 | resolution = camera.data.resolution
271 | scene.render.resolution_x = resolution[0]
272 | scene.render.resolution_y = resolution[1]
273 |
274 | # if camera.data.exposure: #returns 0 when exposure = 0
275 | scene.view_settings.exposure = camera.data.exposure
276 |
277 | if camera.data.world:
278 | try:
279 | world = camera.data.world
280 | scene.world = world
281 | except KeyError:
282 | self.report({'WARNING'}, 'World material could not be found')
283 | pass
284 |
285 | scene.camera = camera
286 | context.view_layer.objects.active = camera
287 | bpy.ops.object.select_all(action='DESELECT')
288 | camera.select_set(True)
289 |
290 | objectlist = list(context.scene.objects)
291 | idx = objectlist.index(camera)
292 |
293 | if self.switch_to_cam:
294 | found_view_3d = False
295 | for screen in bpy.data.screens:
296 | for area in screen.areas:
297 | if area.type == 'VIEW_3D':
298 | for space in area.spaces:
299 | if space.type == 'VIEW_3D':
300 | space.region_3d.view_perspective = 'CAMERA'
301 | space.camera = camera
302 | found_view_3d = True
303 | break
304 | if found_view_3d:
305 | break
306 | if found_view_3d:
307 | break
308 |
309 | scene.camera_list_index = idx
310 |
311 | if camera.data.slot <= len(bpy.data.images['Render Result'].render_slots):
312 | # subtract by one to make 1 the first slot 'Slot1' and not user input 0
313 | bpy.data.images['Render Result'].render_slots.active_index = camera.data.slot - 1
314 |
315 | if scene.output_use_cam_name:
316 | old_path = bpy.context.scene.render.filepath
317 | path, basename = os.path.split(old_path)
318 | new_path = os.path.join(path, self.camera_name)
319 | bpy.context.scene.render.filepath = new_path
320 |
321 | return {'FINISHED'}
322 |
323 |
324 | class CAM_MANAGER_OT_camera_to_collection(bpy.types.Operator):
325 | """Moves a camera to another collection"""
326 | bl_idname = "cameras.add_collection"
327 | bl_label = "To Collection"
328 | bl_description = "Move the camera to a another collection"
329 |
330 | object_name: bpy.props.StringProperty()
331 |
332 | def execute(self, context):
333 | scene = context.scene
334 |
335 | if not scene.cam_collection.collection:
336 | global cam_collection_name
337 |
338 | 'use default from global variable defined at the top'
339 | self.report({'INFO'}, 'A new camera collection was created')
340 | parent_collection = context.scene.collection
341 | scene.cam_collection.collection = make_collection(cam_collection_name, parent_collection)
342 |
343 | if self.object_name and scene.objects[self.object_name]:
344 | camera = scene.objects[self.object_name]
345 | cam_collection = scene.cam_collection.collection
346 | moveToCollection(camera, cam_collection)
347 |
348 | return {'FINISHED'}
349 |
350 |
351 | class CAM_MANAGER_OT_select_active_cam(bpy.types.Operator):
352 | """Selects the currently active camera and sets it to be the active object"""
353 | bl_idname = "cameras.select_active_cam"
354 | bl_label = "Select active camera"
355 |
356 | @classmethod
357 | def poll(cls, context):
358 | return context.scene.camera is not None
359 |
360 | def execute(self, context):
361 | bpy.ops.object.select_all(action='DESELECT')
362 |
363 | cam = context.scene.camera
364 | cam.select_set(True)
365 | context.view_layer.objects.active = cam
366 | return {'FINISHED'}
367 |
368 |
369 | class CAM_MANAGER_OT_all_cameras_to_collection(bpy.types.Operator):
370 | """Moves all camera to another collection"""
371 | bl_idname = "cameras.all_to_collection"
372 | bl_label = "All to collection "
373 | bl_description = "Move all cameras to a specified collection"
374 |
375 | def execute(self, context):
376 | scene = context.scene
377 |
378 | if not scene.cam_collection.collection:
379 | global cam_collection_name
380 |
381 | 'use default from global variable defined at the top'
382 | self.report({'INFO'}, 'A new camera collection was created')
383 | parent_collection = context.scene.collection
384 | scene.cam_collection.collection = make_collection(cam_collection_name, parent_collection)
385 |
386 | for obj in bpy.data.objects:
387 | if obj.type == 'CAMERA':
388 | cam_collection = scene.cam_collection.collection
389 | moveToCollection(obj, cam_collection)
390 |
391 | return {'FINISHED'}
392 |
393 |
394 | class CAM_MANAGER_OT_render(bpy.types.Operator):
395 | """Switch camera and render"""
396 | bl_idname = "cameras.custom_render"
397 | bl_label = "Render"
398 | bl_description = "Switch camera and start a render"
399 |
400 | camera_name: bpy.props.StringProperty()
401 |
402 | @classmethod
403 | def poll(cls, context):
404 | return context.scene.camera is not None
405 |
406 | def execute(self, context):
407 | scene = context.scene
408 | bpy.ops.cam_manager.change_scene_camera(camera_name=self.camera_name, switch_to_cam=False)
409 | bpy.ops.render.render('INVOKE_DEFAULT', animation=False, write_still=scene.output_render, use_viewport=False)
410 | return {'FINISHED'}
411 |
412 |
413 | def resolution_update_func(self, context):
414 | """
415 | Updating scene resolution when changing the resolution of the active camera
416 | :param self:
417 | :param context:
418 | :return: None
419 | """
420 | if context.scene.camera.data.name == self.name:
421 | context.scene.render.resolution_x = self.resolution[0]
422 | context.scene.render.resolution_y = self.resolution[1]
423 |
424 |
425 | def exposure_update_func(self, context):
426 | """
427 | Updating scene exposure when changing the exposure of the active camera
428 | :param self:
429 | :param context:
430 | :return: None
431 | """
432 |
433 | if context.scene.camera.data.name == self.name:
434 | context.scene.view_settings.exposure = self.exposure
435 |
436 |
437 | def world_update_func(self, context):
438 | """
439 | Updating the world material when changing the world material for the active camera
440 | :param self:
441 | :param context:
442 | :return: None
443 | """
444 |
445 | if context.scene.camera.data.name == self.name:
446 | context.scene.world = self.world
447 | self.world.use_fake_user = True
448 |
449 | return None
450 |
451 |
452 | def render_slot_update_funce(self, context):
453 | """
454 | Update the render slot when changing render slot for the active camera. A new render slot will
455 | be created if the number is higher than the number of current render slots. The newly created
456 | render slots gets assigned automatically.
457 | :param self:
458 | :param context:
459 | :return: None
460 | """
461 |
462 | new_slot = False
463 | render_result = bpy.data.images['Render Result']
464 |
465 | # Create new slot if the input is higher than the current number of render slots
466 | if self.slot > len(bpy.data.images['Render Result'].render_slots):
467 | render_result.render_slots.new()
468 | new_slot = True
469 | self.slot = len(bpy.data.images['Render Result'].render_slots)
470 |
471 | if context.scene.camera.data.name == self.name:
472 | if new_slot:
473 | new_render_slot_nr = len(bpy.data.images['Render Result'].render_slots)
474 | render_result.render_slots.active_index = new_render_slot_nr
475 | self.slot = new_render_slot_nr
476 | else:
477 | # subtract by one to make 1 the first slot 'Slot1' and not user input 0
478 | render_result.render_slots.active_index = self.slot - 1
479 |
480 |
481 | def update_func(self, context):
482 | self.show_limits = not self.show_limits
483 | self.show_limits = not self.show_limits
484 |
485 |
486 | classes = (
487 | CAM_MANAGER_OT_camera_to_collection,
488 | CAM_MANAGER_OT_create_collection,
489 | CAM_MANAGER_OT_render,
490 | CAM_MANAGER_OT_resolution_from_img,
491 | CAM_MANAGER_OT_switch_camera,
492 | CAM_MANAGER_OT_cycle_cameras_next,
493 | CAM_MANAGER_OT_cycle_cameras_backward,
494 | CAM_MANAGER_OT_lock_cameras,
495 | CAM_MANAGER_OT_hide_unhide_camera,
496 | CAM_MANAGER_OT_all_cameras_to_collection,
497 | CAM_MANAGER_OT_select_active_cam,
498 | OBJECT_OT_create_camera_from_view,
499 | )
500 |
501 |
502 | def load_post_handler(dummy):
503 | """Ensure all collections have the 'use_root_object' and 'root_object' properties set."""
504 | for cam in bpy.data.cameras:
505 | if not hasattr(cam, "render_selected"):
506 | cam.root_object = False # Ensure property exists
507 |
508 |
509 | def register():
510 | # Register handler to ensure properties are set when loading an older .blend file
511 | bpy.app.handlers.load_post.append(load_post_handler)
512 |
513 | scene = bpy.types.Scene
514 | scene.camera_list_index = bpy.props.IntProperty(name="Index for lis one", default=0)
515 |
516 | # data stored in camera
517 | cam = bpy.types.Camera
518 |
519 | cam.resolution = bpy.props.IntVectorProperty(name='Camera Resolution', description='Camera resolution in px',
520 | default=(1920, 1080),
521 | min=4, soft_min=800, soft_max=8096,
522 | subtype='COORDINATES', size=2, update=resolution_update_func, get=None,
523 | set=None)
524 |
525 | cam.render_selected = bpy.props.BoolProperty(name="Select Camera",
526 | description="Select this camera for rendering",
527 | default=False)
528 |
529 | cam.exposure = bpy.props.FloatProperty(name='exposure', description='Camera exposure', default=0, soft_min=-10,
530 | soft_max=10, update=exposure_update_func)
531 |
532 | cam.slot = bpy.props.IntProperty(name="Slot", default=1, description='Render slot, used when rendering this camera',
533 | min=1, soft_max=15, update=render_slot_update_funce)
534 |
535 | cam.dolly_zoom_target_scale = bpy.props.FloatProperty(name='Target Scale', description='', default=2, min=0,
536 | update=update_func)
537 | cam.dolly_zoom_target_distance = bpy.props.FloatProperty(name='Target Distance', description='', default=10, min=0,
538 | update=update_func)
539 |
540 | from bpy.utils import register_class
541 |
542 | for cls in classes:
543 | register_class(cls)
544 |
545 | # The PointerProperty has to be after registering the classes to know about the custom property type
546 | cam.world = bpy.props.PointerProperty(update=world_update_func, type=bpy.types.World,
547 | name="World Material") # type=WorldMaterialProperty, name="World Material", description='World material assigned to the camera',
548 |
549 |
550 | def unregister():
551 | from bpy.utils import unregister_class
552 |
553 | for cls in reversed(classes):
554 | unregister_class(cls)
555 |
556 | cam = bpy.types.Camera
557 |
558 | del cam.resolution
559 | del cam.render_selected
560 | del cam.slot
561 | del cam.exposure
562 | del cam.world
563 |
--------------------------------------------------------------------------------
/camera_gizmos.py:
--------------------------------------------------------------------------------
1 | from bpy.types import (
2 | GizmoGroup,
3 | Gizmo
4 | )
5 | from .dolly_zoom_modal import calculate_target_width
6 |
7 | import mathutils
8 |
9 | # # Coordinates (each one is a line).
10 | custom_shape_verts_02 = (
11 | (0.2, -1.0, 0.0), (1.0, -1.0, 0.0),
12 | (1.0, -1.0, 0.0), (1.0, -0.2, 0.0),
13 | (-1.0, -0.2, 0.0), (-1.0, -1.0, 0.0),
14 | (-1.0, -1.0, 0.0), (-0.2, -1.0, 0.0),
15 | (-1.0, 1.0, 0.0), (-1.0, 0.2, 0.0),
16 | (-0.2, 1.0, 0.0), (-1.0, 1.0, 0.0),
17 | (1.0, 0.2, 0.0), (1.0, 1.0, 0.0),
18 | (1.0, 1.0, 0.0), (0.2, 1.0, 0.0)
19 | )
20 |
21 |
22 | class MyCustomShapeWidget(Gizmo):
23 | bl_idname = "Custom_Dolly_Gizmo"
24 | bl_target_properties = (
25 | {"id": "offset", "type": 'FLOAT', "array_length": 1},
26 | # {"id": "offset", "type": 'TUPLE', "array_length": 1},
27 | )
28 | #
29 |
30 | __slots__ = (
31 | "custom_shape",
32 | "init_mouse_x",
33 | "init_value",
34 | )
35 |
36 | def _update_offset_matrix(self):
37 | # offset behind the light
38 | self.matrix_offset.col[3][2] = float(self.target_get_value('offset')[0]) * -1
39 |
40 |
41 | def draw(self, context):
42 | self._update_offset_matrix()
43 | self.draw_custom_shape(self.custom_shape)
44 |
45 |
46 | def draw_select(self, context, select_id):
47 | self._update_offset_matrix()
48 | self.draw_custom_shape(self.custom_shape, select_id=select_id)
49 |
50 | def setup(self):
51 | if not hasattr(self, "custom_shape"):
52 | # type (string) – The type of shape to create in (POINTS, LINES, TRIS, LINE_STRIP).
53 | # verts (sequence of 2D or 3D coordinates.) – Coordinates.
54 | # display_name (Callable that takes a string and returns a string.) – Optional callback that takes the full path, returns the name to display.
55 | self.custom_shape = self.new_custom_shape('LINES', custom_shape_verts_02)
56 |
57 |
58 | def invoke(self, context, event):
59 | self.init_mouse_x = event.mouse_x
60 | self.init_value = self.target_get_value('offset')[0]
61 | return {'RUNNING_MODAL'}
62 |
63 | def exit(self, context, cancel):
64 | context.area.header_text_set(None)
65 | if cancel:
66 | self.target_set_value("offset", self.init_value)[0]
67 |
68 | def modal(self, context, event, tweak):
69 | delta = (event.mouse_x - self.init_mouse_x) / 10.0
70 | if 'SNAP' in tweak:
71 | delta = round(delta)
72 | if 'PRECISE' in tweak:
73 | delta /= 10.0
74 | value = self.init_value + delta
75 |
76 | # Set value as a Tuple, it doesn't work otherwise
77 | self.target_set_value("offset", (value,))
78 |
79 | context.area.header_text_set("My Gizmo: %.4f" % value)
80 | return {'RUNNING_MODAL'}
81 |
82 |
83 | class CameraFocusDistance(GizmoGroup):
84 | bl_idname = "OBJECT_GGT_focus_distance_camera"
85 | bl_label = "Camera Focus Distance Widget"
86 | bl_space_type = 'VIEW_3D'
87 | bl_region_type = 'WINDOW'
88 | bl_options = {'3D', 'PERSISTENT', 'SHOW_MODAL_ALL', 'DEPTH_3D'}
89 |
90 | # DEPTH_3D allows for objects in the scene to overlap gizmos. It causes for gizmos not to work properly since Blender 3.1
91 | # bl_options = {'3D', 'PERSISTENT', 'SHOW_MODAL_ALL', 'DEPTH_3D'}
92 |
93 | cam = None
94 |
95 | @classmethod
96 | def poll(cls, context):
97 | ob = context.object
98 |
99 | if ob and ob.type == 'CAMERA':
100 | prefs = context.preferences.addons[__package__].preferences
101 | return prefs.show_dolly_gizmo
102 |
103 | def setup(self, context):
104 | camera = context.object
105 | self.cam = camera
106 |
107 | gizmo = self.gizmos.new(MyCustomShapeWidget.bl_idname)
108 | # gizmo = self.gizmos.new("GIZMO_GT_arrow_3d")
109 | # gizmo = self.gizmos.new("GIZMO_GT_primitive_3d")
110 | # gizmo.use_draw_offset_scale = False
111 |
112 | gizmo.use_draw_scale = False
113 | gizmo.use_draw_offset_scale = True
114 |
115 | # Needed to work with modal operator
116 | gizmo.use_draw_modal = True
117 |
118 | gizmo.color = (1.0, 1.0, 1.0)
119 | gizmo.color_highlight = (1.0, 1.0, 0.0)
120 |
121 | # Draw only when hovering
122 | # gizmo.use_draw_hover = True
123 |
124 | def get_dolly_zoom_target_distance():
125 | if self.cam:
126 | return self.cam.data.dolly_zoom_target_distance
127 | else:
128 | return (0.0,)
129 |
130 | def set_dolly_zoom_target_distance(value):
131 | if self.cam:
132 | self.cam.data.dolly_zoom_target_distance = value
133 |
134 | # gizmo.target_set_prop("offset", camera.data, "dolly_zoom_target_distance")
135 | gizmo.target_set_handler('offset', get=get_dolly_zoom_target_distance, set=set_dolly_zoom_target_distance)
136 |
137 | # Needed to keep the scale constant
138 | gizmo.scale_basis = 1.0
139 |
140 | gizmo.matrix_basis = camera.matrix_world.normalized()
141 |
142 | self.distance_gizmo = gizmo
143 |
144 | def refresh(self, context):
145 | camera = context.object
146 |
147 | self.cam = camera
148 |
149 | w_matrix = self.cam.matrix_world.copy()
150 | orig_loc, orig_rot, orig_scale = w_matrix.normalized().decompose()
151 |
152 | orig_loc_mat = mathutils.Matrix.Translation(orig_loc)
153 | orig_rot_mat = orig_rot.to_matrix().to_4x4()
154 |
155 | scale = calculate_target_width(self.cam.data.dolly_zoom_target_distance, self.cam.data.angle)
156 |
157 | scale_matrix_x2 = mathutils.Matrix.Scale(scale, 4, (1.0, 0.0, 0.0))
158 | scale_matrix_y2 = mathutils.Matrix.Scale(scale, 4, (0.0, 1.0, 0.0))
159 | scale_matrix_z2 = mathutils.Matrix.Scale(1, 4, (0.0, 0.0, 1.0))
160 |
161 | scale_mat = scale_matrix_x2 @ scale_matrix_y2 @ scale_matrix_z2
162 |
163 | # assemble the new matrix
164 | mat_out = orig_loc_mat @ orig_rot_mat @ scale_mat
165 |
166 | self.distance_gizmo.matrix_basis = mat_out
167 |
168 |
169 | classes = (
170 | CameraFocusDistance,
171 | MyCustomShapeWidget,
172 | )
173 |
174 |
175 | def register():
176 | from bpy.utils import register_class
177 |
178 | for cls in classes:
179 | register_class(cls)
180 |
181 |
182 | def unregister():
183 | from bpy.utils import unregister_class
184 |
185 | for cls in reversed(classes):
186 | unregister_class(cls)
187 |
--------------------------------------------------------------------------------
/dolly_zoom_modal.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import blf
4 | import bpy
5 | from mathutils import Vector
6 |
7 |
8 | def calculate_target_width(distance, fov):
9 | width = distance * math.tan(0.5 * fov)
10 | return width
11 |
12 |
13 | def generate_target_location(camera, distance):
14 | # Create the transformation matrix to move 1 unit along x
15 | vec = Vector((0.0, 0.0, -distance))
16 |
17 | inv = camera.matrix_world.copy()
18 | inv.invert()
19 | vec_rot = vec @ inv
20 |
21 | target_location = camera.location.copy() + vec_rot
22 |
23 | return target_location
24 |
25 |
26 | def set_cam_values(cam_dic, camera, distance):
27 | """Store current camera settings in a dictionary """
28 | # Initial Camera Position and Location Matrix
29 | cam_dic['cam_location'] = camera.location.copy()
30 |
31 | # get world matrix and invert
32 | inv = camera.matrix_world.copy()
33 | inv.invert()
34 | cam_dic['inverted_matrix'] = inv
35 |
36 | # Initial Camera Lens Settings
37 | cam_dic['camera_fov'] = camera.data.angle
38 | cam_dic['camera_focal_length'] = camera.data.angle
39 |
40 | # Intitial Distance
41 | target_location = generate_target_location(camera, distance)
42 |
43 | cam_dic['distance'] = distance
44 | cam_dic['target_location'] = target_location
45 |
46 | cam_dic['target_width'] = camera.data.dolly_zoom_target_scale
47 |
48 | return cam_dic
49 |
50 |
51 | def distance_vec(point1: Vector, point2: Vector):
52 | """Calculates distance between two points."""
53 | return (point2 - point1).length
54 |
55 |
56 | def draw_title_text(self, font_id, i, vertical_px_offset, left_margin, name, color):
57 | """Draw UI title text in the 3D Viewport """
58 | blf.size(font_id, 20)
59 |
60 | blf.color(font_id, color[0], color[1], color[2], color[3])
61 | blf.position(font_id, left_margin, i * vertical_px_offset, 0)
62 | blf.draw(font_id, name)
63 |
64 |
65 | def draw_vierport_text(self, font_id, i, vertical_px_offset, left_margin, name, value, initial_value=None,
66 | highlighting=False):
67 | """Draw UI operator parameters as text in the 3D Viewport """
68 | text = '{name:}:'.format(name=name)
69 | text2 = '{value:.2f}'.format(value=value)
70 |
71 | font_size = 20
72 |
73 | if bpy.app.version < (4, 00):
74 | # legacy support
75 | blf.size(font_id, 75, font_size)
76 | else:
77 | blf.size(font_id, font_size)
78 |
79 | # define color for input ignore
80 | if self.ignore_input:
81 | c1 = c2 = [0.5, 0.5, 0.5, 0.5]
82 |
83 | # define color for highlited value
84 | elif highlighting:
85 | c1 = c2 = [0.0, 1.0, 1.0, 1.0]
86 |
87 | # define default values
88 | else:
89 | c1 = [1.0, 1.0, 1.0, 1.0]
90 | c2 = [1.0, 1.0, 0.5, 1.0]
91 |
92 | blf.color(font_id, c1[0], c1[1], c1[2], c1[3])
93 | blf.position(font_id, left_margin, i * vertical_px_offset, 0)
94 | blf.draw(font_id, text)
95 |
96 | blf.color(font_id, c2[0], c2[1], c2[2], c2[3])
97 | blf.position(font_id, left_margin + 190, i * vertical_px_offset, 0)
98 | blf.draw(font_id, text2)
99 |
100 | # optional original value
101 | if initial_value:
102 | text3 = '{initial_value:.2f}'.format(initial_value=initial_value)
103 | blf.color(font_id, c1[0], c1[1], c1[2], c1[3])
104 | blf.position(font_id, left_margin + 350, i * vertical_px_offset, 0)
105 | blf.draw(font_id, text3)
106 |
107 | i += 1
108 |
109 | return i
110 |
111 |
112 | def draw_callback_px(self, context):
113 | """Draw 3d viewport text for the dolly zoom modal operator"""
114 | scene = context.scene
115 |
116 | font_id = 0 # XXX, need to find out how best to get this.
117 | vertical_px_offset = 30
118 | left_margin = bpy.context.area.width / 2 - 200
119 | i = 1
120 |
121 | x = math.degrees(self.current_camera_fov)
122 | y = math.degrees(self.initial_cam_settings['camera_fov'])
123 | i = draw_vierport_text(self, font_id, i, vertical_px_offset, left_margin, 'FOV', x, initial_value=y)
124 |
125 | x = self.current_focal_length
126 | y = self.initial_cam_settings['camera_focal_length']
127 | i = draw_vierport_text(self, font_id, i, vertical_px_offset, left_margin, 'Focal Length', x, initial_value=y)
128 |
129 | x = self.camera.data.dolly_zoom_target_distance
130 | y = self.initial_cam_settings['distance']
131 | i = draw_vierport_text(self, font_id, i, vertical_px_offset, left_margin, 'Distance (D)', x, initial_value=y,
132 | highlighting=self.set_distance)
133 |
134 | x = self.camera.data.dolly_zoom_target_scale
135 | y = self.initial_cam_settings['target_width']
136 | i = draw_vierport_text(self, font_id, i, vertical_px_offset, left_margin, 'Width (F)', x, initial_value=y,
137 | highlighting=self.set_width)
138 |
139 | if self.ignore_input:
140 | color = (0.0, 1.0, 1.0, 1.0)
141 | else:
142 | color = (1.0, 1.0, 1.0, 1.0)
143 |
144 | draw_title_text(self, font_id, i, vertical_px_offset, left_margin, 'IGNORE INPUT (ALT)', color)
145 |
146 |
147 | class CAM_MANAGER_OT_dolly_zoom(bpy.types.Operator):
148 | """Modlar operator that keeps the object size in viewport when changing the focal lenght """
149 |
150 | bl_idname = "cam_manager.modal_camera_dolly_zoom"
151 | bl_label = "Dolly Zoom"
152 | bl_description = "Change focal lenght while keeping the target object at the same size in the camera view"
153 | bl_options = {'REGISTER', 'UNDO'}
154 |
155 | def force_redraw(self):
156 | bpy.context.object.data.show_limits = not self.show_limits
157 | bpy.context.object.data.show_limits = self.show_limits
158 |
159 | def update_camera(self, cam_offset=0, use_cam_offset=False):
160 |
161 | camera = self.camera
162 |
163 | if use_cam_offset:
164 | if cam_offset > -self.ref_cam_settings['distance']:
165 | cam_move = cam_offset
166 | distance = cam_move + self.ref_cam_settings['distance']
167 |
168 | else: # Camera goes past the target
169 | cam_move = abs(cam_offset) - self.ref_cam_settings['distance'] - self.ref_cam_settings['distance']
170 | distance = cam_move + self.ref_cam_settings['distance']
171 |
172 | # move camera
173 | # vec aligned to local axis in Blender 2.8+
174 | vec = Vector((0.0, 0.0, cam_move))
175 | vec_rot = vec @ self.ref_cam_settings['inverted_matrix']
176 | camera.location = self.ref_cam_settings['cam_location'] + vec_rot
177 |
178 | else:
179 | # Calculate Distance from camera to target
180 | cam_vector = Vector(self.camera.matrix_world.to_translation())
181 | target_pos = Vector(self.ref_cam_settings['target_location'])
182 | distance = distance_vec(cam_vector, target_pos)
183 |
184 | camera.data.dolly_zoom_target_distance = distance
185 |
186 | # Dolly Zoom computation
187 | if distance != 0:
188 | field_of_view = 2 * math.atan(self.camera.data.dolly_zoom_target_scale / distance)
189 | else:
190 | field_of_view = 2 * math.atan(self.camera.data.dolly_zoom_target_scale / 0.01)
191 |
192 | # Set camera field of view and
193 | camera.data.angle = field_of_view
194 | self.current_camera_fov = field_of_view
195 | self.current_focal_length = camera.data.lens
196 |
197 | @classmethod
198 | def poll(cls, context):
199 | if context.scene.camera:
200 | return True
201 | else:
202 | return False
203 |
204 | def invoke(self, context, event):
205 | bpy.ops.object.select_all(action='DESELECT')
206 |
207 | if context.scene.camera:
208 | camera = context.scene.camera
209 | camera.select_set(True)
210 | context.view_layer.objects.active = camera
211 |
212 | # initial state Gizmo
213 | prefs = context.preferences.addons[__package__].preferences
214 | self.initial_gizmo_state = prefs.show_dolly_gizmo
215 |
216 | # Camera Object Settings
217 | self.camera = camera
218 | self.show_limits = bpy.context.object.data.show_limits
219 |
220 | # setting used when changing the distance with the modal operator
221 | self.tmp_distance = False
222 |
223 | # Camera lens settings
224 | self.current_camera_fov = camera.data.angle
225 | self.current_focal_length = camera.data.lens
226 |
227 | ##### UI #######
228 | # Mouse
229 | self.mouse_initial_x = event.mouse_x
230 |
231 | # check if alt is pressed
232 | self.ignore_input = False
233 |
234 | # Operator modes
235 | self.set_width = False
236 | self.set_distance = False
237 |
238 | # Target
239 | width = calculate_target_width(camera.data.dolly_zoom_target_distance, camera.data.angle)
240 | camera.data.dolly_zoom_target_scale = width
241 |
242 | # Camera Reference Values
243 | cam_settings = {}
244 | cam_settings = set_cam_values(cam_settings, camera, self.camera.data.dolly_zoom_target_distance)
245 |
246 | # Camera Settings
247 | self.initial_cam_settings = cam_settings
248 | self.ref_cam_settings = cam_settings.copy()
249 |
250 | # update camera
251 | self.update_camera()
252 |
253 | # the arguments we pass to the callback
254 |
255 | args = (self, context)
256 | # Add the region OpenGL drawing callback
257 | # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
258 | self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
259 |
260 | context.window_manager.modal_handler_add(self)
261 | return {'RUNNING_MODAL'}
262 |
263 | else:
264 |
265 | self.report({'WARNING'}, "No scene camera assigned")
266 | return {'CANCELLED'}
267 |
268 | def modal(self, context, event):
269 | """Calculate the FOV from the changed location to the target object """
270 |
271 | camera = self.camera
272 | scene = context.scene
273 |
274 | # Set Gizmo to be visible during the modal operation. Dirty!
275 | prefs = context.preferences.addons[__package__].preferences
276 | prefs.show_dolly_gizmo = True
277 |
278 | # Display keymap information
279 | context.area.header_text_set(
280 | "Dolly Zoom: Left Mouse: Confirm, Right Mouse/ESC: Cancel, F: Toggle Width, D: Toggle Distance, Shift/Ctrl: Adjust Sensitivity")
281 |
282 | # Cancel Operator
283 | if event.type in {'RIGHTMOUSE', 'ESC'}:
284 | # reset camera position and fov. resetting the fov will also reset the focal length
285 | self.camera.location = self.initial_cam_settings['cam_location']
286 | self.camera.data.angle = self.initial_cam_settings['camera_fov']
287 | self.camera.data.dolly_zoom_target_distance = self.initial_cam_settings['distance']
288 | prefs.show_dolly_gizmo = self.initial_gizmo_state
289 |
290 | # Remove Viewport Text
291 | context.area.header_text_set(None)
292 | try:
293 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
294 | except ValueError:
295 | pass
296 | return {'CANCELLED'}
297 |
298 | # Apply operator
299 | elif event.type == 'LEFTMOUSE':
300 | # Remove Viewport Text
301 | prefs.show_dolly_gizmo = self.initial_gizmo_state
302 | context.area.header_text_set(None)
303 | try:
304 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
305 | except ValueError:
306 | pass
307 | return {'FINISHED'}
308 |
309 | elif event.alt:
310 | # update reference camera settings to current camera settings
311 | self.ignore_input = True
312 | self.force_redraw()
313 | return {'RUNNING_MODAL'}
314 |
315 | elif event.type == 'F' and event.value == 'RELEASE':
316 | self.set_width = not self.set_width
317 | self.set_distance = False
318 | self.ref_cam_settings = set_cam_values(self.ref_cam_settings, camera,
319 | self.camera.data.dolly_zoom_target_distance)
320 |
321 | elif event.type == 'D' and event.value == 'RELEASE':
322 | self.set_distance = not self.set_distance
323 | self.set_width = False
324 | if self.set_distance:
325 | self.tmp_distance = self.ref_cam_settings['distance']
326 | else:
327 | self.tmp_distance = False
328 | self.ref_cam_settings = set_cam_values(self.ref_cam_settings, camera,
329 | self.camera.data.dolly_zoom_target_distance)
330 |
331 | # Set ref values when switching mode to avoid jumping of field of view.
332 | elif event.type in ['LEFT_SHIFT', 'LEFT_CTRL'] and event.value in ['PRESS', 'RELEASE']:
333 | # update reference camera settings to current camera settings
334 | self.ref_cam_settings = set_cam_values(self.ref_cam_settings, camera,
335 | self.camera.data.dolly_zoom_target_distance)
336 | self.tmp_distance = self.ref_cam_settings['distance']
337 | # update ref mouse position to current
338 | self.mouse_initial_x = event.mouse_x
339 | # Alt is not pressed anymore after release
340 | self.ignore_input = False
341 | return {'RUNNING_MODAL'}
342 |
343 | # Ignore Mouse Movement. The Operator will behave as starting it newly
344 | elif event.type == 'LEFT_ALT' and event.value == 'RELEASE':
345 | # update reference camera settings to current camera settings
346 | self.ref_cam_settings = set_cam_values(self.ref_cam_settings, camera,
347 | self.camera.data.dolly_zoom_target_distance)
348 | self.tmp_distance = self.ref_cam_settings['distance']
349 | # update ref mouse position to current
350 | self.mouse_initial_x = event.mouse_x
351 | # Alt is not pressed anymore after release
352 | self.ignore_input = False
353 | return {'RUNNING_MODAL'}
354 |
355 | elif event.type == 'MOUSEMOVE':
356 | self.ignore_input = False
357 | # calculate mouse movement and offset camera
358 | delta = int(self.mouse_initial_x - event.mouse_x)
359 | # Ignore if Alt is pressed
360 | if event.alt:
361 | self.ignore_input = True
362 | return {'RUNNING_MODAL'}
363 |
364 | elif self.set_width:
365 | # Mouse Sensitivity and Sensitivity Modifiers (Shift, Ctrl)
366 | factor = 0.005
367 | if event.ctrl:
368 | factor = 0.015
369 | elif event.shift:
370 | factor = 0.001
371 | # calculate width offset
372 | offset = delta * factor
373 | width = abs(self.ref_cam_settings['target_width'] + offset)
374 | # set operator variables and camera property
375 | self.camera.data.dolly_zoom_target_scale = width
376 | # update camera
377 | self.update_camera()
378 |
379 | elif self.set_distance:
380 | factor = 0.05
381 | if event.ctrl:
382 | factor = 0.15
383 | elif event.shift:
384 | factor = 0.005
385 | # calculate width offset
386 | offset = delta * factor
387 | distance = abs(self.tmp_distance + offset)
388 | self.camera.data.dolly_zoom_target_distance = distance
389 | self.ref_cam_settings['distance'] = distance
390 | # Target
391 | width = calculate_target_width(camera.data.dolly_zoom_target_distance, camera.data.angle)
392 | camera.data.dolly_zoom_target_scale = width
393 | # update camera
394 |
395 | else:
396 | # Mouse Sensitivity and Sensitivity Modifiers (Shift, Ctrl)
397 | factor = 0.05
398 | if event.ctrl:
399 | factor = 0.15
400 | elif event.shift:
401 | factor = 0.005
402 | cam_offset = delta * factor
403 | self.update_camera(cam_offset=cam_offset, use_cam_offset=True)
404 |
405 | return {'RUNNING_MODAL'}
406 |
407 |
408 | classes = (
409 | CAM_MANAGER_OT_dolly_zoom,
410 | )
411 |
412 |
413 | def register():
414 | from bpy.utils import register_class
415 |
416 | for cls in classes:
417 | register_class(cls)
418 |
419 |
420 | def unregister():
421 | from bpy.utils import unregister_class
422 |
423 | for cls in reversed(classes):
424 | unregister_class(cls)
425 |
--------------------------------------------------------------------------------
/keymap.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | keymaps_items_dict = {
4 | "Simple Camera Manager": {"name": 'cam_menu', "idname": 'wm.call_panel', "operator":
5 | 'OBJECT_PT_camera_manager_popup', "type": 'C', "value": 'PRESS',
6 | "ctrl": False, "shift": True, "alt": True, "active": True},
7 | "Active Camera Pie": {"name": 'cam_pie', "idname": 'wm.call_menu_pie',
8 | "operator": 'CAMERA_MT_pie_menu',
9 | "type": 'C', "value": 'PRESS', "ctrl": False, "shift": False, "alt": True, "active": True},
10 | "Next Camera": {"name": 'next_cam', "idname": 'cam_manager.cycle_cameras_next',
11 | "operator": '', "type": 'RIGHT_ARROW',
12 | "value": 'PRESS', "ctrl": True, "shift": True, "alt": False, "active": True},
13 | "Previous Camera": {"name": 'prev_cam', "idname": 'cam_manager.cycle_cameras_backward',
14 | "operator": '', "type": 'LEFT_ARROW', "value": 'PRESS', "ctrl": True, "shift": True,
15 | "alt": False, "active": True}}
16 |
17 | def get_keymap_string(item_id, item_type):
18 | # Get all keymaps
19 | keymaps = bpy.context.window_manager.keyconfigs.user.keymaps
20 |
21 | # Find the keymap item for the given item_id and item_type
22 | keymap_item = None
23 | for km in keymaps:
24 | for kmi in km.keymap_items:
25 | if item_type == "PANEL" and kmi.idname == "wm.call_panel" and kmi.properties.name == item_id:
26 | keymap_item = kmi
27 | break
28 | elif item_type == "MENU" and kmi.idname == "wm.call_menu_pie":
29 | if kmi.properties.name == item_id:
30 | keymap_item = kmi
31 | break
32 | elif item_type == "OPERATOR" and kmi.idname == item_id:
33 | keymap_item = kmi
34 | break
35 | if keymap_item:
36 | break
37 |
38 | if not keymap_item:
39 | return "Keymap not found"
40 |
41 | # Extract the key information
42 | modifiers = []
43 | if keymap_item.ctrl:
44 | modifiers.append("Ctrl")
45 | if keymap_item.alt:
46 | modifiers.append("Alt")
47 | if keymap_item.shift:
48 | modifiers.append("Shift")
49 | if keymap_item.oskey:
50 | modifiers.append("Cmd" if bpy.app.build_platform == 'Darwin' else "Win")
51 |
52 | key = keymap_item.type
53 |
54 | # Return the keymap in the desired format
55 | return " + ".join(modifiers + [key])
56 |
57 |
58 |
59 | def add_key(context, idname, type, ctrl, shift, alt, operator, active):
60 | km = context.window_manager.keyconfigs.addon.keymaps.new(name="Window")
61 |
62 | kmi = km.keymap_items.new(idname=idname, type=type, value='PRESS',
63 | ctrl=ctrl, shift=shift,
64 | alt=alt)
65 |
66 | if operator != '':
67 | add_key_to_keymap(operator, kmi, active=active)
68 |
69 |
70 | def remove_key(context, idname, properties_name):
71 | """Removes addon hotkeys from the keymap"""
72 | wm = bpy.context.window_manager
73 | km = wm.keyconfigs.addon.keymaps['Window']
74 |
75 | for kmi in km.keymap_items:
76 | if properties_name:
77 | if kmi.idname == idname and kmi.properties.name == properties_name:
78 | km.keymap_items.remove(kmi)
79 | else:
80 | if kmi.idname == idname:
81 | km.keymap_items.remove(kmi)
82 |
83 |
84 | def add_keymap():
85 | context = bpy.context
86 | prefs = context.preferences.addons[__package__].preferences
87 |
88 | for key, valueDic in keymaps_items_dict.items():
89 | idname = valueDic["idname"]
90 | type = getattr(prefs, f'{valueDic["name"]}_type')
91 | ctrl = getattr(prefs, f'{valueDic["name"]}_ctrl')
92 | shift = getattr(prefs, f'{valueDic["name"]}_shift')
93 | alt = getattr(prefs, f'{valueDic["name"]}_alt')
94 | operator = valueDic["operator"]
95 | active = valueDic["active"]
96 | add_key(context, idname, type, ctrl, shift, alt, operator, active)
97 |
98 |
99 | def add_key_to_keymap(idname, kmi, active=True):
100 | """ Add ta key to the appropriate keymap """
101 | kmi.properties.name = idname
102 | kmi.active = active
103 |
104 |
105 | def remove_keymap():
106 | wm = bpy.context.window_manager
107 | addon_keymaps = wm.keyconfigs.addon.keymaps.get('Window')
108 |
109 | if not addon_keymaps:
110 | return
111 |
112 | # Collect items to remove first
113 | items_to_remove = []
114 | for kmi in addon_keymaps.keymap_items:
115 | for key, valueDic in keymaps_items_dict.items():
116 | idname = valueDic["idname"]
117 | operator = valueDic["operator"]
118 | if kmi.idname == idname and (not operator or getattr(kmi.properties, 'name', '') == operator):
119 | items_to_remove.append(kmi)
120 |
121 | # Remove items
122 | for kmi in items_to_remove:
123 | addon_keymaps.keymap_items.remove(kmi)
124 |
125 |
126 | class REMOVE_OT_hotkey(bpy.types.Operator):
127 | """Tooltip"""
128 | bl_idname = "cam.remove_hotkey"
129 | bl_label = "Remove hotkey"
130 | bl_description = "Remove hotkey"
131 | bl_options = {'REGISTER', 'INTERNAL'}
132 |
133 | idname: bpy.props.StringProperty()
134 | properties_name: bpy.props.StringProperty()
135 | property_prefix: bpy.props.StringProperty()
136 |
137 | def execute(self, context):
138 | remove_key(context, self.idname, self.properties_name)
139 |
140 | prefs = bpy.context.preferences.addons[__package__].preferences
141 | setattr(prefs, f'{self.property_prefix}_type', "NONE")
142 | setattr(prefs, f'{self.property_prefix}_ctrl', False)
143 | setattr(prefs, f'{self.property_prefix}_shift', False)
144 | setattr(prefs, f'{self.property_prefix}_alt', False)
145 |
146 | return {'FINISHED'}
147 |
148 |
149 | class BUTTON_OT_change_key(bpy.types.Operator):
150 | """UI button to assign a new key to an addon hotkey"""
151 | bl_idname = "cam.key_selection_button"
152 | bl_label = "Press the button you want to assign to this operation."
153 | bl_options = {'REGISTER', 'INTERNAL'}
154 |
155 | property_prefix: bpy.props.StringProperty()
156 |
157 | def __init__(self):
158 | self.my_event = ''
159 |
160 | def invoke(self, context, event):
161 | prefs = bpy.context.preferences.addons[__package__].preferences
162 | self.prefs = prefs
163 | setattr(prefs, f'{self.property_prefix}_type', "NONE")
164 |
165 | context.window_manager.modal_handler_add(self)
166 | return {'RUNNING_MODAL'}
167 |
168 | def modal(self, context, event):
169 | self.my_event = 'NONE'
170 |
171 | if event.type and event.value == 'RELEASE': # Apply
172 | self.my_event = event.type
173 |
174 | setattr(self.prefs, f'{self.property_prefix}_type', self.my_event)
175 | self.execute(context)
176 | return {'FINISHED'}
177 |
178 | return {'RUNNING_MODAL'}
179 |
180 | def execute(self, context):
181 | self.report({'INFO'},
182 | "Key change: " + bpy.types.Event.bl_rna.properties['type'].enum_items[self.my_event].name)
183 | return {'FINISHED'}
184 |
185 |
186 | classes = (
187 | BUTTON_OT_change_key,
188 | REMOVE_OT_hotkey,
189 | )
190 |
191 |
192 | def register():
193 | from bpy.utils import register_class
194 | for cls in classes:
195 | register_class(cls)
196 |
197 |
198 | def unregister():
199 | remove_keymap()
200 |
201 | from bpy.utils import unregister_class
202 | for cls in reversed(classes):
203 | unregister_class(cls)
204 |
--------------------------------------------------------------------------------
/pie_menu.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import Menu
3 |
4 |
5 | # spawn an edit mode selection pie (run while object is in edit mode to get a valid output)
6 |
7 | def draw_camera_settings(context, layout, cam_obj, use_subpanel=False):
8 | if cam_obj is None:
9 | return
10 |
11 | cam = cam_obj.data
12 |
13 | # Resolution
14 | row = layout.row(align=True)
15 | row.prop(cam, "resolution", text="Resolution")
16 |
17 | # Lens
18 | col = layout.column(align=True)
19 | row = col.row(align=True)
20 | row.prop(cam, "lens")
21 | row = col.row(align=True)
22 | row.prop(cam, 'angle')
23 |
24 | col = layout.column(align=True)
25 | row = col.row(align=True)
26 | row.prop(cam, "clip_start")
27 | row = col.row(align=True)
28 | row.prop(cam, "clip_end")
29 |
30 | def draw_focus_settings(layout):
31 | dof = cam.dof
32 | layout.label(text="Focus:")
33 | layout.prop(dof, 'use_dof')
34 |
35 | col = layout.column()
36 | col.prop(dof, "focus_object", text="Focus on Object")
37 | if dof.focus_object and dof.focus_object.type == 'ARMATURE':
38 | col.prop_search(dof, "focus_subtarget", dof.focus_object.data, "bones", text="Focus Bone")
39 |
40 | sub = col.row()
41 | sub.active = dof.focus_object is None
42 | sub.prop(dof, "focus_distance", text="Focus Distance")
43 | sub.operator(
44 | "ui.eyedropper_depth",
45 | icon='EYEDROPPER',
46 | text=""
47 | ).prop_data_path = "scene.camera.data.dof.focus_distance"
48 |
49 | def draw_lighting_settings(layout):
50 | layout.label(text="Camera Lighting:")
51 | col = layout.column(align=False)
52 | row = col.row(align=True)
53 | row.prop(cam, 'exposure', text='Exposure')
54 | row = col.row(align=True)
55 | row.prop(cam, 'world', text='World')
56 |
57 | def draw_background_image_settings(layout):
58 | layout.label(text="Background Image:")
59 | col = layout.column(align=True)
60 | if not cam_obj.visible_get():
61 | row = layout.row(align=True)
62 | row.label(text="Camera is hidden", icon='ERROR')
63 |
64 | if len(cam.background_images) > 0:
65 | for img in cam.background_images:
66 | row = col.row(align=True)
67 | row.prop(img, "show_background_image")
68 | row = col.row(align=True)
69 | op = row.operator("cam_manager.camera_resolutio_from_image", icon='IMAGE_BACKGROUND')
70 | op.camera_name = cam.name
71 | row = col.row(align=True)
72 | row.prop(img, "alpha")
73 | row = col.row(align=True)
74 | row.prop(img, "display_depth")
75 | else:
76 | row = col.row(align=True)
77 | row.label(text="Camera has no Background Images", icon='INFO')
78 |
79 | # Conditionally use subpanels or direct layout
80 | if use_subpanel:
81 | header, body = layout.panel(idname="FOCUS_PANEL", default_closed=True)
82 | header.label(text="Focus:")
83 | if body:
84 | draw_focus_settings(body)
85 |
86 | header, body = layout.panel(idname="LIGHT_PANEL", default_closed=True)
87 | header.label(text="Camera Lighting:")
88 | if body:
89 | draw_lighting_settings(body)
90 |
91 | header, body = layout.panel(idname="BACKGROUND_IMG", default_closed=True)
92 | header.label(text="Background Image:")
93 | if body:
94 | draw_background_image_settings(body)
95 | else:
96 | draw_focus_settings(layout)
97 | draw_lighting_settings(layout)
98 | draw_background_image_settings(layout)
99 |
100 |
101 | class CAM_MANAGER_MT_PIE_camera_settings(Menu):
102 | # label is displayed at the center of the pie menu.
103 | bl_label = "Active Camera Pie "
104 | bl_idname = "CAMERA_MT_pie_menu"
105 |
106 | def draw(self, context):
107 | layout = self.layout
108 |
109 | view = bpy.context.space_data
110 | scene = context.scene
111 | r3d = view.region_3d
112 |
113 | pie = layout.menu_pie()
114 | # operator_enum will just spread all available options
115 | # for the type enum of the operator on the pie
116 |
117 | cam_obj = scene.camera
118 |
119 | # West
120 | pie.operator("cam_manager.cycle_cameras_next", text="previous", icon='TRIA_LEFT')
121 |
122 | # East
123 | pie.operator("cam_manager.cycle_cameras_backward", text="next", icon='TRIA_RIGHT')
124 |
125 | # South lock camrea to view
126 | pie.prop(view, "lock_camera")
127 |
128 | # North
129 | box = pie.split()
130 |
131 | if cam_obj:
132 | b = box.box()
133 | column = b.column()
134 | self.draw_left_column(context, column, cam_obj)
135 |
136 | b = box.box()
137 | column = b.column()
138 | self.draw_center_column(context, column, cam_obj)
139 |
140 | b = box.box()
141 | column = b.column()
142 | self.draw_right_column(context, column, cam_obj)
143 | else:
144 | b = box.box()
145 | column = b.column()
146 | column.label(text="Please specify a scene camera", icon='ERROR')
147 |
148 | # North West
149 | pie.separator()
150 |
151 | # North East
152 | pie.separator()
153 |
154 | # South West
155 | if cam_obj:
156 | if cam_obj.get('lock'):
157 | op = pie.operator("cam_manager.lock_unlock_camera", icon='UNLOCKED', text='')
158 | op.camera_name = cam_obj.name
159 | op.cam_lock = False
160 | else:
161 | op = pie.operator("cam_manager.lock_unlock_camera", icon='LOCKED', text='')
162 | op.camera_name = cam_obj.name
163 | op.cam_lock = True
164 | else:
165 | pie.separator()
166 |
167 | # South East
168 | pie.operator('cameras.select_active_cam')
169 |
170 | # pie.operator("view3d.view_camera", text="Toggle Camera View", icon='VIEW_CAMERA')
171 |
172 | def draw_left_column(self, context, col, cam_obj):
173 | row = col.row()
174 | row.scale_y = 1.5
175 |
176 | cam = cam_obj.data
177 | scene = context.scene
178 |
179 | row = col.row(align=True)
180 | row.label(text='Render settings')
181 | row = col.row(align=True)
182 | row.prop(cam, "slot")
183 | op = row.operator('cameras.custom_render', text='Render', icon='RENDER_STILL')
184 | op.camera_name = cam_obj.name
185 |
186 | row = col.row(align=True)
187 | row.label(text='Cameras Collection')
188 | row = col.row(align=True)
189 |
190 | # hide visibility settings for collection if it doesn't yet exist
191 | if scene.cam_collection.collection:
192 | row.prop(scene.cam_collection.collection, 'hide_viewport', text='')
193 | row.prop(context.view_layer.layer_collection.children[scene.cam_collection.collection.name],
194 | 'hide_viewport', text='')
195 |
196 | row.prop_search(scene.cam_collection, "collection", bpy.data, "collections", text='')
197 | op = row.operator("cameras.add_collection", icon='OUTLINER_COLLECTION', text='')
198 | op.object_name = cam_obj.name
199 |
200 | row = col.row(align=True)
201 | row.label(text='Background Images')
202 |
203 | if not cam_obj.visible_get():
204 | row = col.row(align=True)
205 | row.label(text="Camera is hidden", icon='ERROR')
206 |
207 | if len(cam.background_images) > 0:
208 | for img in cam.background_images:
209 | row = col.row(align=True)
210 | row.prop(img, "show_background_image")
211 | row = col.row(align=True)
212 | op = row.operator("cam_manager.camera_resolutio_from_image", icon='IMAGE_BACKGROUND')
213 | op.camera_name = cam.name
214 | row = col.row(align=True)
215 | row.prop(img, "alpha")
216 | row = col.row(align=True)
217 | row.prop(img, "display_depth")
218 | else:
219 | row = col.row(align=True)
220 | row.label(text="Camera has no Backround Images", icon='INFO')
221 |
222 | def draw_center_column(self, context, layout, cam_obj):
223 | row = layout.row(align=True)
224 | row.label(text='Camera Settings')
225 |
226 | draw_camera_settings(context, layout, cam_obj, use_subpanel=False)
227 |
228 | def draw_right_column(self, context, col, cam_obj):
229 | # col.scale_x = 2
230 |
231 | row = col.row()
232 | # row.scale_y = 1.5
233 |
234 | cam = cam_obj.data
235 |
236 | view = context.space_data
237 | overlay = view.overlay
238 | shading = view.shading
239 |
240 | col.use_property_decorate = False
241 | row = col.row(align=True)
242 | row.prop(overlay, "show_overlays", icon='OVERLAY', text="")
243 | row.label(text='Viewport Display')
244 | row = col.row(align=True)
245 |
246 | row.separator()
247 |
248 | row = col.row(align=True)
249 | row.label(text='Passepartout')
250 | row = col.row(align=True)
251 | row.prop(cam, "show_passepartout", text="")
252 | row.active = cam.show_passepartout
253 | row.prop(cam, "passepartout_alpha", text="")
254 | row.prop_decorator(cam, "passepartout_alpha")
255 |
256 | row = col.row(align=True)
257 | row.prop(cam, 'show_name')
258 |
259 | row = col.row()
260 | row.prop(cam, "show_composition_thirds")
261 | row = col.row()
262 | row.prop(cam, "show_composition_center")
263 | row = col.row()
264 | row.prop(cam, "show_composition_center_diagonal")
265 | row = col.row()
266 | row.prop(cam, "show_composition_golden")
267 |
268 |
269 | classes = (
270 | CAM_MANAGER_MT_PIE_camera_settings,
271 | )
272 |
273 |
274 | def register():
275 | from bpy.utils import register_class
276 |
277 | for cls in classes:
278 | register_class(cls)
279 |
280 |
281 | def unregister():
282 | from bpy.utils import unregister_class
283 |
284 | for cls in reversed(classes):
285 | unregister_class(cls)
286 |
--------------------------------------------------------------------------------
/preferences.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import textwrap
3 |
4 | from .keymap import add_keymap, add_key
5 | from .keymap import keymaps_items_dict
6 | from .keymap import remove_key
7 | from .ui import VIEW3D_PT_SimpleCameraManager
8 |
9 |
10 | def label_multiline(context, text, parent):
11 | """
12 | Draw a label with multiline text in the layout.
13 |
14 | Args:
15 | context (Context): The current context.
16 | text (str): The text to display.
17 | parent (UILayout): The parent layout to add the label to.
18 | """
19 | chars = int(context.region.width / 7) # 7 pix on 1 character
20 | wrapper = textwrap.TextWrapper(width=chars)
21 | text_lines = wrapper.wrap(text=text)
22 | for text_line in text_lines:
23 | parent.label(text=text_line)
24 |
25 |
26 | def update_panel_category(self, context):
27 | """Update panel tab for simple export"""
28 | panels = [
29 | VIEW3D_PT_SimpleCameraManager,
30 | ]
31 |
32 | for panel in panels:
33 | try:
34 | bpy.utils.unregister_class(panel)
35 | except:
36 | pass
37 |
38 | prefs = context.preferences.addons[__package__].preferences
39 | panel.bl_category = prefs.panel_category
40 |
41 | if prefs.enable_n_panel:
42 | try:
43 | bpy.utils.register_class(panel)
44 | except ValueError:
45 | pass # Avoid duplicate registrations
46 | return
47 |
48 |
49 | def update_key(self, context, idname, operator_name, property_prefix):
50 | # This functions gets called when the hotkey assignment is updated in the preferences
51 | wm = context.window_manager
52 | km = wm.keyconfigs.addon.keymaps["Window"]
53 |
54 | prefs = context.preferences.addons[__package__.split('.')[0]].preferences
55 |
56 | # Remove previous key assignment
57 | remove_key(context, idname, operator_name)
58 | add_key(context, idname, getattr(prefs, f'{property_prefix}_type'),
59 | getattr(prefs, f'{property_prefix}_ctrl'), getattr(prefs, f'{property_prefix}_shift'),
60 | getattr(prefs, f'{property_prefix}_alt'), operator_name, getattr(prefs, f'{property_prefix}_active'))
61 |
62 |
63 | def update_next_cam_key(self, context):
64 | key_entry = keymaps_items_dict["Next Camera"]
65 | idname = key_entry["idname"]
66 | name = key_entry["name"]
67 | operator_name = key_entry["operator"]
68 | update_key(self, context, idname, operator_name, name)
69 |
70 |
71 | def update_prev_cam_key(self, context):
72 | key_entry = keymaps_items_dict["Previous Camera"]
73 | idname = key_entry["idname"]
74 | name = key_entry["name"]
75 | operator_name = key_entry["operator"]
76 | update_key(self, context, idname, operator_name, name)
77 |
78 |
79 | def update_cam_pie_key(self, context):
80 | key_entry = keymaps_items_dict["Active Camera Pie"]
81 | idname = key_entry["idname"]
82 | name = key_entry["name"]
83 | operator_name = key_entry["operator"]
84 | update_key(self, context, idname, operator_name, name)
85 |
86 |
87 | def update_cam_menu_key(self, context):
88 | key_entry = keymaps_items_dict["Simple Camera Manager"]
89 | idname = key_entry["idname"]
90 | name = key_entry["name"]
91 | operator_name = key_entry["operator"]
92 | update_key(self, context, idname, operator_name, name)
93 |
94 |
95 | # addon Preferences
96 | class CAM_MANAGER_OT_renaming_preferences(bpy.types.AddonPreferences):
97 | """Contains the blender addon preferences"""
98 | # this must match the addon name, use '__package__'
99 | # when defining this in a submodule of a python package.
100 | bl_idname = __package__ ### __package__ works on multifile and __name__ not
101 |
102 | # addon updater preferences
103 |
104 | prefs_tabs: bpy.props.EnumProperty(items=(('GENERAL', "General", "General Settings"),
105 | ('KEYMAPS', "Keymaps", "Keymap Settings"),
106 | ('SUPPORT', "Support", "Support me")),
107 | default='GENERAL')
108 |
109 | next_cam_type: bpy.props.StringProperty(
110 | name="Renaming Popup",
111 | default=keymaps_items_dict["Next Camera"]['type'],
112 | update=update_next_cam_key
113 | )
114 |
115 | next_cam_ctrl: bpy.props.BoolProperty(
116 | name="Ctrl",
117 | default=keymaps_items_dict["Next Camera"]['ctrl'],
118 | update=update_next_cam_key
119 | )
120 |
121 | next_cam_shift: bpy.props.BoolProperty(
122 | name="Shift",
123 | default=keymaps_items_dict["Next Camera"]['shift'],
124 | update=update_next_cam_key
125 | )
126 | next_cam_alt: bpy.props.BoolProperty(
127 | name="Alt",
128 | default=keymaps_items_dict["Next Camera"]['alt'],
129 | update=update_next_cam_key
130 | )
131 |
132 | next_cam_active: bpy.props.BoolProperty(
133 | name="Active",
134 | default=keymaps_items_dict["Next Camera"]['active'],
135 | update=update_next_cam_key
136 | )
137 |
138 | prev_cam_type: bpy.props.StringProperty(
139 | name="Renaming Popup",
140 | default=keymaps_items_dict["Previous Camera"]["type"],
141 | update=update_prev_cam_key
142 | )
143 |
144 | prev_cam_ctrl: bpy.props.BoolProperty(
145 | name="Ctrl",
146 | default=keymaps_items_dict["Previous Camera"]["ctrl"],
147 | update=update_prev_cam_key
148 | )
149 |
150 | prev_cam_shift: bpy.props.BoolProperty(
151 | name="Shift",
152 | default=keymaps_items_dict["Previous Camera"]["shift"],
153 | update=update_prev_cam_key
154 | )
155 | prev_cam_alt: bpy.props.BoolProperty(
156 | name="Alt",
157 | default=keymaps_items_dict["Previous Camera"]["alt"],
158 | update=update_prev_cam_key
159 | )
160 |
161 | prev_cam_active: bpy.props.BoolProperty(
162 | name="Active",
163 | default=keymaps_items_dict["Previous Camera"]["active"],
164 | update=update_prev_cam_key
165 | )
166 |
167 | cam_pie_type: bpy.props.StringProperty(
168 | name="Renaming Popup",
169 | default=keymaps_items_dict["Active Camera Pie"]["type"],
170 | update=update_cam_pie_key
171 | )
172 |
173 | cam_pie_ctrl: bpy.props.BoolProperty(
174 | name="Ctrl",
175 | default=keymaps_items_dict["Active Camera Pie"]["ctrl"],
176 | update=update_cam_pie_key
177 | )
178 |
179 | cam_pie_shift: bpy.props.BoolProperty(
180 | name="Shift",
181 | default=keymaps_items_dict["Active Camera Pie"]["shift"],
182 | update=update_cam_pie_key
183 | )
184 | cam_pie_alt: bpy.props.BoolProperty(
185 | name="Alt",
186 | default=keymaps_items_dict["Active Camera Pie"]["alt"],
187 | update=update_cam_pie_key
188 | )
189 |
190 | cam_pie_active: bpy.props.BoolProperty(
191 | name="Active",
192 | default=keymaps_items_dict["Active Camera Pie"]["active"],
193 | update=update_cam_pie_key
194 | )
195 |
196 | cam_menu_type: bpy.props.StringProperty(
197 | name="Renaming Popup",
198 | default=keymaps_items_dict["Simple Camera Manager"]["type"],
199 | update=update_cam_menu_key
200 | )
201 |
202 | cam_menu_ctrl: bpy.props.BoolProperty(
203 | name="Ctrl",
204 | default=keymaps_items_dict["Simple Camera Manager"]["ctrl"],
205 | update=update_cam_menu_key
206 | )
207 |
208 | cam_menu_shift: bpy.props.BoolProperty(
209 | name="Shift",
210 | default=keymaps_items_dict["Simple Camera Manager"]["shift"],
211 | update=update_cam_menu_key
212 | )
213 | cam_menu_alt: bpy.props.BoolProperty(
214 | name="Alt",
215 | default=keymaps_items_dict["Simple Camera Manager"]["alt"],
216 | update=update_cam_menu_key
217 | )
218 |
219 | cam_menu_active: bpy.props.BoolProperty(
220 | name="Active",
221 | default=keymaps_items_dict["Simple Camera Manager"]["active"],
222 | update=update_cam_menu_key
223 | )
224 |
225 | panel_category: bpy.props.StringProperty(name="Category Tab",
226 | description="The category name used to organize the addon in the properties panel for all the addons",
227 | default='Simple Camera Manager',
228 | update=update_panel_category) # update = update_panel_position,
229 |
230 | enable_n_panel: bpy.props.BoolProperty(
231 | name="Enable Simple Camera Manager N-Panel",
232 | description="Toggle the N-Panel on and off.",
233 | default=True,
234 | update=update_panel_category)
235 |
236 | def keymap_ui(self, layout, title, property_prefix, id_name, properties_name):
237 | box = layout.box()
238 | split = box.split(align=True, factor=0.5)
239 | col = split.column()
240 |
241 | # Is hotkey active checkbox
242 | row = col.row(align=True)
243 | row.prop(self, f'{property_prefix}_active', text="")
244 | row.label(text=title)
245 |
246 | # Button to assign the key assignments
247 | col = split.column()
248 | row = col.row(align=True)
249 | key_type = getattr(self, f'{property_prefix}_type')
250 | text = (
251 | bpy.types.Event.bl_rna.properties['type'].enum_items[key_type].name
252 | if key_type != 'NONE'
253 | else 'Press a key'
254 | )
255 |
256 | op = row.operator("cam.key_selection_button", text=text)
257 | op.property_prefix = property_prefix
258 | # row.prop(self, f'{property_prefix}_type', text="")
259 | op = row.operator("cam.remove_hotkey", text="", icon="X")
260 | op.idname = id_name
261 | op.properties_name = properties_name
262 | op.property_prefix = property_prefix
263 |
264 | row = col.row(align=True)
265 | row.prop(self, f'{property_prefix}_ctrl')
266 | row.prop(self, f'{property_prefix}_shift')
267 | row.prop(self, f'{property_prefix}_alt')
268 |
269 | # Gizmos
270 | show_dolly_gizmo: bpy.props.BoolProperty(name='Dolly Zoom', description='Show the dolly gizmo', default=False)
271 |
272 | def draw(self, context):
273 | """ simple preference UI to define custom inputs and user preferences"""
274 | layout = self.layout
275 |
276 | row = layout.row(align=True)
277 | row.prop(self, "prefs_tabs", expand=True)
278 |
279 | if self.prefs_tabs == 'GENERAL':
280 | box = layout.box()
281 | box.label(text="UI")
282 | box.prop(self, 'enable_n_panel')
283 | box.prop(self, 'panel_category')
284 | # updater draw function
285 | # could also pass in col as third arg
286 |
287 |
288 | box = layout.box()
289 | box.label(text="Gizmos")
290 | row = box.row()
291 | row.label(text='Always show Gizmo')
292 | row = box.row()
293 | row.prop(self, "show_dolly_gizmo", expand=True)
294 |
295 |
296 |
297 |
298 |
299 | # Settings regarding the keymap
300 | elif self.prefs_tabs == 'KEYMAPS':
301 | box = layout.box()
302 |
303 | for title, value in keymaps_items_dict.items():
304 | self.keymap_ui(box, title, value['name'], value["idname"], value["operator"])
305 |
306 |
307 | elif self.prefs_tabs == 'SUPPORT':
308 | # Cross Promotion
309 |
310 | text = "Explore my other Blender Addons designed for more efficient game asset workflows!"
311 | label_multiline(
312 | context=context,
313 | text=text,
314 | parent=layout
315 | )
316 |
317 | layout.label(text="♥♥♥ Leave a Review or Rating! ♥♥♥")
318 | col = layout.column(align=True)
319 |
320 | row = col.row(align=True)
321 | row.label(text="Simple Camera Manager")
322 | row.operator("wm.url_open", text="Superhive",
323 | icon="URL").url = "https://superhivemarket.com/products/simple-camera-manager"
324 | row.operator("wm.url_open", text="Gumroad",
325 | icon="URL").url = "https://weisl.gumroad.com/l/simple_camera_manager"
326 |
327 |
328 | layout.label(text="Other Simple Tools ($)")
329 |
330 | col = layout.column(align=True)
331 | row = col.row(align=True)
332 | row.label(text="Simple Collider")
333 | row.operator("wm.url_open", text="Superhive",
334 | icon="URL").url = "https://superhivemarket.com/products/simple-collider"
335 | row.operator("wm.url_open", text="Gumroad",
336 | icon="URL").url = "https://weisl.gumroad.com/l/simple_collider"
337 |
338 | # row = col.row(align=True)
339 | # row.label(text="Simple Export")
340 | # row.operator("wm.url_open", text="Superhive",
341 | # icon="URL").url = "https://superhivemarket.com/products/simple-export"
342 | # row.operator("wm.url_open", text="Gumroad",
343 | # icon="URL").url = "https://weisl.gumroad.com/l/simple_export"
344 |
345 | layout.label(text="Free Simple Tools")
346 | col = layout.column(align=True)
347 | row = col.row(align=True)
348 | row.label(text="Simple Renaming")
349 | row.operator("wm.url_open", text="Blender Extensions",
350 | icon="URL").url = "https://extensions.blender.org/add-ons/simple-renaming-panel/"
351 | row.operator("wm.url_open", text="Gumroad",
352 | icon="URL").url = "https://weisl.gumroad.com/l/simple_renaming"
353 |
354 | col = layout.column(align=True)
355 | row = col.row()
356 | row.label(text='Support & Feedback')
357 | row = col.row()
358 | row.label(text='Support is primarily provided through the store pages for Superhive and Gumroad.')
359 | row.label(text='Questions or Feedback?')
360 | row = col.row()
361 | row.operator("wm.url_open", text="Join Discord", icon="URL").url = "https://discord.gg/VRzdcFpczm"
362 |
363 |
364 | classes = (
365 | CAM_MANAGER_OT_renaming_preferences,
366 | )
367 |
368 |
369 | def register():
370 | from bpy.utils import register_class
371 |
372 | for cls in classes:
373 | register_class(cls)
374 |
375 | add_keymap()
376 |
377 | # Initialize correct property panel for the Simple Export Panel
378 | update_panel_category(None, bpy.context)
379 |
380 | def unregister():
381 | from bpy.utils import unregister_class
382 |
383 | for cls in reversed(classes):
384 | unregister_class(cls)
385 |
--------------------------------------------------------------------------------
/ui.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import bpy
4 |
5 | from .keymap import get_keymap_string
6 | from .pie_menu import draw_camera_settings
7 |
8 |
9 | def get_addon_name():
10 | """
11 | Returns the addon name as a string.
12 | """
13 | return "Simple Camera Manager"
14 |
15 |
16 | def draw_simple_camera_manager_header(layout):
17 | row = layout.row(align=True)
18 | # Open documentation
19 | row.operator("wm.url_open", text="", icon="HELP").url = "https://weisl.github.io/camera_manager_Overview/"
20 |
21 | # Open Preferences
22 | addon_name = get_addon_name()
23 | op = row.operator("simple_camera.open_preferences", text="", icon='PREFERENCES')
24 | op.addon_name = addon_name
25 | op.prefs_tabs = 'GENERAL'
26 |
27 | # Open Export Popup
28 | op = row.operator("wm.call_panel", text="", icon="WINDOW")
29 | op.name = "OBJECT_PT_camera_manager_popup"
30 |
31 | # Display the combined label and keymap information
32 | row.label(text=f"Simple Camera Manager")
33 |
34 |
35 | class CAMERA_OT_open_in_explorer(bpy.types.Operator):
36 | """Open render output directory in Explorer"""
37 | bl_idname = "cameras.open_in_explorer"
38 | bl_label = "Open Folder"
39 | bl_description = "Open the render output folder in explorer"
40 |
41 | def execute(self, context):
42 | filepath = os.path.dirname(os.path.abspath(context.scene.render.filepath))
43 | bpy.ops.file.external_operation(filepath=filepath, operation='FOLDER_OPEN')
44 | return {'FINISHED'}
45 |
46 |
47 | class VIEW3D_PT_SimpleCameraManager(bpy.types.Panel):
48 | """Creates a Panel in the Object properties window"""
49 |
50 | bl_space_type = 'VIEW_3D'
51 | bl_region_type = 'UI'
52 | bl_category = "Simple Camera Manager"
53 | bl_label = ""
54 |
55 | def draw_header(self, context):
56 | layout = self.layout
57 | draw_simple_camera_manager_header(layout)
58 |
59 | def draw(self, context):
60 | layout = self.layout
61 |
62 | scene = context.scene
63 |
64 | row = layout.row()
65 | cam = scene.camera
66 |
67 | if cam:
68 | row.label(text=f"Active Camera: {cam.name}", icon='VIEW_CAMERA')
69 |
70 | # template_list now takes two new args.
71 | # The first one is the identifier of the registered UIList to use (if you want only the default list,
72 | # with no custom draw code, use "UI_UL_list").
73 |
74 | row = layout.row()
75 | row.template_list("CAMERA_UL_cameras_scene", "", scene, "objects", scene, "camera_list_index")
76 | col = row.column(align=True)
77 | col.operator("cam_manager.cycle_cameras_backward", text="", icon='TRIA_UP')
78 | col.operator("cam_manager.cycle_cameras_next", text="", icon='TRIA_DOWN')
79 | col.menu("OBJECT_MT_camera_list_dropdown_menu", icon='DOWNARROW_HLT', text="")
80 |
81 | row = layout.row()
82 | row.operator("cam_manager.multi_camera_rendering_handlers", text="Batch Render ", icon="RENDER_ANIMATION")
83 | row = layout.row()
84 | row.prop(context.scene.render, 'filepath', text='Folder')
85 | # col.operator("cam_manager.multi_camera_rendering_modal", text="Batch Render (Background)", icon="FILE_SCRIPT")
86 |
87 | # Get the keymap for the panel
88 | panel_keymap = get_keymap_string("OBJECT_PT_camera_manager_popup", "PANEL")
89 | menu_keymap = get_keymap_string("CAMERA_MT_pie_menu", "MENU")
90 | operator1_keymap = get_keymap_string("cam_manager.cycle_cameras_backward", "OPERATOR")
91 | operator2_keymap = get_keymap_string("cam_manager.cycle_cameras_next", "OPERATOR")
92 |
93 | # Draw the panel header
94 | header, body = layout.panel(idname="ACTIVE_COL_PANEL", default_closed=False)
95 | header.label(text=f"Active Camera", icon='OUTLINER_COLLECTION')
96 |
97 | if body:
98 | cam_obj = context.scene.camera
99 | draw_camera_settings(context, body, cam_obj, use_subpanel=True)
100 |
101 | layout.label(text='Dolly Zoom', icon='VIEW_CAMERA')
102 | col = layout.column(align=True)
103 | row = col.row(align=True)
104 | row.operator("cam_manager.modal_camera_dolly_zoom", text="Dolly Zoom", icon='CON_CAMERASOLVER')
105 |
106 | row = col.row(align=True)
107 | prefs = context.preferences.addons[__package__].preferences
108 | row.prop(prefs, "show_dolly_gizmo", text="Gizmo")
109 |
110 | layout.separator()
111 | layout.menu(CameraOperatorDropdownMenu.bl_idname, icon='OUTLINER_COLLECTION')
112 |
113 | layout.separator()
114 | layout.label(text='Keymap')
115 | # Display the combined label and keymap information
116 | layout.label(text=f"Camera Manager ({panel_keymap})")
117 | layout.label(text=f"Camera Pie ({menu_keymap})")
118 | layout.label(text=f"Previous Cam ({operator1_keymap})")
119 | layout.label(text=f"Next Cam ({operator2_keymap})")
120 |
121 |
122 | class CAM_MANAGER_PT_scene_panel:
123 | """Properties Panel in the scene tab"""
124 | bl_space_type = 'PROPERTIES'
125 | bl_region_type = 'WINDOW'
126 | bl_context = "scene"
127 |
128 |
129 | class CAM_MANAGER_PT_scene_properties(CAM_MANAGER_PT_scene_panel, bpy.types.Panel):
130 | bl_idname = "OBJECT_PT_camera_manager"
131 | bl_label = ""
132 | bl_region_type = 'WINDOW'
133 | bl_context = "scene"
134 |
135 | def draw_header(self, context):
136 | layout = self.layout
137 | draw_simple_camera_manager_header(layout)
138 |
139 | def draw(self, context):
140 | layout = self.layout
141 |
142 | scene = context.scene
143 |
144 | row = layout.row()
145 | row.prop(scene, "camera")
146 |
147 | row = layout.row()
148 | # template_list now takes two new args.
149 | # The first one is the identifier of the registered UIList to use (if you want only the default list,
150 | # with no custom draw code, use "UI_UL_list").
151 |
152 | row = layout.row()
153 | row.template_list("CAMERA_UL_cameras_scene", "", scene, "objects", scene, "camera_list_index")
154 | col = row.column(align=True)
155 | col.operator("cam_manager.cycle_cameras_backward", text="", icon='TRIA_UP')
156 | col.operator("cam_manager.cycle_cameras_next", text="", icon='TRIA_DOWN')
157 |
158 | layout.separator()
159 | layout.label(text='All Cameras')
160 |
161 | row = layout.row(align=True)
162 | row.prop_search(scene.cam_collection, "collection", bpy.data, "collections", text='Camera Collection')
163 | row.operator("camera.create_collection", text='New Collection', icon='COLLECTION_NEW')
164 |
165 | row = layout.row()
166 | row.operator('cameras.all_to_collection')
167 |
168 |
169 | class CAM_MANAGER_PT_popup(bpy.types.Panel):
170 | bl_idname = "OBJECT_PT_camera_manager_popup"
171 | bl_label = "Simple Camera Manager Popup"
172 | bl_space_type = 'VIEW_3D'
173 | bl_region_type = 'WINDOW'
174 | bl_context = "empty"
175 | bl_ui_units_x = 45
176 |
177 | def draw(self, context):
178 | layout = self.layout
179 |
180 | row = layout.row()
181 | row.label(text="Simple Camera Manager")
182 |
183 | scene = context.scene
184 | split = layout.split(factor=0.333)
185 | col_01 = split.column()
186 | split = split.split(factor=0.5)
187 | col_02 = split.column()
188 | col_03 = split.column()
189 |
190 | # Collections
191 | row = col_01.row(align=True)
192 | row.prop_search(scene.cam_collection, "collection", bpy.data, "collections", text='')
193 | row.operator("camera.create_collection", text='', icon='COLLECTION_NEW')
194 | col_01.operator('cameras.all_to_collection')
195 |
196 | # Camera Settings
197 | col_03.operator("view3d.view_camera", text="Toggle Camera View", icon='VIEW_CAMERA')
198 |
199 | layout.separator()
200 | # template_list now takes two new args.
201 | # The first one is the identifier of the registered UIList to use (if you want only the default list,
202 | # with no custom draw code, use "UI_UL_list").
203 |
204 | layout.separator()
205 |
206 | split = layout.split(factor=0.6)
207 | split_left = split.column().split(factor=0.45)
208 |
209 | # Camera name
210 | col_01 = split_left.column()
211 | col_02 = split_left.column()
212 | split_right = split.column().split(factor=0.5)
213 | col_03 = split_right.column()
214 | split_right_02 = split_right.split(factor=0.5)
215 | col_04 = split_right_02.column()
216 | col_05 = split_right_02.column()
217 |
218 | row = col_01.row(align=True)
219 | row.label(text="Camera")
220 | row.label(text="Visibility")
221 | # col_02.label(text="Focal Length, Resolution, Clipping")
222 | row = col_02.row(align=True)
223 | row.label(text="Focal Length")
224 | row.label(text="Resolution")
225 | row.label(text="Clipping")
226 | # col_03.label(text="World & Exposure")
227 | row = col_03.row(align=True)
228 | row.label(text="World")
229 | row.label(text="Exposure")
230 | row = col_04.row(align=True)
231 | row.label(text="Collection")
232 | row = col_05.row(align=True)
233 | row.label(text="Render Slot")
234 |
235 | row = layout.row()
236 | row.template_list("CAMERA_UL_cameras_popup", "", scene, "objects", scene, "camera_list_index")
237 | col = row.column(align=True)
238 | col.operator("cam_manager.cycle_cameras_backward", text="", icon='TRIA_UP')
239 | col.operator("cam_manager.cycle_cameras_next", text="", icon='TRIA_DOWN')
240 |
241 | row = layout.row()
242 | row.prop(scene, 'output_render')
243 | row = layout.row()
244 | row.prop(scene, 'output_use_cam_name')
245 | row = layout.row()
246 | row.prop(context.scene.render, 'filepath')
247 | row.operator('cameras.open_in_explorer', text='Open Render Folder', icon='FILE_FOLDER')
248 | row = layout.row() # layout.label(text="Output path" + os.path.abspath(context.scene.render.filepath))
249 |
250 |
251 | class CAM_MANAGER_PT_camera_properties(bpy.types.Panel):
252 | bl_idname = "CAMERA_PT_manager_menu"
253 | bl_label = "Simple Camera Manager Menu"
254 | bl_space_type = 'PROPERTIES'
255 | bl_region_type = 'WINDOW'
256 | bl_context = "data"
257 | bl_options = {'HIDE_HEADER'}
258 | COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}
259 |
260 | @classmethod
261 | def poll(cls, context):
262 | engine = context.engine
263 | # Check if the properties data panel is for the camera or not
264 | return context.camera and (engine in cls.COMPAT_ENGINES)
265 |
266 | def draw(self, context):
267 | layout = self.layout
268 |
269 | cam = context.camera
270 |
271 | row = layout.row(align=True)
272 | row.prop(cam, "resolution", text="")
273 | op = row.operator("cam_manager.camera_resolutio_from_image", text="",
274 | icon='IMAGE_BACKGROUND').camera_name = cam.name
275 |
276 |
277 | class CameraCollectionProperty(bpy.types.PropertyGroup):
278 | collection: bpy.props.PointerProperty(name="Collection", type=bpy.types.Collection, )
279 |
280 |
281 | class CAMERA_OT_SelectAllCameras(bpy.types.Operator):
282 | bl_idname = "cam_manager.select_all_cameras"
283 | bl_label = "Select All Collections"
284 | bl_options = {'REGISTER', 'UNDO'}
285 |
286 | invert: bpy.props.BoolProperty()
287 |
288 | def execute(self, context):
289 | for cam in bpy.data.cameras:
290 | cam.render_selected = not self.invert
291 | return {'FINISHED'}
292 |
293 |
294 | # Define the custom menu
295 | class CameraOperatorDropdownMenu(bpy.types.Menu):
296 | bl_label = "Camera Operators"
297 | bl_idname = "OBJECT_MT_camera_dropdown_menu"
298 |
299 | def draw(self, context):
300 | layout = self.layout
301 | layout.operator("camera.create_collection", text='Camera Collection', icon='COLLECTION_NEW')
302 | layout.operator("cameras.all_to_collection", text='Move to Camera Collection', icon='OUTLINER_COLLECTION')
303 | layout.operator("camera.create_camera_from_view", text='Create Camera from View', icon='VIEW_CAMERA')
304 | layout.operator("view3d.view_camera", text="Toggle Camera View", icon='VIEW_CAMERA')
305 | layout.operator("cameras.open_in_explorer", text='Open Render Folder', icon='FILE_FOLDER')
306 |
307 |
308 | classes = (
309 | CameraCollectionProperty,
310 | CAMERA_OT_open_in_explorer,
311 | CAMERA_OT_SelectAllCameras,
312 | CAM_MANAGER_PT_scene_properties,
313 | CAM_MANAGER_PT_popup,
314 | CAM_MANAGER_PT_camera_properties,
315 | VIEW3D_PT_SimpleCameraManager,
316 | CameraOperatorDropdownMenu,
317 | )
318 |
319 |
320 | def register():
321 | from bpy.utils import register_class
322 |
323 | for cls in classes:
324 | register_class(cls)
325 |
326 | scene = bpy.types.Scene
327 |
328 | # The PointerProperty has to be after registering the classes to know about the custom property type
329 | scene.cam_collection = bpy.props.PointerProperty(name="Camera Collection",
330 | description='User collection dedicated for the cameras',
331 | type=CameraCollectionProperty)
332 |
333 | scene.output_render = bpy.props.BoolProperty(name="Save Render to Disk", description="Save renders to disk",
334 | default=True)
335 |
336 | scene.output_use_cam_name = bpy.props.BoolProperty(name="Use Camera Name as File Name",
337 | description="Use camera name as file name", default=True)
338 |
339 |
340 | def unregister():
341 | from bpy.utils import unregister_class
342 |
343 | for cls in reversed(classes):
344 | unregister_class(cls)
345 |
346 | scene = bpy.types.Scene
347 |
348 | del scene.my_operator
349 | del scene.output_use_cam_name
350 | del scene.output_render
351 | del scene.cam_collection
352 |
--------------------------------------------------------------------------------
/ui_helpers.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class EXPORTER_OT_open_preferences(bpy.types.Operator):
4 | """Tooltip"""
5 | bl_idname = "simple_camera.open_preferences"
6 | bl_label = "Open Addon preferences"
7 |
8 | addon_name: bpy.props.StringProperty()
9 | prefs_tabs: bpy.props.StringProperty()
10 |
11 | def execute(self, context):
12 |
13 | bpy.ops.screen.userpref_show()
14 |
15 | bpy.context.preferences.active_section = 'ADDONS'
16 | bpy.data.window_managers["WinMan"].addon_search = self.addon_name
17 |
18 | prefs = context.preferences.addons[__package__].preferences
19 | prefs.prefs_tabs = self.prefs_tabs
20 |
21 | import addon_utils
22 | mod = addon_utils.addons_fake_modules.get('simple_camera_manager')
23 |
24 | # mod is None the first time the operation is called :/
25 | if mod:
26 | mod.bl_info['show_expanded'] = True
27 |
28 | # Find User Preferences area and redraw it
29 | for window in bpy.context.window_manager.windows:
30 | for area in window.screen.areas:
31 | if area.type == 'USER_PREFERENCES':
32 | area.tag_redraw()
33 |
34 | bpy.ops.preferences.addon_expand(module=self.addon_name)
35 | return {'FINISHED'}
36 |
37 | classes = (
38 | EXPORTER_OT_open_preferences,
39 | )
40 |
41 |
42 | def register():
43 | from bpy.utils import register_class
44 | for cls in classes:
45 | register_class(cls)
46 |
47 |
48 | def unregister():
49 | from bpy.utils import unregister_class
50 | for cls in reversed(classes):
51 | unregister_class(cls)
52 |
--------------------------------------------------------------------------------
/uilist.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | def filter_list(self, context):
5 | """
6 | Filter cameras from all objects for the UI list and sort them
7 | :param self:
8 | :param context:
9 | :return: flt_flags is a bit-flag containing the filtering and flt
10 | flt_neworder defines the order of all cameras
11 | """
12 | helper_funcs = bpy.types.UI_UL_list
13 |
14 | # Default return values.
15 | flt_flags = []
16 | flt_neworder = []
17 |
18 | # Get all objects from scene.
19 | objects = context.scene.objects
20 |
21 | # Create bitmask for all objects
22 | flt_flags = [0] * len(objects)
23 |
24 | # Filter by object type and name.
25 | filter_name = self.filter_name.lower()
26 | invert_filter = self.use_filter_name_reverse
27 | filtered_cameras = []
28 |
29 | for idx, obj in enumerate(objects):
30 | if obj.type == "CAMERA":
31 | name_match = not filter_name or filter_name in obj.name.lower()
32 | if (name_match and not invert_filter) or (not name_match and invert_filter):
33 | flt_flags[idx] = self.bitflag_filter_item | self.CAMERA_FILTER
34 | filtered_cameras.append(idx)
35 | else:
36 | flt_flags[idx] = 0
37 | else:
38 | flt_flags[idx] = 0
39 |
40 | # Sort filtered cameras by name.
41 | if self.use_order_name:
42 | filtered_cameras.sort(key=lambda idx: objects[idx].name.lower())
43 | if self.use_filter_orderby_invert:
44 | filtered_cameras.reverse()
45 |
46 | # Create new order list.
47 | flt_neworder = filtered_cameras + [idx for idx in range(len(objects)) if idx not in filtered_cameras]
48 |
49 | return flt_flags, flt_neworder
50 |
51 |
52 | class CAMERA_UL_cameras_popup(bpy.types.UIList):
53 | """UI list showing all cameras with associated resolution. The resolution can be changed directly from this list"""
54 | # The draw_item function is called for each item of the collection that is visible in the list.
55 | # data is the RNA object containing the collection,
56 | # item is the current drawn item of the collection,
57 | # icon is the "computed" icon for the item (as an integer, because some objects like materials or textures
58 | # have custom icons ID, which are not available as enum items).
59 | # active_data is the RNA object containing the active property for the collection (i.e. integer pointing to the
60 | # active item of the collection).
61 | # active_propname is the name of the active property (use 'getattr(active_data, active_propname)').
62 | # index is index of the current item in the collection.
63 | # flt_flag is the result of the filtering process for this item.
64 | # Note: as index and flt_flag are optional arguments, you do not have to use/declare them here if you don't
65 | # need them.
66 |
67 | # Constants (flags)
68 | # Be careful not to shadow FILTER_ITEM!
69 | CAMERA_FILTER = 1 << 0
70 |
71 | use_filter_name_reverse: bpy.props.BoolProperty(
72 | name="Reverse Name",
73 | default=False,
74 | options=set(),
75 | description="Reverse name filtering",
76 | )
77 |
78 | # This allows us to have mutually exclusive options, which are also all disable-able!
79 | def _gen_order_update(name1, name2):
80 | def _u(self, ctxt):
81 | if (getattr(self, name1)):
82 | setattr(self, name2, False)
83 |
84 | return _u
85 |
86 | use_order_name: bpy.props.BoolProperty(
87 | name="Name", default=False, options=set(),
88 | description="Sort groups by their name (case-insensitive)",
89 | update=_gen_order_update("use_order_name", "use_order_importance"),
90 | )
91 | use_order_importance: bpy.props.BoolProperty(
92 | name="Importance",
93 | default=False,
94 | options=set(),
95 | description="Sort groups by their average weight in the mesh",
96 | update=_gen_order_update("use_order_importance", "use_order_name"),
97 | )
98 |
99 | def draw_filter(self, context, layout):
100 | # Nothing much to say here, it's usual UI code...
101 | row = layout.row()
102 |
103 | subrow = row.row(align=True)
104 | subrow.prop(self, "filter_name", text="")
105 | icon = 'ARROW_LEFTRIGHT'
106 | subrow.prop(self, "use_filter_name_reverse", text="", icon=icon)
107 |
108 | def filter_items(self, context, data, propname):
109 | flt_flags, flt_neworder = filter_list(self, context)
110 | return flt_flags, flt_neworder
111 |
112 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
113 |
114 | obj = item
115 | cam = item.data
116 |
117 | # draw_item must handle the three layout types. Usually 'DEFAULT' and 'COMPACT' can share the same code.
118 | if self.layout_type in {'DEFAULT', 'COMPACT'}:
119 | # You should always start your row layout by a label (icon + text), or a non-embossed text field,
120 | # this will also make the row easily selectable in the list! The latter also enables ctrl-click rename.
121 | # We use icon_value of label, as our given icon is an integer value, not an enum ID.
122 | # Note "data" names should never be translated!
123 | if obj.type == 'CAMERA':
124 |
125 | split = layout.split(factor=0.6)
126 | split_left = split.column().split(factor=0.45)
127 | # Camera name
128 | col_01 = split_left.column()
129 | col_02 = split_left.column()
130 | split_right = split.column().split(factor=0.5)
131 | col_03 = split_right.column()
132 | split_right_02 = split_right.split(factor=0.5)
133 | col_04 = split_right_02.column()
134 | col_05 = split_right_02.column()
135 |
136 | ###### Col01 #####
137 | # Camera name and visibility
138 |
139 | row = col_01.row(align=True)
140 | icon = 'VIEW_CAMERA' if obj == bpy.context.scene.camera else 'FORWARD'
141 | op = row.operator("cam_manager.change_scene_camera", text='', icon=icon)
142 | op.camera_name = obj.name
143 | op.switch_to_cam = False
144 | row.prop(obj, 'name', text='')
145 |
146 | icon = 'HIDE_OFF' if obj.visible_get() else 'HIDE_ON'
147 | op = row.operator("camera.hide_unhide", icon=icon, text='')
148 | op.camera_name = obj.name
149 | op.cam_hide = obj.visible_get()
150 | row.prop(obj, "hide_viewport", text='')
151 | row.prop(obj, "hide_select", text='')
152 |
153 | if obj.get('lock'):
154 | op = row.operator("cam_manager.lock_unlock_camera", icon='LOCKED', text='')
155 | op.camera_name = obj.name
156 | op.cam_lock = False
157 | else:
158 | op = row.operator("cam_manager.lock_unlock_camera", icon='UNLOCKED', text='')
159 | op.camera_name = obj.name
160 | op.cam_lock = True
161 |
162 | ###### Col02 #####
163 | row = col_02.row()
164 | c = row.column(align=True)
165 | c.prop(cam, 'lens', text='')
166 | c.prop(cam, 'angle', text='')
167 | c = row.column(align=True)
168 | c.prop(cam, "resolution", text="")
169 | op = row.operator("cam_manager.camera_resolutio_from_image", text="", icon='IMAGE_BACKGROUND')
170 | op.camera_name = cam.name
171 | c = row.column(align=True)
172 | c.prop(cam, "clip_start", text="")
173 | c.prop(cam, "clip_end", text="")
174 |
175 | ###### Col03 #####
176 | row = col_03.row(align=True)
177 | row.prop_search(cam, "world", bpy.data, "worlds", text='')
178 | row.prop(cam, 'exposure', text='EXP')
179 |
180 | ###### Col04 #####
181 | row = col_04.row(align=True)
182 | op = row.operator("cameras.add_collection", icon='OUTLINER_COLLECTION')
183 | op.object_name = obj.name
184 |
185 | ###### Col05 #####
186 | row = col_05.row(align=True)
187 | row.prop(cam, "slot")
188 | op = row.operator('cameras.custom_render', text='', icon='RENDER_STILL')
189 | op.camera_name = obj.name
190 |
191 |
192 | else:
193 | layout.label(text=obj.name)
194 |
195 | # 'GRID' layout type should be as compact as possible (typically a single icon!).
196 | elif self.layout_type in {'GRID'}:
197 | layout.alignment = 'CENTER'
198 | layout.label(text=obj.name)
199 |
200 |
201 | class CAMERA_UL_cameras_scene(bpy.types.UIList):
202 | """UI list showing all cameras with associated resolution. The resolution can be changed directly from this list"""
203 | CAMERA_FILTER = 1 << 0
204 |
205 | use_filter_name_reverse: bpy.props.BoolProperty(
206 | name="Reverse Name",
207 | default=False,
208 | options=set(),
209 | description="Reverse name filtering",
210 | )
211 |
212 | # This allows us to have mutually exclusive options, which are also all disable-able!
213 | def _gen_order_update(name1, name2):
214 | def _u(self, ctxt):
215 | if (getattr(self, name1)):
216 | setattr(self, name2, False)
217 |
218 | return _u
219 |
220 | use_order_name: bpy.props.BoolProperty(
221 | name="Name", default=False, options=set(),
222 | description="Sort groups by their name (case-insensitive)",
223 | update=_gen_order_update("use_order_name", "use_order_importance"),
224 | )
225 | use_order_importance: bpy.props.BoolProperty(
226 | name="Importance",
227 | default=False,
228 | options=set(),
229 | description="Sort groups by their average weight in the mesh",
230 | update=_gen_order_update("use_order_importance", "use_order_name"),
231 | )
232 |
233 | def draw_filter(self, context, layout):
234 | # Nothing much to say here, it's usual UI code...
235 | row = layout.row()
236 |
237 | subrow = row.row(align=True)
238 | subrow.prop(self, "filter_name", text="")
239 | icon = 'ARROW_LEFTRIGHT'
240 | subrow.prop(self, "use_filter_name_reverse", text="", icon=icon)
241 |
242 | def filter_items(self, context, data, propname):
243 | flt_flags, flt_neworder = filter_list(self, context)
244 | return flt_flags, flt_neworder
245 |
246 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
247 |
248 | obj = item
249 | cam = item.data
250 |
251 | # draw_item must handle the three layout types. Usually 'DEFAULT' and 'COMPACT' can share the same code.
252 | if self.layout_type in {'DEFAULT', 'COMPACT'}:
253 | # You should always start your row layout by a label (icon + text), or a non-embossed text field,
254 | # this will also make the row easily selectable in the list! The latter also enables ctrl-click rename.
255 | # We use icon_value of label, as our given icon is an integer value, not an enum ID.
256 | # Note "data" names should never be translated!
257 | if obj.type == 'CAMERA':
258 | c = layout.column()
259 | row = c.row()
260 |
261 | split = row.split(factor=0.6)
262 | col_01 = split.column()
263 | col_02 = split.column()
264 |
265 | # COLUMN 01
266 | row = col_01.row(align=True)
267 |
268 | # Checkbox for selecting the collection for export
269 | row.prop(cam, "render_selected", text="")
270 |
271 | # Change icon for already active cam
272 | icon = 'VIEW_CAMERA' if obj == bpy.context.scene.camera else 'FORWARD'
273 | op = row.operator("cam_manager.change_scene_camera", text='', icon=icon)
274 | op.camera_name = obj.name
275 | op.switch_to_cam = False
276 | row.prop(obj, 'name', text='')
277 |
278 | # COLUMN 02
279 | row = col_02.row(align=True)
280 |
281 | if obj.get('lock'):
282 | op = row.operator("cam_manager.lock_unlock_camera", icon='LOCKED', text='')
283 | op.camera_name = obj.name
284 | op.cam_lock = False
285 | else:
286 | op = row.operator("cam_manager.lock_unlock_camera", icon='UNLOCKED', text='')
287 | op.camera_name = obj.name
288 | op.cam_lock = True
289 |
290 | row = row.row(align=True)
291 |
292 | row.prop(cam, 'slot', text='')
293 | op = row.operator('cameras.custom_render', text='', icon='RENDER_STILL')
294 | op.camera_name = obj.name
295 |
296 |
297 | else:
298 | layout.label(text=obj.name)
299 |
300 | # 'GRID' layout type should be as compact as possible (typically a single icon!).
301 | elif self.layout_type in {'GRID'}:
302 | layout.alignment = 'CENTER'
303 | layout.label(text=obj.name)
304 |
305 |
306 | class UIListDropdownMenu(bpy.types.Menu):
307 | bl_label = "Camera List Operators"
308 | bl_idname = "OBJECT_MT_camera_list_dropdown_menu"
309 |
310 | def draw(self, context):
311 | layout = self.layout
312 | layout.operator("cam_manager.select_all_cameras", text='Select All', icon='CHECKBOX_HLT').invert = False
313 | layout.operator("cam_manager.select_all_cameras", text='Select None', icon='CHECKBOX_DEHLT').invert = True
314 |
315 |
316 | classes = (
317 | CAMERA_UL_cameras_popup,
318 | CAMERA_UL_cameras_scene,
319 | UIListDropdownMenu,
320 | )
321 |
322 |
323 | def register():
324 | from bpy.utils import register_class
325 |
326 | for cls in classes:
327 | register_class(cls)
328 |
329 |
330 | def unregister():
331 | from bpy.utils import unregister_class
332 |
333 | for cls in reversed(classes):
334 | unregister_class(cls)
335 |
--------------------------------------------------------------------------------