2025-12-01
This commit is contained in:
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,20 +1,33 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from .operators import register as operators_register, unregister as operators_unregister
|
||||
from .tools import register as tools_register, unregister as tools_unregister
|
||||
from . import (
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
)
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [operators,
|
||||
tools,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
print("Add-on Reloaded: Bool Tool")
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
operators,
|
||||
tools,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
operators,
|
||||
tools,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
@@ -26,15 +39,9 @@ def register():
|
||||
for module in modules:
|
||||
module.register()
|
||||
|
||||
operators_register()
|
||||
tools_register()
|
||||
|
||||
preferences.update_sidebar_category(bpy.context.preferences.addons[__package__].preferences, bpy.context)
|
||||
|
||||
|
||||
def unregister():
|
||||
for module in reversed(modules):
|
||||
module.unregister()
|
||||
|
||||
operators_unregister()
|
||||
tools_unregister()
|
||||
|
||||
@@ -2,15 +2,15 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "bool_tool"
|
||||
name = "Bool Tool"
|
||||
version = "1.1.3"
|
||||
tagline = "Quick boolean operations and tools for mesh modeling"
|
||||
version = "1.1.5"
|
||||
tagline = "Quick boolean operators and tools for hard surface modeling"
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "Nika Kutsniashvili <nickberckley@gmail.com>"
|
||||
website = "https://github.com/nickberckley/bool_tool"
|
||||
tags = ["Modeling", "Object"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
blender_version_min = "4.5.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import bpy, gpu, mathutils, math
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import bpy
|
||||
import gpu
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .math import (
|
||||
draw_circle,
|
||||
draw_polygon,
|
||||
draw_array,
|
||||
)
|
||||
|
||||
|
||||
magic_number = 1.41
|
||||
color = (0.48, 0.04, 0.04, 1.0)
|
||||
secondary_color = (0.28, 0.04, 0.04, 1.0)
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
@@ -48,172 +58,74 @@ def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def carver_overlay(self, context):
|
||||
"""Shape (rectangle, circle) overlay for carver tool"""
|
||||
def carver_shape_box(self, context, shape):
|
||||
"""Shape overlay for box carver tool"""
|
||||
|
||||
color = (0.48, 0.04, 0.04, 1.0)
|
||||
secondary_color = (0.28, 0.04, 0.04, 1.0)
|
||||
subdivision = self.subdivision if shape == 'CIRCLE' else 4
|
||||
rotation = 0 if shape == 'CIRCLE' else 45
|
||||
|
||||
if self.shape == 'CIRCLE':
|
||||
coords, indices, rows, columns = draw_circle(self, self.subdivision, 0)
|
||||
# coords = coords[1:] # remove_extra_vertex
|
||||
self.verts = coords
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
# Create Shape
|
||||
coords, indices, bounds = draw_circle(self, subdivision, rotation)
|
||||
self.verts = coords
|
||||
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if not self.rotate:
|
||||
bounds, __, __ = get_bounding_box_coords(self, coords)
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
# Draw Shaders
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if not self.rotate and not self.bevel:
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
|
||||
# Array
|
||||
if self.rows > 1 or self.columns > 1:
|
||||
carver_shape_array(self, coords, indices, 'SOLID')
|
||||
|
||||
|
||||
elif self.shape == 'BOX':
|
||||
coords, indices, rows, columns = draw_circle(self, 4, 45)
|
||||
self.verts = coords
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if (self.rotate == False) and (self.bevel == False):
|
||||
bounds, __, __ = get_bounding_box_coords(self, coords)
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
|
||||
|
||||
elif self.shape == 'POLYLINE':
|
||||
coords, indices, first_point, rows, columns = draw_polygon(self)
|
||||
self.verts = list(dict.fromkeys(self.mouse_path))
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
draw_shader(color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
|
||||
draw_shader(color, 1.0, 'POINTS', coords, size=5)
|
||||
if self.closed and len(self.mouse_path) > 2:
|
||||
# polygon_fill
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
|
||||
if (self.closed and len(coords) > 3) or (self.closed == False and len(coords) > 4):
|
||||
# circle_around_first_point
|
||||
draw_shader(color, 0.8, 'OUTLINE', first_point, size=3)
|
||||
|
||||
|
||||
# Snapping Grid
|
||||
if self.snap and self.move == False:
|
||||
if self.snap:
|
||||
mini_grid(self, context)
|
||||
|
||||
# ARRAY
|
||||
array_shader = 'LINE_LOOP' if self.shape == 'POLYLINE' and self.closed == False else 'SOLID'
|
||||
if self.rows > 1:
|
||||
for i, duplicate in rows.items():
|
||||
draw_shader(secondary_color, 0.4, array_shader, duplicate, size=2, indices=indices[:-2])
|
||||
if self.columns > 1:
|
||||
for i, duplicate in columns.items():
|
||||
draw_shader(secondary_color, 0.4, array_shader, duplicate, size=2, indices=indices[:-2])
|
||||
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def draw_polygon(self):
|
||||
"""Returns polygonal 2d shape in which each cursor click is taken as a new vertice"""
|
||||
def carver_shape_polyline(self, context):
|
||||
"""Shape overlay for polyline carver tool"""
|
||||
|
||||
indices = []
|
||||
coords = []
|
||||
for idx, vals in enumerate(self.mouse_path):
|
||||
vert = mathutils.Vector([vals[0], vals[1], 0.0])
|
||||
vert += mathutils.Vector([self.position_x, self.position_y, 0.0])
|
||||
coords.append(vert)
|
||||
# Create Shape
|
||||
coords, indices, first_point, array_coords = draw_polygon(self)
|
||||
self.verts = list(dict.fromkeys(self.mouse_path))
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx <= len(self.mouse_path) else 1
|
||||
indices.append((0, i1, i2))
|
||||
# Draw Shaders
|
||||
draw_shader(color, 1.0, 'POINTS', coords, size=5)
|
||||
draw_shader(color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
|
||||
|
||||
# circle_around_first_point
|
||||
radius = self.distance_from_first
|
||||
segments = 4
|
||||
if self.closed and len(self.mouse_path) > 2:
|
||||
# polygon_fill
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
|
||||
click_point = [coords[0]]
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
x = coords[0][0] + radius * math.cos(angle)
|
||||
y = coords[0][1] + radius * math.sin(angle)
|
||||
z = coords[0][2]
|
||||
vector = mathutils.Vector((x, y, z))
|
||||
click_point.append(vector)
|
||||
if (self.closed and len(coords) > 3) or (self.closed == False and len(coords) > 4):
|
||||
# circle_around_first_point
|
||||
draw_shader(color, 0.8, 'OUTLINE', first_point, size=3)
|
||||
|
||||
# remove_duplicate_verts
|
||||
# NOTE: This is needed to remove extra vertices for duplicates which are not removed because `dict.fromkeys()`...
|
||||
# NOTE: can't be called on `coords` list, because it contains unfrozen Vectors.
|
||||
unique_verts = []
|
||||
for vert in coords:
|
||||
if vert not in unique_verts:
|
||||
unique_verts.append(vert)
|
||||
# Array
|
||||
if len(self.mouse_path) > 2 and (self.rows > 1 or self.columns > 1):
|
||||
carver_shape_array(self, array_coords, indices, 'LINE_LOOP' if self.closed == False else 'SOLID')
|
||||
|
||||
|
||||
# ARRAY
|
||||
rows = columns = {}
|
||||
if len(self.mouse_path) > 2:
|
||||
array_coords = unique_verts if self.closed else unique_verts[:-1]
|
||||
get_bounding_box_coords(self, array_coords)
|
||||
rows, columns = array(self, array_coords)
|
||||
if self.snap:
|
||||
mini_grid(self, context)
|
||||
|
||||
return coords, indices, click_point, rows, columns
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def draw_circle(self, subdivision, rotation):
|
||||
"""Returns the coordinates & indices of a circle using a triangle fan"""
|
||||
"""NOTE: Origin point code is duplicated on purpose (to experiment with different math easily)"""
|
||||
def carver_shape_array(self, verts, indices, shader):
|
||||
"""Draws given shape for each row and column of the array"""
|
||||
|
||||
def create_2d_circle(self, step, rotation):
|
||||
"""Create the vertices of a 2d circle at (0, 0)"""
|
||||
rows, columns = draw_array(self, verts)
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
modifier = 2 if self.shape == 'CIRCLE' else magic_number
|
||||
if self.origin == 'CENTER':
|
||||
modifier /= 2
|
||||
|
||||
verts = []
|
||||
for i in range(step):
|
||||
angle = (360 / step) * i + rotation
|
||||
verts.append(math.cos(math.radians(angle)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(angle)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
verts.append(math.cos(math.radians(0.0 + rotation)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(0.0 + rotation)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
return verts
|
||||
|
||||
tris_verts = []
|
||||
indices = []
|
||||
verts = create_2d_circle(self, int(subdivision), rotation)
|
||||
|
||||
rotation_matrix = mathutils.Matrix.Rotation(self.rotation, 4, 'Z')
|
||||
fixed_point = mathutils.Vector((self.mouse_path[0][0], self.mouse_path[0][1], 0.0))
|
||||
current_mouse_position = mathutils.Vector((self.mouse_path[1][0], self.mouse_path[1][1], 0.0))
|
||||
shape_center = fixed_point + (current_mouse_position - fixed_point) / 2
|
||||
|
||||
min_x = min(verts[0::3]) if self.mouse_path[1][0] > self.mouse_path[0][0] else -min(verts[0::3])
|
||||
min_y = min(verts[1::3]) if self.mouse_path[1][1] > self.mouse_path[0][1] else -min(verts[1::3])
|
||||
|
||||
for idx in range((len(verts) // 3) - 1):
|
||||
x = verts[idx * 3]
|
||||
y = verts[idx * 3 + 1]
|
||||
z = verts[idx * 3 + 2]
|
||||
vert = mathutils.Vector((x, y, z))
|
||||
vert = rotation_matrix @ vert
|
||||
vert = vert + fixed_point if self.origin == 'CENTER' else shape_center - vert
|
||||
vert += mathutils.Vector((self.position_x, self.position_y, 0.0))
|
||||
tris_verts.append(vert)
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= ((360 / int(subdivision)) * (idx + 1) + rotation) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
|
||||
# BEVEL
|
||||
if self.use_bevel and self.bevel_radius > 0.01:
|
||||
tris_verts, indices = bevel_verts(self, tris_verts, (self.bevel_radius * 50), self.bevel_segments)
|
||||
|
||||
# ARRAY
|
||||
rows, columns = array(self, tris_verts)
|
||||
|
||||
return tris_verts, indices, rows, columns
|
||||
if self.rows > 1:
|
||||
for i, duplicate in rows.items():
|
||||
draw_shader(secondary_color, 0.4, shader, duplicate, size=2, indices=indices[:-2])
|
||||
if self.columns > 1:
|
||||
for i, duplicate in columns.items():
|
||||
draw_shader(secondary_color, 0.4, shader, duplicate, size=2, indices=indices[:-2])
|
||||
|
||||
|
||||
def mini_grid(self, context):
|
||||
@@ -222,8 +134,8 @@ def mini_grid(self, context):
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
for i, a in enumerate(context.screen.areas):
|
||||
if a.type == 'VIEW_3D':
|
||||
for i, area in enumerate(context.screen.areas):
|
||||
if area.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
screen_height = context.screen.areas[i].height
|
||||
screen_width = context.screen.areas[i].width
|
||||
@@ -262,139 +174,3 @@ def mini_grid(self, context):
|
||||
(mouse_coord[0] - 25 - snap_value, mouse_coord[1] - snap_value),]
|
||||
|
||||
draw_shader((1.0, 1.0, 1.0), 0.66, 'LINES', grid_coords, size=1.5)
|
||||
|
||||
|
||||
def get_bounding_box_coords(self, verts):
|
||||
"""Calculates the bounding box coordinates from a list of vertices in a counter-clockwise order"""
|
||||
|
||||
if verts:
|
||||
min_x = min(v[0] for v in verts)
|
||||
max_x = max(v[0] for v in verts)
|
||||
min_y = min(v[1] for v in verts)
|
||||
max_y = max(v[1] for v in verts)
|
||||
self.center_origin = [(min_x, min_y), (max_x, max_y)]
|
||||
|
||||
bounding_box_coords = [
|
||||
mathutils.Vector((min_x, min_y, 0)), # bottom-left
|
||||
mathutils.Vector((max_x, min_y, 0)), # bottom-right
|
||||
mathutils.Vector((max_x, max_y, 0)), # top-right
|
||||
mathutils.Vector((min_x, max_y, 0)), # top-left
|
||||
mathutils.Vector((min_x, min_y, 0)) # closing_the_loop_manually
|
||||
]
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
return bounding_box_coords, width, height
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
def array(self, verts):
|
||||
"""Duplicates given list of vertices in rows and columns (on x and y axis)"""
|
||||
"""Returns two dicts of lists of vertices for rows and columns separately"""
|
||||
|
||||
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
|
||||
if len(self.center_origin) == 0:
|
||||
get_bounding_box_coords(self, verts)
|
||||
|
||||
rows = {}
|
||||
if self.rows > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((((self.center_origin[1][0] - self.center_origin[0][0]) + (self.rows_gap)), 0.0, 0.0))
|
||||
if self.rows_direction == 'LEFT':
|
||||
offset.x = -offset.x
|
||||
|
||||
for i in range(self.rows - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
rows[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
|
||||
columns = {}
|
||||
if self.columns > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((0.0, -((self.center_origin[1][1] - self.center_origin[0][1]) + (self.columns_gap)), 0.0))
|
||||
if self.columns_direction == 'UP':
|
||||
offset.y = -offset.y
|
||||
|
||||
for i in range(self.columns - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
columns[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
for row_idx, row in rows.items():
|
||||
columns[(i, row_idx)] = [vert.copy() + accumulated_offset for vert in row]
|
||||
|
||||
return rows, columns
|
||||
|
||||
|
||||
def bevel_verts(self, verts, radius, segments):
|
||||
"""Takes in list of verts(Vectors) and bevels them, Returns a new list with new vertices"""
|
||||
|
||||
def get_rounded_corner(self, angular_point, p1, p2, radius, segments):
|
||||
# clamp_radius_to_reduce_clipping
|
||||
__, width, height = get_bounding_box_coords(self, verts)
|
||||
max_radius = min(width / 2.5, height / 2.5)
|
||||
clamped_radius = min(radius, max_radius)
|
||||
|
||||
if radius > clamped_radius:
|
||||
radius = clamped_radius
|
||||
|
||||
|
||||
# calculate_vectors (NOTE: Why it only works when reversed like this is unknown to me)
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
vector1 = -(p1 - angular_point)
|
||||
vector2 = -(p2 - angular_point)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
vector1 = p2 - angular_point
|
||||
vector2 = p1 - angular_point
|
||||
|
||||
# compute_lengths_of_vectors
|
||||
length1 = vector1.length
|
||||
length2 = vector2.length
|
||||
if length1 == 0 or length2 == 0:
|
||||
return [angular_point] * segments
|
||||
|
||||
vector1.normalize()
|
||||
vector2.normalize()
|
||||
|
||||
# calculate_the_angle_between_the_vectors
|
||||
dot_product = vector1.dot(vector2)
|
||||
angle = math.acos(max(-1.0, min(1.0, dot_product)))
|
||||
|
||||
arc_length = radius * angle
|
||||
segment_length = arc_length / (segments - 1)
|
||||
bisector = (vector1 + vector2).normalized()
|
||||
|
||||
# generate_points_along_the_arc
|
||||
rounded_corners = []
|
||||
for i in range(segments):
|
||||
fraction = i / (segments - 1)
|
||||
theta = angle * fraction
|
||||
interpolated_vector = (vector1 * math.sin(theta) + vector2 * math.cos(theta)).normalized() * radius
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector * (clamped_radius * magic_number)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector / (clamped_radius)
|
||||
rounded_corners.append(point_on_arc)
|
||||
|
||||
return rounded_corners
|
||||
|
||||
rounded_verts = []
|
||||
indices = []
|
||||
num_verts = len(verts)
|
||||
|
||||
for idx in range(num_verts):
|
||||
angular_point = verts[idx]
|
||||
prev_idx = (idx - 1) % num_verts
|
||||
next_idx = (idx + 1) % num_verts
|
||||
|
||||
p1 = verts[prev_idx]
|
||||
p2 = verts[next_idx]
|
||||
|
||||
corner_points = get_rounded_corner(self, angular_point, p1, p2, radius, segments)
|
||||
rounded_verts.extend(corner_points)
|
||||
|
||||
for idx, vert in enumerate(reversed(rounded_verts)):
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= len(rounded_verts) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
return rounded_verts, indices
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
from .object import convert_to_mesh
|
||||
|
||||
|
||||
#### ------------------------------ /all/ ------------------------------ ####
|
||||
@@ -18,35 +17,6 @@ def list_canvases():
|
||||
|
||||
#### ------------------------------ /selected/ ------------------------------ ####
|
||||
|
||||
def list_candidate_objects(self, context, canvas):
|
||||
"""Filter out objects from selected ones that can't be used as a cutter"""
|
||||
|
||||
cutters = []
|
||||
for obj in context.selected_objects:
|
||||
if obj != context.active_object and obj.type in ('MESH', 'CURVE', 'FONT'):
|
||||
if obj.library or obj.override_library:
|
||||
self.report({'ERROR'}, f"{obj.name} is linked and can not be used as a cutter")
|
||||
|
||||
else:
|
||||
if obj.type in ('CURVE', 'FONT'):
|
||||
if obj.data.bevel_depth != 0 or obj.data.extrude != 0:
|
||||
convert_to_mesh(context, obj)
|
||||
cutters.append(obj)
|
||||
|
||||
else:
|
||||
# exclude_if_object_is_already_a_cutter_for_canvas
|
||||
if canvas in list_cutter_users([obj]):
|
||||
continue
|
||||
# exclude_if_canvas_is_cutting_the_object_(avoid_dependancy_loop)
|
||||
if obj in list_cutter_users([canvas]):
|
||||
self.report({'WARNING'}, f"{obj.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
cutters.append(obj)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def list_selected_cutters(context):
|
||||
"""List selected cutters"""
|
||||
|
||||
@@ -175,17 +145,17 @@ def list_unused_cutters(cutters, *canvases, do_leftovers=False):
|
||||
return cutters, leftovers
|
||||
|
||||
|
||||
def list_pre_boolean_modifiers(obj):
|
||||
"""Returns list of boolean modifiers + all modifiers that come before last boolean modifier"""
|
||||
def list_pre_boolean_modifiers(obj) -> list:
|
||||
"""Returns a list of boolean modifiers & modifiers that come before last boolean modifier"""
|
||||
|
||||
# find_the_index_of_last_boolean_modifier
|
||||
# Find the index of a last boolean modifier
|
||||
last_boolean_index = -1
|
||||
for i in reversed(range(len(obj.modifiers))):
|
||||
if obj.modifiers[i].type == 'BOOLEAN':
|
||||
last_boolean_index = i
|
||||
break
|
||||
|
||||
# if_boolean_modifier_found_list_all_modifiers_before
|
||||
# If boolean modifier is found, list all modifiers that come before it.
|
||||
if last_boolean_index != -1:
|
||||
return [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import bpy
|
||||
import math
|
||||
import mathutils
|
||||
|
||||
|
||||
magic_number = 1.41
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_circle(self, subdivision, rotation):
|
||||
"""Returns the coordinates & indices of a 2d circle in screen-space"""
|
||||
|
||||
def create_2d_circle(self, step, rotation):
|
||||
"""Create the vertices of a 2d circle at (0, 0)"""
|
||||
|
||||
modifier = 2 if self.shape == 'CIRCLE' else magic_number
|
||||
if self.origin == 'CENTER':
|
||||
modifier /= 2
|
||||
|
||||
verts = []
|
||||
for i in range(step):
|
||||
angle = (360 / step) * i + rotation
|
||||
verts.append(math.cos(math.radians(angle)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(angle)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
verts.append(math.cos(math.radians(0.0 + rotation)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(0.0 + rotation)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
return verts
|
||||
|
||||
tris_verts = []
|
||||
indices = []
|
||||
verts = create_2d_circle(self, int(subdivision), rotation)
|
||||
|
||||
rotation_matrix = mathutils.Matrix.Rotation(self.rotation, 4, 'Z')
|
||||
fixed_point = mathutils.Vector((self.mouse_path[0][0], self.mouse_path[0][1], 0.0))
|
||||
current_mouse_position = mathutils.Vector((self.mouse_path[1][0], self.mouse_path[1][1], 0.0))
|
||||
shape_center = fixed_point + (current_mouse_position - fixed_point) / 2
|
||||
|
||||
for idx in range((len(verts) // 3) - 1):
|
||||
x = verts[idx * 3]
|
||||
y = verts[idx * 3 + 1]
|
||||
z = verts[idx * 3 + 2]
|
||||
vert = mathutils.Vector((x, y, z))
|
||||
vert = rotation_matrix @ vert
|
||||
vert = vert + fixed_point if self.origin == 'CENTER' else shape_center - vert
|
||||
vert += mathutils.Vector((self.position_offset_x, self.position_offset_y, 0.0))
|
||||
tris_verts.append(vert)
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= ((360 / int(subdivision)) * (idx + 1) + rotation) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
# BEVEL
|
||||
if self.use_bevel and self.bevel_radius > 0.01:
|
||||
tris_verts, indices = bevel_verts(self, tris_verts, (self.bevel_radius * 50), self.bevel_segments)
|
||||
|
||||
|
||||
# BOUNDING_BOX
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(tris_verts)
|
||||
bounds = [
|
||||
mathutils.Vector((min_x, min_y, 0)), # bottom-left
|
||||
mathutils.Vector((max_x, min_y, 0)), # bottom-right
|
||||
mathutils.Vector((max_x, max_y, 0)), # top-right
|
||||
mathutils.Vector((min_x, max_y, 0)), # top-left
|
||||
mathutils.Vector((min_x, min_y, 0)) # closing_the_loop_manually
|
||||
]
|
||||
|
||||
return tris_verts, indices, bounds
|
||||
|
||||
|
||||
def draw_polygon(self):
|
||||
"""Returns polygonal 2d shape in screen-space where each cursor click is taken as a new vertice"""
|
||||
|
||||
indices = []
|
||||
coords = []
|
||||
for idx, vals in enumerate(self.mouse_path):
|
||||
vert = mathutils.Vector([vals[0], vals[1], 0.0])
|
||||
vert += mathutils.Vector([self.position_offset_x, self.position_offset_y, 0.0])
|
||||
coords.append(vert)
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx <= len(self.mouse_path) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
# circle_around_first_point
|
||||
radius = self.distance_from_first
|
||||
segments = 4
|
||||
|
||||
click_point = [coords[0]]
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
x = coords[0][0] + radius * math.cos(angle)
|
||||
y = coords[0][1] + radius * math.sin(angle)
|
||||
z = coords[0][2]
|
||||
vector = mathutils.Vector((x, y, z))
|
||||
click_point.append(vector)
|
||||
|
||||
|
||||
# ARRAY (remove_duplicate_verts)
|
||||
"""NOTE: This is needed to remove extra vertices for duplicates which are not removed because `dict.fromkeys()`..."""
|
||||
"""NOTE: can't be called on `coords` list, because it contains unfrozen Vectors."""
|
||||
unique_verts = []
|
||||
for vert in coords:
|
||||
if vert not in unique_verts:
|
||||
unique_verts.append(vert)
|
||||
|
||||
array_coords = unique_verts if self.closed else unique_verts[:-1]
|
||||
|
||||
return coords, indices, click_point, array_coords
|
||||
|
||||
|
||||
def draw_array(self, verts):
|
||||
"""Duplicates given list of vertices in rows and columns (on screen-space x and y axis)"""
|
||||
"""Returns two dicts of lists of vertices for rows and columns separately"""
|
||||
|
||||
# get_bounding_box_of_the_shape
|
||||
"""NOTE: Calculated separately because verts needed for array differs from verts needed for shape for polyline"""
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(verts)
|
||||
|
||||
rows = {}
|
||||
if self.rows > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((((max_x - min_x) + (self.rows_gap)), 0.0, 0.0))
|
||||
if self.rows_direction == 'LEFT':
|
||||
offset.x = -offset.x
|
||||
|
||||
for i in range(self.rows - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
rows[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
|
||||
columns = {}
|
||||
if self.columns > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((0.0, -((max_y - min_y) + (self.columns_gap)), 0.0))
|
||||
if self.columns_direction == 'UP':
|
||||
offset.y = -offset.y
|
||||
|
||||
for i in range(self.columns - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
columns[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
for row_idx, row in rows.items():
|
||||
columns[(i, row_idx)] = [vert.copy() + accumulated_offset for vert in row]
|
||||
|
||||
return rows, columns
|
||||
|
||||
|
||||
def bevel_verts(self, verts, radius, segments):
|
||||
"""Takes in list of verts(Vectors) and bevels them, Returns a new list with new vertices"""
|
||||
|
||||
def get_rounded_corner(self, angular_point, p1, p2, radius, segments):
|
||||
# get_bounding_box_of_the_shape
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(verts)
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
# clamp_radius_to_reduce_clipping
|
||||
max_radius = min(width / 2.5, height / 2.5)
|
||||
clamped_radius = min(radius, max_radius)
|
||||
|
||||
if radius > clamped_radius:
|
||||
radius = clamped_radius
|
||||
|
||||
|
||||
# calculate_vectors (NOTE: Why it only works when reversed like this is unknown to me)
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
vector1 = -(p1 - angular_point)
|
||||
vector2 = -(p2 - angular_point)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
vector1 = p2 - angular_point
|
||||
vector2 = p1 - angular_point
|
||||
|
||||
# compute_lengths_of_vectors
|
||||
length1 = vector1.length
|
||||
length2 = vector2.length
|
||||
if length1 == 0 or length2 == 0:
|
||||
return [angular_point] * segments
|
||||
|
||||
vector1.normalize()
|
||||
vector2.normalize()
|
||||
|
||||
# calculate_the_angle_between_the_vectors
|
||||
dot_product = vector1.dot(vector2)
|
||||
angle = math.acos(max(-1.0, min(1.0, dot_product)))
|
||||
|
||||
arc_length = radius * angle
|
||||
segment_length = arc_length / (segments - 1)
|
||||
bisector = (vector1 + vector2).normalized()
|
||||
|
||||
# generate_points_along_the_arc
|
||||
rounded_corners = []
|
||||
for i in range(segments):
|
||||
fraction = i / (segments - 1)
|
||||
theta = angle * fraction
|
||||
interpolated_vector = (vector1 * math.sin(theta) + vector2 * math.cos(theta)).normalized() * radius
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector * (clamped_radius * magic_number)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector / (clamped_radius)
|
||||
rounded_corners.append(point_on_arc)
|
||||
|
||||
return rounded_corners
|
||||
|
||||
rounded_verts = []
|
||||
indices = []
|
||||
num_verts = len(verts)
|
||||
|
||||
for idx in range(num_verts):
|
||||
angular_point = verts[idx]
|
||||
prev_idx = (idx - 1) % num_verts
|
||||
next_idx = (idx + 1) % num_verts
|
||||
|
||||
p1 = verts[prev_idx]
|
||||
p2 = verts[next_idx]
|
||||
|
||||
corner_points = get_rounded_corner(self, angular_point, p1, p2, radius, segments)
|
||||
rounded_verts.extend(corner_points)
|
||||
|
||||
for idx, vert in enumerate(reversed(rounded_verts)):
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= len(rounded_verts) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
return rounded_verts, indices
|
||||
|
||||
|
||||
def get_bounding_box(verts):
|
||||
"""Calculates the bounding box coordinates from a list of vertices"""
|
||||
|
||||
min_x = min(v[0] for v in verts)
|
||||
max_x = max(v[0] for v in verts)
|
||||
min_y = min(v[1] for v in verts)
|
||||
max_y = max(v[1] for v in verts)
|
||||
|
||||
return min_x, min_y, max_x, max_y
|
||||
@@ -1,4 +1,7 @@
|
||||
import bpy, bmesh, mathutils, math
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
@@ -19,8 +22,8 @@ def create_cutter_shape(self, context):
|
||||
if self.depth == 'CURSOR':
|
||||
plane_point = context.scene.cursor.location
|
||||
elif self.depth == 'VIEW':
|
||||
plane_point = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
|
||||
__, plane_point = combined_bounding_box(self.selected_objects)
|
||||
plane_point = mathutils.Vector(plane_point)
|
||||
|
||||
# Create Mesh & Object
|
||||
faces = {}
|
||||
@@ -61,7 +64,7 @@ def extrude(self, mesh):
|
||||
faces = [f for f in bm.faces]
|
||||
|
||||
# move_the_mesh_towards_view
|
||||
box_bounding = combined_bounding_box(self.selected_objects)
|
||||
box_bounding, __ = combined_bounding_box(self.selected_objects)
|
||||
for face in faces:
|
||||
for vert in face.verts:
|
||||
vert.co += -self.view_depth * box_bounding
|
||||
@@ -85,7 +88,7 @@ def extrude(self, mesh):
|
||||
|
||||
def combined_bounding_box(objects):
|
||||
"""Calculate the combined bounding box of multiple objects."""
|
||||
|
||||
|
||||
min_corner = mathutils.Vector((float('inf'), float('inf'), float('inf')))
|
||||
max_corner = mathutils.Vector((-float('inf'), -float('inf'), -float('inf')))
|
||||
|
||||
@@ -103,7 +106,10 @@ def combined_bounding_box(objects):
|
||||
|
||||
# Calculate the diagonal of the combined bounding box
|
||||
bounding_box_diag = (max_corner - min_corner).length
|
||||
return bounding_box_diag
|
||||
# Calculate the center of bounding box
|
||||
bounding_box_center = (max_corner + min_corner) * 0.5
|
||||
|
||||
return bounding_box_diag, bounding_box_center
|
||||
|
||||
|
||||
def create_face(context, direction, depth, bm, name, faces, verts, polyline=False):
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
from .poll import (
|
||||
is_instanced_data,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, redo=True):
|
||||
"Adds boolean modifier with specified cutter and properties to a single object"
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and solver == 'FLOAT':
|
||||
solver = 'FAST'
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
|
||||
# Set solver options (inherited from operator properties).
|
||||
if redo:
|
||||
modifier.material_mode = self.material_mode
|
||||
modifier.use_self = self.use_self
|
||||
modifier.use_hole_tolerant = self.use_hole_tolerant
|
||||
modifier.double_threshold = self.double_threshold
|
||||
|
||||
if prefs.show_in_editmode:
|
||||
modifier.show_in_editmode = True
|
||||
|
||||
# Move modifier to the index 0 (make it first in the stack).
|
||||
if pin:
|
||||
index = obj.modifiers.find(modifier.name)
|
||||
obj.modifiers.move(index, 0)
|
||||
|
||||
return modifier
|
||||
|
||||
|
||||
def apply_modifiers(context, obj, modifiers: list):
|
||||
"""
|
||||
Apply modifiers on object.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, this function uses
|
||||
`bpy.data.meshes.new_from_object` built-in function to create a temporary
|
||||
mesh from the evaluated object (basically with visible modifiers applied).
|
||||
Temporary mesh is then transferred to objects mesh with `bmesh`.
|
||||
|
||||
This method is up to 2x faster, although it's considered experimental
|
||||
and may fail in some cases, so a fallback to `bpy.ops.object.modifier_apply` is kept.
|
||||
"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Make object data unique if it's instanced.
|
||||
if is_instanced_data(obj):
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
|
||||
try:
|
||||
# Don't use this method if it's not enabled by user in add-on preferences.
|
||||
if not prefs.fast_modifier_apply:
|
||||
raise Exception("")
|
||||
|
||||
with hide_modifiers(obj, excluding=modifiers):
|
||||
# Create a temporary mesh from evaluated object.
|
||||
evaluated_obj = obj.evaluated_get(context.evaluated_depsgraph_get())
|
||||
temp_data = bpy.data.meshes.new_from_object(evaluated_obj)
|
||||
|
||||
# Create `bmesh` from temporary mesh and update edit mesh.
|
||||
if context.mode == 'EDIT_MESH':
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm.clear()
|
||||
bm.from_mesh(temp_data)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(temp_data)
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
evaluated_obj.to_mesh_clear()
|
||||
|
||||
# Remove modifiers and purge temporary mesh.
|
||||
bpy.data.meshes.remove(temp_data)
|
||||
for mod in modifiers:
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
# Remove shape keys if there are any.
|
||||
# (after above operations none of the shape keys have any effect).
|
||||
if obj.data.shape_keys:
|
||||
obj.shape_key_clear()
|
||||
|
||||
# Use `bpy.ops` operator to apply modifiers if above fails.
|
||||
except Exception as e:
|
||||
# print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method")
|
||||
|
||||
context_override = {"object": obj, "mode": 'OBJECT'}
|
||||
with context.temp_override(**context_override):
|
||||
# Apply shape keys if there are any.
|
||||
if obj.data.shape_keys:
|
||||
bpy.ops.object.shape_key_remove(all=True, apply_mix=True)
|
||||
|
||||
# If all modifiers need to be applied convert to Mesh.
|
||||
if modifiers == obj.modifiers.values():
|
||||
print("Applying all modifiers by converting to Mesh")
|
||||
convert_to_mesh(context, obj)
|
||||
return
|
||||
|
||||
for mod in modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def hide_modifiers(obj, excluding: list):
|
||||
"""Hides all modifiers of a given object in viewport except those in excluding list"""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
if mod in excluding:
|
||||
continue
|
||||
if mod.show_viewport == True:
|
||||
visible_modifiers.append(mod)
|
||||
mod.show_viewport = False
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for mod in visible_modifiers:
|
||||
mod.show_viewport = True
|
||||
@@ -1,86 +1,10 @@
|
||||
import bpy, bmesh, mathutils
|
||||
import bpy
|
||||
import mathutils
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def add_boolean_modifier(self, context, canvas, cutter, mode, solver, apply=False, pin=False, redo=True, single_user=False):
|
||||
"Adds boolean modifier with specified cutter and properties to a single object"
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
modifier = canvas.modifiers.new("boolean_" + cutter.name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
|
||||
if redo:
|
||||
modifier.material_mode = self.material_mode
|
||||
modifier.use_self = self.use_self
|
||||
modifier.use_hole_tolerant = self.use_hole_tolerant
|
||||
modifier.double_threshold = self.double_threshold
|
||||
|
||||
if prefs.show_in_editmode:
|
||||
modifier.show_in_editmode = True
|
||||
|
||||
if pin:
|
||||
index = canvas.modifiers.find(modifier.name)
|
||||
canvas.modifiers.move(index, 0)
|
||||
|
||||
if apply:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
if context.mode == 'EDIT_MESH':
|
||||
"""Applying boolean modifier in mesh edit mode:"""
|
||||
"""1. Hiding other visible modifiers and creating new (temporary) mesh from evaluated object"""
|
||||
"""2. Transfering temporary mesh to `bmesh` to update active mesh in edit mode"""
|
||||
"""3. Removing boolean modifier and purging temporary mesh"""
|
||||
"""4. Restoring visibility of other modifiers from (1)"""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in canvas.modifiers:
|
||||
if mod == modifier:
|
||||
continue
|
||||
if mod.show_viewport == True:
|
||||
visible_modifiers.append(mod)
|
||||
mod.show_viewport = False
|
||||
|
||||
evaluated_obj = canvas.evaluated_get(context.evaluated_depsgraph_get())
|
||||
temp_data = bpy.data.meshes.new_from_object(evaluated_obj)
|
||||
|
||||
bm = bmesh.from_edit_mesh(canvas.data)
|
||||
bm.clear()
|
||||
bm.from_mesh(temp_data)
|
||||
bmesh.update_edit_mesh(canvas.data)
|
||||
evaluated_obj.to_mesh_clear()
|
||||
|
||||
canvas.modifiers.remove(modifier)
|
||||
bpy.data.meshes.remove(temp_data)
|
||||
|
||||
for mod in visible_modifiers:
|
||||
mod.show_viewport = True
|
||||
|
||||
else:
|
||||
context_override = {'object': canvas, 'mode': 'OBJECT'}
|
||||
with context.temp_override(**context_override):
|
||||
apply_modifier(context, canvas, modifier, single_user=single_user)
|
||||
|
||||
|
||||
def apply_modifier(context, obj, modifier, single_user=False):
|
||||
"""Applies given modifier to object."""
|
||||
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
try:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
except:
|
||||
if single_user:
|
||||
# Make Single User
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
|
||||
|
||||
def set_cutter_properties(context, canvas, cutter, mode, parent=True, hide=False, collection=True):
|
||||
"""Ensures cutter is properly set: has right properties, is hidden, in a collection & parented"""
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import bpy
|
||||
from .list import list_canvas_cutters
|
||||
|
||||
from .list import (
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def basic_poll(context, check_linked=False):
|
||||
if context.mode == 'OBJECT':
|
||||
if context.active_object is not None:
|
||||
if context.active_object.type == 'MESH':
|
||||
if check_linked and is_linked(context) == True:
|
||||
return False
|
||||
def basic_poll(cls, context, check_linked=False):
|
||||
"""Basic poll for boolean operators."""
|
||||
|
||||
return True
|
||||
if context.mode != 'OBJECT':
|
||||
return False
|
||||
if context.active_object is None:
|
||||
return False
|
||||
|
||||
obj = context.active_object
|
||||
if obj.type != 'MESH':
|
||||
cls.poll_message_set("Boolean operators can only be used for mesh objects")
|
||||
return False
|
||||
|
||||
if check_linked and is_linked(context, obj) == True:
|
||||
cls.poll_message_set("Boolean operators can not be executed on linked objects")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(context, obj=None):
|
||||
if not obj:
|
||||
obj = context.active_object
|
||||
def is_linked(context, obj):
|
||||
"""Checks whether the object is linked from an external .blend file (including library-overrides)."""
|
||||
|
||||
if obj not in context.editable_objects:
|
||||
if obj.library:
|
||||
@@ -31,19 +47,22 @@ def is_linked(context, obj=None):
|
||||
|
||||
|
||||
def is_canvas(obj):
|
||||
"""Checks whether the object is a boolean canvas (i.e. has boolean cutters)."""
|
||||
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
# Even if object is marked as canvas, check if it actually has any cutters
|
||||
cutters, __ = list_canvas_cutters([obj])
|
||||
if len(cutters) != 0:
|
||||
if len(cutters) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_instanced_data(obj):
|
||||
"""Checks if obj.data has more than one users, i.e. is instanced"""
|
||||
"""Function only considers object types as users, and excludes pointers"""
|
||||
"""Checks if `obj.data` has more than one users, i.e. is instanced."""
|
||||
"""Function only considers object types as users, and excludes pointers."""
|
||||
|
||||
data = bpy.data.meshes.get(obj.data.name)
|
||||
users = 0
|
||||
@@ -59,18 +78,103 @@ def is_instanced_data(obj):
|
||||
return False
|
||||
|
||||
|
||||
def active_modifier_poll(context):
|
||||
"""Checks whether the active modifier for active object is a boolean"""
|
||||
def active_modifier_poll(obj):
|
||||
"""Checks whether the active modifier for active object is a boolean."""
|
||||
|
||||
if context.object:
|
||||
if len(context.object.modifiers) == 0:
|
||||
return False
|
||||
# Check if active modifier exists.
|
||||
if len(obj.modifiers) == 0:
|
||||
return False
|
||||
if obj.modifiers.active is None:
|
||||
return False
|
||||
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
if modifier.object == None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
# Check if active modifier is a boolean with a valid object.
|
||||
modifier = obj.modifiers.active
|
||||
if modifier.type != "BOOLEAN":
|
||||
return False
|
||||
if modifier.object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_evaluated_mesh(context, obj):
|
||||
"""Checks if an object (non-mesh type) has an evaluated mesh created by Geometry Nodes modifiers."""
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
obj_eval = depsgraph.id_eval_get(obj)
|
||||
geometry = obj_eval.evaluated_geometry()
|
||||
|
||||
if geometry.mesh:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def list_candidate_objects(self, context, canvas):
|
||||
"""Filter out objects from the selection that can't be used as a cutter."""
|
||||
|
||||
cutters = []
|
||||
for obj in context.selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
if is_linked(context, obj):
|
||||
self.report({'WARNING'}, f"{obj.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
if obj.type == 'MESH':
|
||||
# Exclude if object is already a cutter for canvas.
|
||||
if canvas in list_cutter_users([obj]):
|
||||
continue
|
||||
# Exclude if canvas is cutting the object (avoid dependancy loop).
|
||||
if obj in list_cutter_users([canvas]):
|
||||
self.report({'WARNING'}, f"{obj.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
cutters.append(obj)
|
||||
|
||||
elif obj.type in ('CURVE', 'FONT'):
|
||||
if has_evaluated_mesh(context, obj):
|
||||
convert_to_mesh(context, obj)
|
||||
cutters.append(obj)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def destructive_op_confirmation(self, context, event, canvases: list, title="Boolean Operation"):
|
||||
"""
|
||||
Creates & returns the confirmation pop-up window for destructive boolean operators.\n
|
||||
Confirmation window is triggered by canvas objects that have instanced object data or shape keys.\n
|
||||
If none of the canvas objects have them the operator is executed without any confirmation.
|
||||
"""
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_data(obj))
|
||||
has_shape_keys = any(obj for obj in canvases if obj.data.shape_keys)
|
||||
|
||||
if has_instanced_data or has_shape_keys:
|
||||
# Instanced data message.
|
||||
if has_instanced_data and not has_shape_keys:
|
||||
message = ("Object(s) you're trying to cut have instanced object data.\n"
|
||||
"In order to apply modifiers, they need to be made single-user.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
# Shape keys message.
|
||||
if has_shape_keys and not has_instanced_data:
|
||||
message = ("Object(s) you're trying to cut have shape keys.\n"
|
||||
"In order to apply modifiers shape keys need to be applied as well.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
# Combined message.
|
||||
if has_instanced_data and has_shape_keys:
|
||||
message = ("Object(s) you're trying to cut have shape keys and instanced object data.\n"
|
||||
"In order to apply modifiers shape keys need to be applied, and object data made single user.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(self, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import bpy, mathutils
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from .draw import get_bounding_box_coords
|
||||
|
||||
from .math import get_bounding_box
|
||||
from .poll import is_linked, is_instanced_data
|
||||
|
||||
|
||||
@@ -59,7 +61,7 @@ def is_inside_selection(context, obj, rect_min, rect_max):
|
||||
for corner_2d in bound_corners_2d:
|
||||
if corner_2d and (rect_min.x <= corner_2d.x <= rect_max.x and rect_min.y <= corner_2d.y <= rect_max.y):
|
||||
return True
|
||||
|
||||
|
||||
# check_if_any_part_of_the_bounding_box_intersects_the_selection_rectangle
|
||||
min_x = min(corner_2d.x for corner_2d in bound_corners_2d if corner_2d)
|
||||
max_x = max(corner_2d.x for corner_2d in bound_corners_2d if corner_2d)
|
||||
@@ -69,33 +71,30 @@ def is_inside_selection(context, obj, rect_min, rect_max):
|
||||
return not (max_x < rect_min.x or min_x > rect_max.x or max_y < rect_min.y or min_y > rect_max.y)
|
||||
|
||||
|
||||
def selection_fallback(self, context, objects, include_cutters=False):
|
||||
"""Selects mesh objects that fall inside given 2d rectangle coordinates"""
|
||||
"""Used to get exactly which objects should be cut and avoid adding and applying unnecessary modifiers"""
|
||||
"""NOTE: bounding box isn't always returning correct results for objects, but full surface check would be too expensive"""
|
||||
def selection_fallback(self, context, objects, shape='BOX', include_cutters=False):
|
||||
"""Returns mesh objects that fall inside given 2d rectangle (bounding box of the shape) coordinates"""
|
||||
"""Needed to know exactly which objects should be carved, to avoid adding and applying unnecessary modifiers"""
|
||||
"""NOTE: bounding box isn't always returning correct results, but checking full shape would be too expensive"""
|
||||
|
||||
# convert_2d_rectangle_coordinates_to_world_coordinates
|
||||
if self.origin == 'EDGE':
|
||||
if self.shape == 'POLYLINE':
|
||||
x_values = [point[0] for point in self.mouse_path]
|
||||
y_values = [point[1] for point in self.mouse_path]
|
||||
rect_min = mathutils.Vector((min(x_values), min(y_values)))
|
||||
rect_max = mathutils.Vector((max(x_values), max(y_values)))
|
||||
else:
|
||||
if shape == 'POLYLINE':
|
||||
x_values = [point[0] for point in self.mouse_path]
|
||||
y_values = [point[1] for point in self.mouse_path]
|
||||
rect_min = mathutils.Vector((min(x_values), min(y_values)))
|
||||
rect_max = mathutils.Vector((max(x_values), max(y_values)))
|
||||
|
||||
elif shape == 'BOX':
|
||||
if self.origin == 'EDGE':
|
||||
rect_min = mathutils.Vector((min(self.mouse_path[0][0], self.mouse_path[1][0]),
|
||||
min(self.mouse_path[0][1], self.mouse_path[1][1])))
|
||||
rect_max = mathutils.Vector((max(self.mouse_path[0][0], self.mouse_path[1][0]),
|
||||
max(self.mouse_path[0][1], self.mouse_path[1][1])))
|
||||
|
||||
elif self.origin == 'CENTER':
|
||||
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
|
||||
if len(self.center_origin) == 0:
|
||||
get_bounding_box_coords(self, self.verts)
|
||||
elif self.origin == 'CENTER':
|
||||
# get_bounding_box_of_the_shape
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(self.verts)
|
||||
|
||||
rect_min = mathutils.Vector((min(self.center_origin[0][0], self.center_origin[1][0]),
|
||||
min(self.center_origin[0][1], self.center_origin[1][1])))
|
||||
rect_max = mathutils.Vector((max(self.center_origin[0][0], self.center_origin[1][0]),
|
||||
max(self.center_origin[0][1], self.center_origin[1][1])))
|
||||
rect_min = mathutils.Vector((min(min_x, max_x), min(min_y, max_y)))
|
||||
rect_max = mathutils.Vector((max(min_x, max_x), max(min_y, max_y)))
|
||||
|
||||
# ARRAY
|
||||
if self.rows > 1:
|
||||
@@ -103,6 +102,7 @@ def selection_fallback(self, context, objects, include_cutters=False):
|
||||
if self.columns > 1:
|
||||
rect_min.y = rect_max.y - (rect_max.y - rect_min.y) * self.columns - (self.columns_gap * (self.columns - 1))
|
||||
|
||||
|
||||
intersecting_objects = []
|
||||
for obj in objects:
|
||||
if obj.type != 'MESH':
|
||||
@@ -120,11 +120,8 @@ def selection_fallback(self, context, objects, include_cutters=False):
|
||||
continue
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
if obj.data.shape_keys:
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
if is_instanced_data(obj):
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has instanced object data")
|
||||
self.report({'ERROR'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
intersecting_objects.append(obj)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import bpy
|
||||
from . import (
|
||||
boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
)
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import bpy
|
||||
from collections import defaultdict
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
list_candidate_objects,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
apply_modifiers,
|
||||
)
|
||||
from ..functions.object import (
|
||||
apply_modifier,
|
||||
convert_to_mesh,
|
||||
add_boolean_modifier,
|
||||
set_cutter_properties,
|
||||
change_parent,
|
||||
create_slice,
|
||||
delete_cutter,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_candidate_objects,
|
||||
list_cutter_users,
|
||||
list_pre_boolean_modifiers,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class ModifierProperties():
|
||||
material_mode: bpy.props.EnumProperty(
|
||||
name = "Materials",
|
||||
description = "Method for setting materials on the new faces",
|
||||
items = (('INDEX', "Index Based", "Set the material on new faces based on the order of the material slot lists. If a material doesn’t exist on the\n"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn’t have enough slots."),
|
||||
('TRANSFER', "Transfer", "Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh.")),
|
||||
items = (('INDEX', "Index Based", ("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer", ("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
default = 'INDEX',
|
||||
)
|
||||
use_self: bpy.props.BoolProperty(
|
||||
@@ -60,7 +64,7 @@ class ModifierProperties():
|
||||
layout.prop(self, "material_mode")
|
||||
layout.prop(self, "use_self")
|
||||
layout.prop(self, "use_hole_tolerant")
|
||||
elif prefs.solver == 'FAST':
|
||||
elif prefs.solver == 'FLOAT':
|
||||
layout.prop(self, "double_threshold")
|
||||
|
||||
|
||||
@@ -68,20 +72,20 @@ class ModifierProperties():
|
||||
#### ------------------------------ /brush_boolean/ ------------------------------ ####
|
||||
|
||||
class BrushBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
# abort_when_no_selected_objects
|
||||
# Abort if there are less than 2 selected objects.
|
||||
if len(context.selected_objects) < 2:
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# abort_when_linked
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'WARNING'}, "Booleans can not be performed on linked objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.cutters = list_candidate_objects(self, context, context.active_object)
|
||||
if len(self.cutters) == 0:
|
||||
self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return self.execute(context)
|
||||
@@ -90,20 +94,23 @@ class BrushBoolean(ModifierProperties):
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
|
||||
# Create Slices
|
||||
if len(cutters) == 0:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in self.cutters:
|
||||
"""NOTE: Slices need to be created in separate loop to avoid inheriting boolean modifiers that operator adds"""
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
|
||||
for cutter in self.cutters:
|
||||
for cutter in cutters:
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
set_cutter_properties(context, canvas, cutter, self.mode, parent=prefs.parent, collection=prefs.use_collection)
|
||||
add_boolean_modifier(self, context, canvas, cutter, "DIFFERENCE" if self.mode == "SLICE" else self.mode, prefs.solver, pin=prefs.pin)
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
|
||||
|
||||
context.view_layer.objects.active = canvas
|
||||
canvas.booleans.canvas = True
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -115,10 +122,6 @@ class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean):
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
|
||||
@@ -128,10 +131,6 @@ class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean):
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
|
||||
@@ -141,10 +140,6 @@ class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean):
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
|
||||
@@ -154,10 +149,6 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
bl_description = "Slice active object along the selected ones. Will create slices as separate objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
|
||||
@@ -165,90 +156,89 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
#### ------------------------------ /auto_boolean/ ------------------------------ ####
|
||||
|
||||
class AutoBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
# abort_when_no_selected_objects
|
||||
# Abort if there are less than 2 selected objects.
|
||||
if len(context.selected_objects) < 2:
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# abort_when_linked
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'ERROR'}, "Modifiers can't be applied to linked object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.cutters = list_candidate_objects(self, context, context.active_object)
|
||||
if len(self.cutters) == 0:
|
||||
self.report({'ERROR'}, "Modifiers cannot be applied to linked object")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
if is_instanced_data(context.active_object):
|
||||
return context.window_manager.invoke_confirm(self, event,
|
||||
title="Auto Boolean", confirm_text="Yes", icon='WARNING',
|
||||
message=("Canvas object has instanced object data.\n"
|
||||
"In order to apply modifiers, it needs to be made single-user.\n"
|
||||
"Do you proceed?"))
|
||||
else:
|
||||
return self.execute(context)
|
||||
return destructive_op_confirmation(self, context, event, [context.active_object], title="Auto Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
new_modifiers = defaultdict(list)
|
||||
|
||||
# apply_modifiers
|
||||
if (prefs.apply_order == 'ALL') or (prefs.apply_order == 'BEFORE' and prefs.pin == False):
|
||||
convert_to_mesh(context, canvas)
|
||||
else:
|
||||
if canvas.data.shape_keys:
|
||||
self.report({'ERROR'}, "Modifiers can't be applied to object with shape keys")
|
||||
return {'CANCELLED'}
|
||||
if len(cutters) == 0:
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
# Create Slices
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in self.cutters:
|
||||
"""NOTE: Slices need to be created in separate loop to avoid inheriting boolean modifiers that operator adds"""
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, apply=True, single_user=True)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
|
||||
|
||||
for cutter in self.cutters:
|
||||
# Add Modifier (& Apply)
|
||||
for cutter in cutters:
|
||||
# Add boolean modifier on canvas.
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, apply=True, pin=prefs.pin, single_user=True)
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
|
||||
# Transfer Children
|
||||
# Transfer cutters children to canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(child, canvas)
|
||||
|
||||
# Delete Cutter
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply modifiers on canvas & slices.
|
||||
for obj, modifiers in new_modifiers.items():
|
||||
modifiers = self._get_modifiers_to_apply(prefs, obj, modifiers)
|
||||
apply_modifiers(context, obj, modifiers)
|
||||
|
||||
# Delete cutters.
|
||||
for cutter in cutters:
|
||||
delete_cutter(cutter)
|
||||
|
||||
if self.mode == "SLICE":
|
||||
slice.select_set(True)
|
||||
context.view_layer.objects.active = slice
|
||||
|
||||
|
||||
# apply_modifiers_before_final_boolean
|
||||
if prefs.apply_order == 'BEFORE' and prefs.pin:
|
||||
modifiers = list_pre_boolean_modifiers(canvas)
|
||||
for mod in modifiers:
|
||||
apply_modifier(context, canvas, mod, single_user=True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def _get_modifiers_to_apply(self, prefs, obj, new_modifiers) -> list:
|
||||
"""Returns a list of modifiers that need to be applied based on add-on preferences."""
|
||||
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in obj.modifiers]
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = new_modifiers
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(obj)
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_union(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_union"
|
||||
bl_label = "Boolean Union (Auto)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
|
||||
@@ -258,10 +248,6 @@ class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean):
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
|
||||
@@ -271,10 +257,6 @@ class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean):
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
|
||||
@@ -284,10 +266,6 @@ class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
bl_description = "Slice active object along the selected ones. Will create slices as separate objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context)
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
|
||||
@@ -317,7 +295,7 @@ def register():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Object Mode")
|
||||
|
||||
# brush_operators
|
||||
# Brush Operators
|
||||
kmi = km.keymap_items.new("object.boolean_brush_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True)
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -334,7 +312,7 @@ def register():
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
# auto_operators
|
||||
# Auto Operators
|
||||
kmi = km.keymap_items.new("object.boolean_auto_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True, shift=True)
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import bpy, itertools
|
||||
import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_canvas,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
)
|
||||
from ..functions.object import (
|
||||
apply_modifier,
|
||||
convert_to_mesh,
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
@@ -36,7 +39,7 @@ class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
canvases = list_selected_canvases(context)
|
||||
@@ -79,7 +82,7 @@ class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
@@ -153,29 +156,12 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Filter Objects
|
||||
self.canvases = []
|
||||
for obj in list_selected_canvases(context):
|
||||
# excude_canvases_with_shape_keys
|
||||
if obj.data.shape_keys:
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
|
||||
self.canvases.append(obj)
|
||||
|
||||
|
||||
if any(obj for obj in self.canvases if is_instanced_data(obj)):
|
||||
return context.window_manager.invoke_confirm(self, event,
|
||||
title="Apply Boolean Cutters", confirm_text="Yes", icon='WARNING',
|
||||
message=("Canvas object(s) have instanced object data.\n"
|
||||
"In order to apply modifiers, they need to be made single-user.\n"
|
||||
"Do you proceed?"))
|
||||
else:
|
||||
return self.execute(context)
|
||||
self.canvases = list_selected_canvases(context)
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
@@ -184,6 +170,8 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
cutters, __ = list_canvas_cutters(self.canvases)
|
||||
slices = list_canvas_slices(self.canvases)
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
@@ -193,17 +181,13 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
|
||||
# Apply Modifiers
|
||||
if prefs.apply_order == 'ALL':
|
||||
convert_to_mesh(context, canvas)
|
||||
|
||||
modifiers = [mod for mod in canvas.modifiers]
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(canvas)
|
||||
for mod in modifiers:
|
||||
apply_modifier(context, canvas, mod, single_user=True)
|
||||
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
apply_modifier(context, canvas, mod, single_user=True)
|
||||
modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name]
|
||||
|
||||
apply_modifiers(context, canvas, modifiers)
|
||||
|
||||
# remove_boolean_properties
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
@@ -4,9 +4,12 @@ from .. import __package__ as base_package
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
)
|
||||
from ..functions.object import (
|
||||
apply_modifier,
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
@@ -45,7 +48,7 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True)
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
def execute(self, context):
|
||||
if self.method == 'SPECIFIED':
|
||||
@@ -118,7 +121,7 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True)
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
@@ -220,7 +223,7 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context, check_linked=True)
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -232,25 +235,9 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
|
||||
elif self.method == 'ALL':
|
||||
self.cutters = list_selected_cutters(context)
|
||||
self.canvases = []
|
||||
self.canvases = list_cutter_users(self.cutters)
|
||||
|
||||
for obj in list_cutter_users(self.cutters):
|
||||
# excude_canvases_with_shape_keys
|
||||
if obj.data.shape_keys:
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
|
||||
self.canvases.append(obj)
|
||||
|
||||
|
||||
if any(obj for obj in self.canvases if is_instanced_data(obj)):
|
||||
return context.window_manager.invoke_confirm(self, event,
|
||||
title="Apply Boolean Cutter", confirm_text="Yes", icon='WARNING',
|
||||
message=("Canvas object(s) have instanced object data.\n"
|
||||
"In order to apply modifiers, they need to be made single-user.\n"
|
||||
"Do you proceed?"))
|
||||
else:
|
||||
return self.execute(context)
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
@@ -258,6 +245,8 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
leftovers = []
|
||||
|
||||
if self.cutters:
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in self.cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
@@ -266,10 +255,12 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
for canvas in self.canvases:
|
||||
context.view_layer.objects.active = canvas
|
||||
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in self.cutters:
|
||||
apply_modifier(context, canvas, mod, single_user=True)
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
@@ -281,9 +272,11 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
if self.method == 'SPECIFIED':
|
||||
# Apply Modifier for Slices (for_specified_method)
|
||||
for slice in self.slices:
|
||||
boolean_mods = []
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in self.cutters:
|
||||
apply_modifier(context, slice, mod, single_user=True)
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, slice, boolean_mods)
|
||||
|
||||
|
||||
unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True)
|
||||
|
||||
@@ -25,7 +25,7 @@ class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context) and context.active_object.booleans.cutter
|
||||
return basic_poll(cls, context) and context.active_object.booleans.cutter
|
||||
|
||||
def execute(self, context):
|
||||
cutters = list_selected_cutters(context)
|
||||
@@ -48,7 +48,7 @@ class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(context) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
canvases = list_selected_canvases(context)
|
||||
@@ -72,7 +72,7 @@ class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return (basic_poll(context) and active_modifier_poll(context) and
|
||||
return (basic_poll(cls, context) and active_modifier_poll(context.active_object) and
|
||||
context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and
|
||||
prefs.double_click)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from . import ui
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def update_sidebar_category(self, context):
|
||||
"""Change sidebar category of add-ons panels"""
|
||||
"""Change sidebar category of add-ons panel."""
|
||||
|
||||
panel_classes = [
|
||||
ui.VIEW3D_PT_boolean,
|
||||
@@ -31,13 +31,12 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
# UI
|
||||
show_in_sidebar: bpy.props.BoolProperty(
|
||||
name = "Show Addon Panel in Sidebar",
|
||||
description = ("Add add-on operators and properties to 3D viewport sidebar category.\n"
|
||||
"Most of the features are already available in 3D viewport's Object > Boolean menu, but brush list is only in sidebar panel"),
|
||||
description = "Add a sidebar panel in 3D Viewport with add-ons operators and properties",
|
||||
default = True,
|
||||
)
|
||||
sidebar_category: bpy.props.StringProperty(
|
||||
name = "Category Name",
|
||||
description = "Set sidebar category name. You can type in name of the existing category and panel will be added there, instead of creating new category",
|
||||
description = "Sidebar category name. Using the name of the existing category will add panel there",
|
||||
default = "Edit",
|
||||
update = update_sidebar_category,
|
||||
)
|
||||
@@ -46,9 +45,10 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
solver: bpy.props.EnumProperty(
|
||||
name = "Boolean Solver",
|
||||
description = "Which solver to use for automatic and brush booleans",
|
||||
items = [('FAST', "Fast", ""),
|
||||
('EXACT', "Exact", "")],
|
||||
default = 'FAST',
|
||||
items = [('FLOAT', "Float", ""),
|
||||
('EXACT', "Exact", ""),
|
||||
('MANIFOLD', "Manifold", "")],
|
||||
default = 'FLOAT',
|
||||
)
|
||||
wireframe: bpy.props.BoolProperty(
|
||||
name = "Display Cutters as Wireframe",
|
||||
@@ -98,6 +98,14 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
# Features
|
||||
fast_modifier_apply: bpy.props.BoolProperty(
|
||||
name = "Faster Destructive Booleans",
|
||||
description = ("Experimental method of applying modifiers that results in 30-50% faster destructive booleans.\n"
|
||||
"Performance improvements also affect the add-ons operators that apply cutters.\n"
|
||||
"However, changing modifier properties in the redo panel (like material transfer)\n"
|
||||
"is not available for this method yet."),
|
||||
default = False,
|
||||
)
|
||||
double_click: bpy.props.BoolProperty(
|
||||
name = "Double-click Select",
|
||||
description = ("Select boolean cutters by dbl-clicking on the boolean modifier.\n"
|
||||
@@ -157,6 +165,7 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
# Features
|
||||
layout.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
col.prop(self, "double_click")
|
||||
|
||||
# Experimentals
|
||||
|
||||
@@ -1,19 +1,59 @@
|
||||
import bpy
|
||||
from . import (
|
||||
carver,
|
||||
)
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [carver_box,
|
||||
carver_circle,
|
||||
carver_polyline,
|
||||
ui,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
carver_box,
|
||||
carver_circle,
|
||||
carver_polyline,
|
||||
)
|
||||
from .common import (
|
||||
ui,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
carver,
|
||||
carver_box,
|
||||
# carver_circle,
|
||||
carver_polyline,
|
||||
ui,
|
||||
]
|
||||
|
||||
main_tools = [
|
||||
carver_box.OBJECT_WT_carve_box,
|
||||
carver_box.MESH_WT_carve_box,
|
||||
]
|
||||
secondary_tools = [
|
||||
carver_circle.OBJECT_WT_carve_circle,
|
||||
carver_circle.MESH_WT_carve_circle,
|
||||
carver_polyline.OBJECT_WT_carve_polyline,
|
||||
carver_polyline.MESH_WT_carve_polyline,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
module.register()
|
||||
|
||||
for tool in main_tools:
|
||||
bpy.utils.register_tool(tool, separator=False, after="builtin.primitive_cube_add", group=True)
|
||||
for tool in secondary_tools:
|
||||
bpy.utils.register_tool(tool, separator=False, after="object.carve_box", group=False)
|
||||
|
||||
|
||||
def unregister():
|
||||
for module in reversed(modules):
|
||||
module.unregister()
|
||||
|
||||
for tool in main_tools:
|
||||
bpy.utils.unregister_tool(tool)
|
||||
for tool in secondary_tools:
|
||||
bpy.utils.unregister_tool(tool)
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
import bpy, mathutils, math, os
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.draw import (
|
||||
carver_overlay,
|
||||
)
|
||||
from ..functions.object import (
|
||||
add_boolean_modifier,
|
||||
set_cutter_properties,
|
||||
delete_cutter,
|
||||
set_object_origin,
|
||||
)
|
||||
from ..functions.mesh import (
|
||||
create_cutter_shape,
|
||||
extrude,
|
||||
shade_smooth_by_angle,
|
||||
)
|
||||
from ..functions.select import (
|
||||
cursor_snap,
|
||||
selection_fallback,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /tool_shelf_draw/ ------------------------------ ####
|
||||
|
||||
class CarverToolshelf():
|
||||
def draw_settings(context, layout, tool):
|
||||
props = tool.operator_properties("object.carve")
|
||||
if context.object:
|
||||
mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH"
|
||||
active_tool = context.workspace.tools.from_space_view3d_mode(mode, create=False).idname
|
||||
|
||||
layout.prop(props, "mode", text="")
|
||||
layout.prop(props, "depth", text="")
|
||||
row = layout.row()
|
||||
row.prop(props, "solver", expand=True)
|
||||
|
||||
if context.object:
|
||||
layout.popover("TOPBAR_PT_carver_shape", text="Shape")
|
||||
layout.popover("TOPBAR_PT_carver_array", text="Array")
|
||||
layout.popover("TOPBAR_PT_carver_cutter", text="Cutter")
|
||||
|
||||
class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
bl_label = "Carver Shape"
|
||||
bl_idname = "TOPBAR_PT_carver_shape"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH"
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode, create=False)
|
||||
op = tool.operator_properties("object.carve")
|
||||
|
||||
if tool.idname == "object.carve_polyline":
|
||||
layout.prop(op, "closed")
|
||||
else:
|
||||
if tool.idname == "object.carve_circle":
|
||||
layout.prop(op, "subdivision", text="Vertices")
|
||||
layout.prop(op, "rotation")
|
||||
layout.prop(op, "aspect", expand=True)
|
||||
layout.prop(op, "origin", expand=True)
|
||||
|
||||
if tool.idname == 'object.carve_box':
|
||||
layout.separator()
|
||||
layout.prop(op, "use_bevel", text="Bevel")
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
if prefs.experimental:
|
||||
row.prop(op, "bevel_profile", text="Profile", expand=True)
|
||||
col.prop(op, "bevel_segments", text="Segments")
|
||||
col.prop(op, "bevel_radius", text="Radius")
|
||||
|
||||
if op.use_bevel == False:
|
||||
col.enabled = False
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_array(bpy.types.Panel):
|
||||
bl_label = "Carver Array"
|
||||
bl_idname = "TOPBAR_PT_carver_array"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH"
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode, create=False)
|
||||
op = tool.operator_properties("object.carve")
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(op, "rows")
|
||||
row = col.row(align=True)
|
||||
row.prop(op, "rows_direction", text="Direction", expand=True)
|
||||
col.prop(op, "rows_gap", text="Gap")
|
||||
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(op, "columns")
|
||||
row = col.row(align=True)
|
||||
row.prop(op, "columns_direction", text="Direction", expand=True)
|
||||
col.prop(op, "columns_gap", text="Gap")
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
bl_label = "Carver Cutter"
|
||||
bl_idname = "TOPBAR_PT_carver_cutter"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH"
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode, create=False)
|
||||
op = tool.operator_properties("object.carve")
|
||||
|
||||
col = layout.column()
|
||||
col.prop(op, "pin", text="Pin Modifier")
|
||||
col.prop(op, "parent")
|
||||
if op.mode == 'MODIFIER':
|
||||
col.prop(op, "hide")
|
||||
|
||||
# auto_smooth
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(op, "auto_smooth", text="Auto Smooth")
|
||||
col.prop(op, "sharp_angle")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ TOOLS ------------------------------ ####
|
||||
|
||||
class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool, CarverToolshelf):
|
||||
bl_idname = "object.carve_box"
|
||||
bl_label = "Box Carve"
|
||||
bl_description = ("Boolean cut rectangular shapes into mesh objects")
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_box")
|
||||
# bl_widget = 'VIEW3D_GGT_placement'
|
||||
bl_keymap = (
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
)
|
||||
|
||||
class MESH_WT_carve_box(OBJECT_WT_carve_box):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
|
||||
|
||||
class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool, CarverToolshelf):
|
||||
bl_idname = "object.carve_circle"
|
||||
bl_label = "Circle Carve"
|
||||
bl_description = ("Boolean cut circlular shapes into mesh objects")
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_circle")
|
||||
# bl_widget = 'VIEW3D_GGT_placement'
|
||||
bl_keymap = (
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
)
|
||||
|
||||
class MESH_WT_carve_circle(OBJECT_WT_carve_circle):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
|
||||
|
||||
class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool, CarverToolshelf):
|
||||
bl_idname = "object.carve_polyline"
|
||||
bl_label = "Polyline Carve"
|
||||
bl_description = ("Boolean cut custom polygonal shapes into mesh objects")
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_polyline")
|
||||
# bl_widget = 'VIEW3D_GGT_placement'
|
||||
bl_keymap = (
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK'}, {"properties": [("shape", 'POLYLINE')]}),
|
||||
("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, {"properties": [("shape", 'POLYLINE')]}),
|
||||
# select
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("mode", 'ADD')]}),
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("mode", 'SUB')]}),
|
||||
)
|
||||
|
||||
class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve(bpy.types.Operator):
|
||||
bl_idname = "object.carve"
|
||||
bl_label = "Carve"
|
||||
bl_description = "Boolean cut square shapes into mesh objects"
|
||||
bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'}
|
||||
bl_cursor_pending = 'PICK_AREA'
|
||||
|
||||
# OPERATOR-properties
|
||||
shape: bpy.props.EnumProperty(
|
||||
name = "Shape",
|
||||
items = (('BOX', "Box", ""),
|
||||
('CIRCLE', "Circle", ""),
|
||||
('POLYLINE', "Polyline", "")),
|
||||
default = 'BOX',
|
||||
)
|
||||
mode: bpy.props.EnumProperty(
|
||||
name = "Mode",
|
||||
items = (('DESTRUCTIVE', "Destructive", "Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0),
|
||||
('MODIFIER', "Modifier", "Cuts are stored as boolean modifiers and cutters placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
default = 'DESTRUCTIVE',
|
||||
)
|
||||
# orientation: bpy.props.EnumProperty(
|
||||
# name = "Orientation",
|
||||
# items = (('SURFACE', "Surface", "Surface normal of the mesh under the cursor"),
|
||||
# ('VIEW', "View", "View-aligned orientation")),
|
||||
# default = 'SURFACE',
|
||||
# )
|
||||
depth: bpy.props.EnumProperty(
|
||||
name = "Depth",
|
||||
items = (('VIEW', "View", "Depth is automatically calculated from view orientation", 'VIEW_CAMERA_UNSELECTED', 0),
|
||||
('CURSOR', "Cursor", "Depth is automatically set at 3D cursor location", 'PIVOT_CURSOR', 1)),
|
||||
default = 'VIEW',
|
||||
)
|
||||
|
||||
# SHAPE-properties
|
||||
aspect: bpy.props.EnumProperty(
|
||||
name = "Aspect",
|
||||
items = (('FREE', "Free", "Use an unconstrained aspect"),
|
||||
('FIXED', "Fixed", "Use a fixed 1:1 aspect")),
|
||||
default = 'FREE',
|
||||
)
|
||||
origin: bpy.props.EnumProperty(
|
||||
name = "Origin",
|
||||
description = "The initial position for placement",
|
||||
items = (('EDGE', "Edge", ""),
|
||||
('CENTER', "Center", "")),
|
||||
default = 'EDGE',
|
||||
)
|
||||
rotation: bpy.props.FloatProperty(
|
||||
name = "Rotation",
|
||||
subtype = "ANGLE",
|
||||
soft_min = -360, soft_max = 360,
|
||||
default = 0,
|
||||
)
|
||||
subdivision: bpy.props.IntProperty(
|
||||
name = "Circle Subdivisions",
|
||||
description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder",
|
||||
min = 3, soft_max = 128,
|
||||
default = 16,
|
||||
)
|
||||
closed: bpy.props.BoolProperty(
|
||||
name = "Closed Polygon",
|
||||
description = "When enabled, mouse position at the moment of execution will be registered as last point of the polygon",
|
||||
default = True,
|
||||
)
|
||||
|
||||
# CUTTER-properties
|
||||
hide: bpy.props.BoolProperty(
|
||||
name = "Hide Cutter",
|
||||
description = ("Hide cutter objects in the viewport after they're created.\n"
|
||||
"NOTE: They are hidden in render regardless of this property"),
|
||||
default = True,
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent to Canvas",
|
||||
description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n"
|
||||
"If there is no active object in selection cutters parent might be chosen seemingly randomly"),
|
||||
default = True,
|
||||
)
|
||||
auto_smooth: bpy.props.BoolProperty(
|
||||
name = "Shade Auto Smooth",
|
||||
description = ("Cutter object will be shaded smooth with sharp edges (above 30 degrees) marked as sharp\n"
|
||||
"NOTE: This is one time operator. 'Smooth by Angle' modifier will not be added on object"),
|
||||
default = True,
|
||||
)
|
||||
sharp_angle: bpy.props.FloatProperty(
|
||||
name = "Angle",
|
||||
description = "Maximum face angle for sharp edges",
|
||||
subtype = "ANGLE",
|
||||
min = 0, max = math.pi,
|
||||
default = 0.523599,
|
||||
)
|
||||
|
||||
# ARRAY-properties
|
||||
rows: bpy.props.IntProperty(
|
||||
name = "Rows",
|
||||
description = "Number of times shape is duplicated on X axis",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
rows_gap: bpy.props.FloatProperty(
|
||||
name = "Gap between Rows",
|
||||
min = 0, soft_max = 250,
|
||||
default = 50,
|
||||
)
|
||||
rows_direction: bpy.props.EnumProperty(
|
||||
name = "Direction of Rows",
|
||||
items = (('LEFT', "Left", ""),
|
||||
('RIGHT', "Right", "")),
|
||||
default = 'RIGHT',
|
||||
)
|
||||
|
||||
columns: bpy.props.IntProperty(
|
||||
name = "Columns",
|
||||
description = "Number of times shape is duplicated on Y axis",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
columns_direction: bpy.props.EnumProperty(
|
||||
name = "Direction of Rows",
|
||||
items = (('UP', "Up", ""),
|
||||
('DOWN', "Down", "")),
|
||||
default = 'DOWN',
|
||||
)
|
||||
columns_gap: bpy.props.FloatProperty(
|
||||
name = "Gap between Columns",
|
||||
min = 0, soft_max = 250,
|
||||
default = 50,
|
||||
)
|
||||
|
||||
# BEVEL-properties
|
||||
use_bevel: bpy.props.BoolProperty(
|
||||
name = "Bevel Cutter",
|
||||
description = "Bevel each side edge of the cutter",
|
||||
default = False,
|
||||
)
|
||||
bevel_profile: bpy.props.EnumProperty(
|
||||
name = "Bevel Profile",
|
||||
items = (('CONVEX', "Convex", "Outside bevel (rounded corners)"),
|
||||
('CONCAVE', "Concave", "Inside bevel")),
|
||||
default = 'CONVEX',
|
||||
)
|
||||
bevel_segments: bpy.props.IntProperty(
|
||||
name = "Bevel Segments",
|
||||
description = "Segments for curved edge",
|
||||
min = 2, soft_max = 32,
|
||||
default = 8,
|
||||
)
|
||||
bevel_radius: bpy.props.FloatProperty(
|
||||
name = "Bevel Radius",
|
||||
description = "Amout of the bevel (in screen-space units)",
|
||||
min = 0.01, soft_max = 5,
|
||||
default = 1,
|
||||
)
|
||||
|
||||
# MODIFIER-properties
|
||||
solver: bpy.props.EnumProperty(
|
||||
name = "Solver",
|
||||
items = [('FAST', "Fast", ""),
|
||||
('EXACT', "Exact", "")],
|
||||
default = 'FAST',
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifier",
|
||||
description = ("When enabled boolean modifier will be moved above every other modifier on the object (if there are any).\n"
|
||||
"Order of modifiers can drastically affect the result (especially in destructive mode)"),
|
||||
default = True,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D'
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selected_objects = context.selected_objects
|
||||
self.initial_selection = context.selected_objects
|
||||
self.mouse_path = [(event.mouse_region_x, event.mouse_region_y),
|
||||
(event.mouse_region_x, event.mouse_region_y)]
|
||||
|
||||
# initialize_empty_values
|
||||
self.verts = []
|
||||
self.cutter = None
|
||||
self.duplicates = []
|
||||
self.view_depth = mathutils.Vector()
|
||||
self.cached_mouse_position = ()
|
||||
|
||||
# modifier_keys
|
||||
self.initial_origin = self.origin
|
||||
self.initial_aspect = self.aspect
|
||||
self.snap = False
|
||||
self.move = False
|
||||
self.rotate = False
|
||||
self.gap = False
|
||||
self.bevel = False
|
||||
|
||||
# overlay_position
|
||||
self.position_x = 0
|
||||
self.position_y = 0
|
||||
self.initial_position = False
|
||||
self.center_origin = []
|
||||
self.distance_from_first = 0
|
||||
|
||||
# Add Draw Handler
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_overlay, (self, bpy.context), 'WINDOW', 'POST_PIXEL')
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap else ""
|
||||
if self.shape == 'POLYLINE':
|
||||
shape_text = "[BACKSPACE]: Remove Last Point, [ENTER]: Confirm"
|
||||
else:
|
||||
shape_text = "[SHIFT]: Aspect, [ALT]: Origin, [R]: Rotate, [ARROWS]: Array"
|
||||
array_text = ", [A]: Gap" if (self.rows > 1 or self.columns > 1) else ""
|
||||
bevel_text = ", [B]: Bevel" if self.shape == 'BOX' else ""
|
||||
context.workspace.status_text_set("[CTRL]: Snap Invert, [SPACEBAR]: Move, " + shape_text + bevel_text + array_text + snap_text)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
region_types = {'WINDOW', 'UI'}
|
||||
for area in context.window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for region in area.regions:
|
||||
if not region_types or region.type in region_types:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
# SNAP
|
||||
# change_the_snap_increment_value_using_the_wheel_mouse
|
||||
if (self.move is False) and (self.rotate is False):
|
||||
for i, a in enumerate(context.screen.areas):
|
||||
if a.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
space.overlay.grid_subdivisions -= 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
space.overlay.grid_subdivisions += 1
|
||||
|
||||
self.snap = context.scene.tool_settings.use_snap
|
||||
if event.ctrl and (self.move is False) and (self.rotate is False):
|
||||
self.snap = not self.snap
|
||||
|
||||
|
||||
# ASPECT
|
||||
if event.shift and (self.shape != 'POLYLINE'):
|
||||
if self.initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self.initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self.initial_aspect
|
||||
|
||||
|
||||
# ORIGIN
|
||||
if event.alt and (self.shape != 'POLYLINE'):
|
||||
if self.initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self.initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self.initial_origin
|
||||
|
||||
|
||||
# ROTATE
|
||||
if event.type == 'R' and (self.shape != 'POLYLINE'):
|
||||
if event.value == 'PRESS':
|
||||
self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1])
|
||||
context.window.cursor_set("NONE")
|
||||
self.rotate = True
|
||||
elif event.value == 'RELEASE':
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1]))
|
||||
self.rotate = False
|
||||
|
||||
|
||||
# BEVEL
|
||||
if event.type == 'B' and (self.shape == 'BOX'):
|
||||
if event.value == 'PRESS':
|
||||
self.use_bevel = True
|
||||
self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1])
|
||||
context.window.cursor_set("NONE")
|
||||
self.bevel = True
|
||||
elif event.value == 'RELEASE':
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1]))
|
||||
self.bevel = False
|
||||
|
||||
if self.bevel:
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
|
||||
# ARRAY
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
if event.value == 'PRESS':
|
||||
self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1])
|
||||
context.window.cursor_set("NONE")
|
||||
self.gap = True
|
||||
elif event.value == 'RELEASE':
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window.cursor_warp(self.cached_mouse_position[0], self.cached_mouse_position[1])
|
||||
self.gap = False
|
||||
|
||||
|
||||
# MOVE
|
||||
if event.type == 'SPACE':
|
||||
if event.value == 'PRESS':
|
||||
self.move = True
|
||||
elif event.value == 'RELEASE':
|
||||
self.move = False
|
||||
|
||||
if self.move:
|
||||
# initial_position_variable_before_moving_the_brush
|
||||
if self.initial_position is False:
|
||||
self.position_x = 0
|
||||
self.position_y = 0
|
||||
self.last_mouse_region_x = event.mouse_region_x
|
||||
self.last_mouse_region_y = event.mouse_region_y
|
||||
self.initial_position = True
|
||||
self.move = True
|
||||
|
||||
# update_the_coordinates
|
||||
if self.initial_position and self.move is False:
|
||||
for i in range(0, len(self.mouse_path)):
|
||||
l = list(self.mouse_path[i])
|
||||
l[0] += self.position_x
|
||||
l[1] += self.position_y
|
||||
self.mouse_path[i] = tuple(l)
|
||||
|
||||
self.position_x = self.position_y = 0
|
||||
self.initial_position = False
|
||||
|
||||
|
||||
# Remove Point (Polyline)
|
||||
if event.type == 'BACK_SPACE' and event.value == 'PRESS':
|
||||
if len(self.mouse_path) > 2:
|
||||
context.window.cursor_warp(self.mouse_path[-2][0], self.mouse_path[-2][1])
|
||||
self.mouse_path = self.mouse_path[:-2]
|
||||
|
||||
|
||||
if event.type in {'MIDDLEMOUSE', 'N', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
|
||||
'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9'}:
|
||||
return {'PASS_THROUGH'}
|
||||
if self.bevel == False and event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# mouse_move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
if self.rotate:
|
||||
self.rotation = event.mouse_region_x * 0.01
|
||||
|
||||
elif self.move:
|
||||
# MOVE
|
||||
self.position_x += (event.mouse_region_x - self.last_mouse_region_x)
|
||||
self.position_y += (event.mouse_region_y - self.last_mouse_region_y)
|
||||
|
||||
self.last_mouse_region_x = event.mouse_region_x
|
||||
self.last_mouse_region_y = event.mouse_region_y
|
||||
|
||||
elif self.gap:
|
||||
self.rows_gap = event.mouse_region_x * 0.1
|
||||
self.columns_gap = event.mouse_region_y * 0.1
|
||||
|
||||
elif self.bevel:
|
||||
self.bevel_radius = event.mouse_region_x * 0.002
|
||||
|
||||
else:
|
||||
if len(self.mouse_path) > 0:
|
||||
# ASPECT
|
||||
if self.aspect == 'FIXED':
|
||||
side = max(abs(event.mouse_region_x - self.mouse_path[0][0]),
|
||||
abs(event.mouse_region_y - self.mouse_path[0][1]))
|
||||
self.mouse_path[len(self.mouse_path) - 1] = \
|
||||
(self.mouse_path[0][0] + (side if event.mouse_region_x >= self.mouse_path[0][0] else -side),
|
||||
self.mouse_path[0][1] + (side if event.mouse_region_y >= self.mouse_path[0][1] else -side))
|
||||
|
||||
elif self.aspect == 'FREE':
|
||||
self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y)
|
||||
|
||||
# SNAP (find_the_closest_position_on_the_overlay_grid_and_snap_the_shape_to_it)
|
||||
if self.snap:
|
||||
cursor_snap(self, context, event, self.mouse_path)
|
||||
|
||||
if self.shape == 'POLYLINE':
|
||||
# get_distance_from_first_point
|
||||
distance = math.sqrt((self.mouse_path[-1][0] - self.mouse_path[0][0]) ** 2 +
|
||||
(self.mouse_path[-1][1] - self.mouse_path[0][1]) ** 2)
|
||||
min_radius = 0
|
||||
max_radius = 30
|
||||
self.distance_from_first = max(max_radius - distance, min_radius)
|
||||
|
||||
|
||||
# Confirm
|
||||
elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'):
|
||||
# selection_fallback
|
||||
if self.shape != 'POLYLINE':
|
||||
if len(self.selected_objects) == 0:
|
||||
self.selected_objects = selection_fallback(self, context, context.view_layer.objects)
|
||||
for obj in self.selected_objects:
|
||||
obj.select_set(True)
|
||||
|
||||
if len(self.selected_objects) == 0:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
empty = self.selection_fallback(context)
|
||||
if empty:
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
if len(self.initial_selection) == 0:
|
||||
# expand_selection_fallback_on_every_polyline_click
|
||||
self.selected_objects = selection_fallback(self, context, context.view_layer.objects)
|
||||
for obj in self.selected_objects:
|
||||
obj.select_set(True)
|
||||
|
||||
# Polyline
|
||||
if self.shape == 'POLYLINE':
|
||||
if not (event.type == 'RET' and event.value == 'PRESS') and (self.distance_from_first < 15):
|
||||
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
if self.closed == False:
|
||||
# NOTE: Additional vert is needed for open loop.
|
||||
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
else:
|
||||
# Confirm Cut (Polyline)
|
||||
if self.closed == False:
|
||||
self.verts.pop() # dont_add_current_mouse_position_as_vert
|
||||
|
||||
if self.distance_from_first > 15:
|
||||
self.verts[-1] = self.verts[0]
|
||||
|
||||
if len(self.verts) / 2 <= 1:
|
||||
self.report({'INFO'}, "At least two points are required to make polygonal shape")
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.closed and self.mouse_path[-1] == self.mouse_path[-2]:
|
||||
context.window.cursor_warp(event.mouse_region_x - 1, event.mouse_region_y)
|
||||
|
||||
# NOTE: Polyline needs separate selection fallback, because it needs to calculate selection bounding box...
|
||||
# NOTE: after all points are already drawn, i.e. before execution.
|
||||
empty = self.selection_fallback(context)
|
||||
if empty:
|
||||
return {'FINISHED'}
|
||||
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
# Confirm Cut (Box, Circle)
|
||||
else:
|
||||
# protection_against_returning_no_rectangle_by_clicking
|
||||
delta_x = abs(event.mouse_region_x - self.mouse_path[0][0])
|
||||
delta_y = abs(event.mouse_region_y - self.mouse_path[0][1])
|
||||
min_distance = 5
|
||||
|
||||
if delta_x > min_distance or delta_y > min_distance:
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def confirm(self, context):
|
||||
create_cutter_shape(self, context)
|
||||
extrude(self, self.cutter.data)
|
||||
set_object_origin(self.cutter)
|
||||
if self.auto_smooth:
|
||||
shade_smooth_by_angle(self.cutter, angle=math.degrees(self.sharp_angle))
|
||||
|
||||
self.Cut(context)
|
||||
self.cancel(context)
|
||||
|
||||
|
||||
def cancel(self, context):
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||
context.workspace.status_text_set(None)
|
||||
context.window.cursor_set('DEFAULT' if context.object.mode == 'OBJECT' else 'CROSSHAIR')
|
||||
|
||||
|
||||
def selection_fallback(self, context):
|
||||
# filter_out_objects_not_inside_the_selection_bounding_box
|
||||
self.selected_objects = selection_fallback(self, context, self.selected_objects, include_cutters=True)
|
||||
|
||||
# silently_fail_if_no_objects_inside_selection_bounding_box
|
||||
empty = False
|
||||
if len(self.selected_objects) == 0:
|
||||
self.cancel(context)
|
||||
empty = True
|
||||
|
||||
return empty
|
||||
|
||||
|
||||
def Cut(self, context):
|
||||
# ensure_active_object
|
||||
if not context.active_object:
|
||||
context.view_layer.objects.active = self.selected_objects[0]
|
||||
|
||||
# Add Modifier
|
||||
for obj in self.selected_objects:
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, apply=True, pin=self.pin, redo=False)
|
||||
elif self.mode == 'MODIFIER':
|
||||
add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, pin=self.pin, redo=False)
|
||||
obj.booleans.canvas = True
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
# Remove Cutter
|
||||
delete_cutter(self.cutter)
|
||||
|
||||
elif self.mode == 'MODIFIER':
|
||||
# Set Cutter Properties
|
||||
canvas = None
|
||||
if context.active_object and context.active_object in self.selected_objects:
|
||||
canvas = context.active_object
|
||||
else:
|
||||
canvas = self.selected_objects[0]
|
||||
|
||||
set_cutter_properties(context, canvas, self.cutter, "Difference", parent=self.parent, hide=self.hide)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
OBJECT_OT_carve,
|
||||
TOPBAR_PT_carver_shape,
|
||||
TOPBAR_PT_carver_array,
|
||||
TOPBAR_PT_carver_cutter,
|
||||
]
|
||||
|
||||
main_tools = [
|
||||
OBJECT_WT_carve_box,
|
||||
MESH_WT_carve_box,
|
||||
]
|
||||
secondary_tools = [
|
||||
OBJECT_WT_carve_circle,
|
||||
OBJECT_WT_carve_polyline,
|
||||
MESH_WT_carve_circle,
|
||||
MESH_WT_carve_polyline,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
for tool in main_tools:
|
||||
bpy.utils.register_tool(tool, separator=False, after="builtin.primitive_cube_add", group=True)
|
||||
for tool in secondary_tools:
|
||||
bpy.utils.register_tool(tool, separator=False, after="object.carve_box", group=False)
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
for tool in main_tools:
|
||||
bpy.utils.unregister_tool(tool)
|
||||
for tool in secondary_tools:
|
||||
bpy.utils.unregister_tool(tool)
|
||||
@@ -0,0 +1,272 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import os
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from .common.base import (
|
||||
CarverModifierKeys,
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverOperatorProperties,
|
||||
CarverModifierProperties,
|
||||
CarverCutterProperties,
|
||||
CarverArrayProperties,
|
||||
CarverBevelProperties,
|
||||
)
|
||||
from .common.ui import (
|
||||
carver_ui_common,
|
||||
)
|
||||
|
||||
from ..functions.draw import (
|
||||
carver_shape_box,
|
||||
)
|
||||
from ..functions.select import (
|
||||
cursor_snap,
|
||||
selection_fallback,
|
||||
)
|
||||
|
||||
|
||||
description = "Cut primitive shapes into mesh objects by box drawing"
|
||||
|
||||
#### ------------------------------ TOOLS ------------------------------ ####
|
||||
|
||||
class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool):
|
||||
bl_idname = "object.carve_box"
|
||||
bl_label = "Box Carve"
|
||||
bl_description = description
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_box")
|
||||
bl_keymap = (
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
carver_ui_common(context, layout, props)
|
||||
|
||||
|
||||
class MESH_WT_carve_box(OBJECT_WT_carve_box):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_box(CarverBase, CarverModifierKeys, bpy.types.Operator,
|
||||
CarverOperatorProperties, CarverModifierProperties, CarverCutterProperties,
|
||||
CarverArrayProperties, CarverBevelProperties):
|
||||
bl_idname = "object.carve_box"
|
||||
bl_label = "Box Carve"
|
||||
bl_description = description
|
||||
bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'}
|
||||
bl_cursor_pending = 'PICK_AREA'
|
||||
|
||||
shape: bpy.props.EnumProperty(
|
||||
name = "Shape",
|
||||
items = (('BOX', "Box", ""),
|
||||
('CIRCLE', "Circle", ""),
|
||||
('POLYLINE', "Polyline", "")),
|
||||
default = 'BOX',
|
||||
)
|
||||
|
||||
# SHAPE-properties
|
||||
aspect: bpy.props.EnumProperty(
|
||||
name = "Aspect",
|
||||
items = (('FREE', "Free", "Use an unconstrained aspect"),
|
||||
('FIXED', "Fixed", "Use a fixed 1:1 aspect")),
|
||||
default = 'FREE',
|
||||
)
|
||||
origin: bpy.props.EnumProperty(
|
||||
name = "Origin",
|
||||
description = "The initial position for placement",
|
||||
items = (('EDGE', "Edge", ""),
|
||||
('CENTER', "Center", "")),
|
||||
default = 'EDGE',
|
||||
)
|
||||
rotation: bpy.props.FloatProperty(
|
||||
name = "Rotation",
|
||||
subtype = "ANGLE",
|
||||
soft_min = -360, soft_max = 360,
|
||||
default = 0,
|
||||
)
|
||||
subdivision: bpy.props.IntProperty(
|
||||
name = "Circle Subdivisions",
|
||||
description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder",
|
||||
min = 3, soft_max = 128,
|
||||
default = 16,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D'
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selected_objects = context.selected_objects
|
||||
self.mouse_path = [(event.mouse_region_x, event.mouse_region_y),
|
||||
(event.mouse_region_x, event.mouse_region_y)]
|
||||
|
||||
# initialize_empty_values
|
||||
self.verts = []
|
||||
self.duplicates = []
|
||||
self.cutter = None
|
||||
self.view_depth = mathutils.Vector()
|
||||
self.cached_mouse_position = () # needed_for_custom_modifier_keys
|
||||
|
||||
# cached_variables
|
||||
"""Important for storing context as it was when operator was invoked (untouched by the modal)"""
|
||||
self.initial_origin = self.origin
|
||||
self.initial_aspect = self.aspect
|
||||
|
||||
# modifier_keys
|
||||
self.snap = False
|
||||
self.move = False
|
||||
self.rotate = False
|
||||
self.gap = False
|
||||
self.bevel = False
|
||||
|
||||
# overlay_position (needed_for_moving_the_shape)
|
||||
self.position_offset_x = 0
|
||||
self.position_offset_y = 0
|
||||
self.initial_position = False
|
||||
|
||||
# Add Draw Handler
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_shape_box, (self, context, self.shape), 'WINDOW', 'POST_PIXEL')
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
# Status Bar Text
|
||||
snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap else ""
|
||||
shape_text = "[SHIFT]: Aspect, [ALT]: Origin, [R]: Rotate, [ARROWS]: Array"
|
||||
array_text = ", [A]: Gap" if (self.rows > 1 or self.columns > 1) else ""
|
||||
bevel_text = ", [B]: Bevel" if self.shape == 'BOX' else ""
|
||||
context.workspace.status_text_set("[CTRL]: Snap Invert, [SPACEBAR]: Move, " + shape_text + bevel_text + array_text + snap_text)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
|
||||
# Modifier Keys
|
||||
self.modifier_snap(context, event)
|
||||
self.modifier_aspect(context, event)
|
||||
self.modifier_origin(context, event)
|
||||
self.modifier_rotate(context, event)
|
||||
self.modifier_bevel(context, event)
|
||||
self.modifier_array(context, event)
|
||||
self.modifier_move(context, event)
|
||||
|
||||
if event.type in {'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
|
||||
'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9',
|
||||
'MIDDLEMOUSE', 'N'}:
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
if self.bevel == False and event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
# move
|
||||
if self.move:
|
||||
self.position_offset_x += (event.mouse_region_x - self.last_mouse_region_x)
|
||||
self.position_offset_y += (event.mouse_region_y - self.last_mouse_region_y)
|
||||
self.last_mouse_region_x = event.mouse_region_x
|
||||
self.last_mouse_region_y = event.mouse_region_y
|
||||
|
||||
# rotate
|
||||
elif self.rotate:
|
||||
self.rotation = event.mouse_region_x * 0.01
|
||||
|
||||
# array
|
||||
elif self.gap:
|
||||
self.rows_gap = event.mouse_region_x * 0.1
|
||||
self.columns_gap = event.mouse_region_y * 0.1
|
||||
|
||||
# bevel
|
||||
elif self.bevel:
|
||||
self.bevel_radius = event.mouse_region_x * 0.002
|
||||
|
||||
# Draw Shape
|
||||
else:
|
||||
if len(self.mouse_path) > 0:
|
||||
# aspect
|
||||
if self.aspect == 'FIXED':
|
||||
side = max(abs(event.mouse_region_x - self.mouse_path[0][0]),
|
||||
abs(event.mouse_region_y - self.mouse_path[0][1]))
|
||||
self.mouse_path[len(self.mouse_path) - 1] = \
|
||||
(self.mouse_path[0][0] + (side if event.mouse_region_x >= self.mouse_path[0][0] else -side),
|
||||
self.mouse_path[0][1] + (side if event.mouse_region_y >= self.mouse_path[0][1] else -side))
|
||||
|
||||
elif self.aspect == 'FREE':
|
||||
self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y)
|
||||
|
||||
# snap (find_the_closest_position_on_the_overlay_grid_and_snap_the_shape_to_it)
|
||||
if self.snap:
|
||||
cursor_snap(self, context, event, self.mouse_path)
|
||||
|
||||
|
||||
# Confirm
|
||||
elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'):
|
||||
# selection_fallback
|
||||
if len(self.selected_objects) == 0:
|
||||
self.selected_objects = selection_fallback(self, context, context.view_layer.objects, shape='BOX')
|
||||
for obj in self.selected_objects:
|
||||
obj.select_set(True)
|
||||
|
||||
if len(self.selected_objects) == 0:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
selection = self.validate_selection(context, shape='BOX')
|
||||
if not selection:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
# protection_against_returning_no_rectangle_by_clicking
|
||||
delta_x = abs(event.mouse_region_x - self.mouse_path[0][0])
|
||||
delta_y = abs(event.mouse_region_y - self.mouse_path[0][1])
|
||||
min_distance = 5
|
||||
|
||||
if delta_x > min_distance or delta_y > min_distance:
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
OBJECT_OT_carve_box,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,39 @@
|
||||
import bpy
|
||||
import os
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from .common.ui import (
|
||||
carver_ui_common,
|
||||
)
|
||||
|
||||
|
||||
description = "Cut primitive shapes into mesh objects with brush"
|
||||
|
||||
#### ------------------------------ TOOLS ------------------------------ ####
|
||||
|
||||
class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool):
|
||||
bl_idname = "object.carve_circle"
|
||||
bl_label = "Circle Carve"
|
||||
bl_description = description
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_circle")
|
||||
bl_keymap = (
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
carver_ui_common(context, layout, props)
|
||||
|
||||
class MESH_WT_carve_circle(OBJECT_WT_carve_circle):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
@@ -0,0 +1,241 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import math
|
||||
import os
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from .common.base import (
|
||||
CarverModifierKeys,
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverOperatorProperties,
|
||||
CarverModifierProperties,
|
||||
CarverCutterProperties,
|
||||
CarverArrayProperties,
|
||||
)
|
||||
from .common.ui import (
|
||||
carver_ui_common,
|
||||
)
|
||||
|
||||
from ..functions.draw import (
|
||||
carver_shape_polyline,
|
||||
)
|
||||
from ..functions.select import (
|
||||
cursor_snap,
|
||||
selection_fallback,
|
||||
)
|
||||
|
||||
|
||||
description = "Cut custom polygonal shapes into mesh objects"
|
||||
|
||||
#### ------------------------------ TOOLS ------------------------------ ####
|
||||
|
||||
class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool):
|
||||
bl_idname = "object.carve_polyline"
|
||||
bl_label = "Polyline Carve"
|
||||
bl_description = description
|
||||
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_polyline")
|
||||
bl_keymap = (
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None),
|
||||
# select
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("mode", 'ADD')]}),
|
||||
("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("mode", 'SUB')]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
carver_ui_common(context, layout, props)
|
||||
|
||||
|
||||
class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
|
||||
bl_context_mode = 'EDIT_MESH'
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_polyline(CarverBase, CarverModifierKeys, bpy.types.Operator,
|
||||
CarverOperatorProperties, CarverModifierProperties, CarverCutterProperties, CarverArrayProperties):
|
||||
bl_idname = "object.carve_polyline"
|
||||
bl_label = "Polyline Carve"
|
||||
bl_description = description
|
||||
bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'}
|
||||
bl_cursor_pending = 'PICK_AREA'
|
||||
|
||||
# SHAPE-properties
|
||||
closed: bpy.props.BoolProperty(
|
||||
name = "Closed Polygon",
|
||||
description = "When enabled, mouse position at the moment of execution will be registered as last point of the polygon",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D'
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.selected_objects = context.selected_objects
|
||||
self.mouse_path = [(event.mouse_region_x, event.mouse_region_y),
|
||||
(event.mouse_region_x, event.mouse_region_y)]
|
||||
|
||||
# initialize_empty_values
|
||||
self.verts = []
|
||||
self.duplicates = []
|
||||
self.cutter = None
|
||||
self.view_depth = mathutils.Vector()
|
||||
self.cached_mouse_position = () # needed_for_custom_modifier_keys
|
||||
self.distance_from_first = 0
|
||||
|
||||
# cached_variables
|
||||
"""Important for storing context as it was when operator was invoked (untouched by the modal)"""
|
||||
self.initial_selection = context.selected_objects
|
||||
|
||||
# modifier_keys
|
||||
self.snap = False
|
||||
self.move = False
|
||||
self.gap = False
|
||||
|
||||
# overlay_position (needed_for_moving_the_shape)
|
||||
self.position_offset_x = 0
|
||||
self.position_offset_y = 0
|
||||
self.initial_position = False
|
||||
|
||||
# Add Draw Handler
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_shape_polyline, (self, context), 'WINDOW', 'POST_PIXEL')
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
# Tool Settings Text
|
||||
snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap else ""
|
||||
shape_text = "[BACKSPACE]: Remove Last Point, [ENTER]: Confirm"
|
||||
array_text = ", [A]: Gap" if (self.rows > 1 or self.columns > 1) else ""
|
||||
context.workspace.status_text_set("[CTRL]: Snap Invert, [SPACEBAR]: Move, " + shape_text + array_text + snap_text)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
|
||||
# Modifier Keys
|
||||
self.modifier_snap(context, event)
|
||||
self.modifier_array(context, event)
|
||||
self.modifier_move(context, event)
|
||||
|
||||
if event.type in {'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
|
||||
'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9',
|
||||
'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'N'}:
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
# move
|
||||
if self.move:
|
||||
self.position_offset_x += (event.mouse_region_x - self.last_mouse_region_x)
|
||||
self.position_offset_y += (event.mouse_region_y - self.last_mouse_region_y)
|
||||
self.last_mouse_region_x = event.mouse_region_x
|
||||
self.last_mouse_region_y = event.mouse_region_y
|
||||
|
||||
# array
|
||||
elif self.gap:
|
||||
self.rows_gap = event.mouse_region_x * 0.1
|
||||
self.columns_gap = event.mouse_region_y * 0.1
|
||||
|
||||
# Draw Shape
|
||||
else:
|
||||
if len(self.mouse_path) > 0:
|
||||
self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y)
|
||||
|
||||
# snap (find_the_closest_position_on_the_overlay_grid_and_snap_the_shape_to_it)
|
||||
if self.snap:
|
||||
cursor_snap(self, context, event, self.mouse_path)
|
||||
|
||||
# get_distance_from_first_point
|
||||
distance = math.sqrt((self.mouse_path[-1][0] - self.mouse_path[0][0]) ** 2 +
|
||||
(self.mouse_path[-1][1] - self.mouse_path[0][1]) ** 2)
|
||||
min_radius = 0
|
||||
max_radius = 30
|
||||
self.distance_from_first = max(max_radius - distance, min_radius)
|
||||
|
||||
|
||||
# Add Points & Confirm
|
||||
elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'):
|
||||
# selection_fallback (expand_selection_on_every_polyline_click)
|
||||
if len(self.initial_selection) == 0:
|
||||
self.selected_objects = selection_fallback(self, context, context.view_layer.objects, shape='POLYLINE')
|
||||
for obj in self.selected_objects:
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
# add_new_points
|
||||
if not (event.type == 'RET' and event.value == 'PRESS') and (self.distance_from_first < 15):
|
||||
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
if self.closed == False:
|
||||
"""NOTE: Additional vert is needed for open loop."""
|
||||
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
|
||||
# confirm_cut
|
||||
else:
|
||||
if self.closed == False:
|
||||
self.verts.pop() # dont_add_current_mouse_position_as_vert
|
||||
|
||||
if self.distance_from_first > 15:
|
||||
self.verts[-1] = self.verts[0]
|
||||
|
||||
if len(self.verts) / 2 <= 1:
|
||||
self.report({'INFO'}, "At least two points are required to make polygonal shape")
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.closed and self.mouse_path[-1] == self.mouse_path[-2]:
|
||||
context.window.cursor_warp(event.mouse_region_x - 1, event.mouse_region_y)
|
||||
|
||||
selection = self.validate_selection(context, shape='POLYLINE')
|
||||
if not selection:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Remove Last Point
|
||||
if event.type == 'BACK_SPACE' and event.value == 'PRESS':
|
||||
if len(self.mouse_path) > 2:
|
||||
context.window.cursor_warp(int(self.mouse_path[-2][0]), int(self.mouse_path[-2][1]))
|
||||
self.mouse_path = self.mouse_path[:-1]
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
OBJECT_OT_carve_polyline,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,239 @@
|
||||
import bpy
|
||||
import math
|
||||
|
||||
from ...functions.mesh import (
|
||||
create_cutter_shape,
|
||||
extrude,
|
||||
shade_smooth_by_angle,
|
||||
)
|
||||
from ...functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
apply_modifiers,
|
||||
)
|
||||
from ...functions.object import (
|
||||
set_cutter_properties,
|
||||
delete_cutter,
|
||||
set_object_origin,
|
||||
)
|
||||
from ...functions.select import (
|
||||
selection_fallback,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def custom_modifier_event(self, context, event, modifier):
|
||||
"""Creates custom modifier event when key is held and hides cursor until it's released"""
|
||||
|
||||
if event.value == 'PRESS':
|
||||
if not self.move:
|
||||
self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1])
|
||||
context.window.cursor_set("NONE")
|
||||
setattr(self, modifier, True)
|
||||
|
||||
elif event.value == 'RELEASE':
|
||||
if not self.move:
|
||||
context.window.cursor_set("MUTE")
|
||||
context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1]))
|
||||
setattr(self, modifier, False)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /base/ ------------------------------ ####
|
||||
|
||||
class CarverModifierKeys():
|
||||
"""NOTE: Order of the modifier key events is important, because key value might change after function checks for it"""
|
||||
"""Functions that check last are most important because they can overwrite all modifier states"""
|
||||
|
||||
def modifier_snap(self, context, event):
|
||||
"""Modifier keys for snapping"""
|
||||
|
||||
self.snap = context.scene.tool_settings.use_snap
|
||||
if (self.move == False) and (not hasattr(self, "rotate") or (hasattr(self, "rotate") and not self.rotate)):
|
||||
|
||||
# change_the_snap_increment_value_using_the_wheel_mouse
|
||||
for i, area in enumerate(context.screen.areas):
|
||||
if area.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
space.overlay.grid_subdivisions -= 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
space.overlay.grid_subdivisions += 1
|
||||
|
||||
# invert_snapping
|
||||
if event.ctrl:
|
||||
self.snap = not self.snap
|
||||
|
||||
|
||||
def modifier_aspect(self, context, event):
|
||||
"""Modifier keys for changing aspect of the shape"""
|
||||
|
||||
if event.shift:
|
||||
if self.initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self.initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self.initial_aspect
|
||||
|
||||
|
||||
def modifier_origin(self, context, event):
|
||||
"""Modifier keys for changing the origin of the shape"""
|
||||
|
||||
if event.alt:
|
||||
if self.initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self.initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self.initial_origin
|
||||
|
||||
|
||||
def modifier_rotate(self, context, event):
|
||||
"""Modifier keys for rotating the shape"""
|
||||
|
||||
if event.type == 'R':
|
||||
custom_modifier_event(self, context, event, "rotate")
|
||||
|
||||
|
||||
def modifier_bevel(self, context, event):
|
||||
"""Modifier keys for beveling the shape"""
|
||||
|
||||
if self.shape == 'BOX':
|
||||
if event.type == 'B':
|
||||
custom_modifier_event(self, context, event, "bevel")
|
||||
|
||||
if self.bevel:
|
||||
self.use_bevel = True
|
||||
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
|
||||
def modifier_array(self, context, event):
|
||||
"""Modifier keys for creating the array of the shape"""
|
||||
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
custom_modifier_event(self, context, event, "gap")
|
||||
|
||||
|
||||
def modifier_move(self, context, event):
|
||||
"""Modifier keys for moving the shape"""
|
||||
|
||||
if event.type == 'SPACE':
|
||||
if event.value == 'PRESS':
|
||||
self.move = True
|
||||
elif event.value == 'RELEASE':
|
||||
self.move = False
|
||||
|
||||
if self.move:
|
||||
# reset_initial_position_before_moving_the_shape
|
||||
if self.initial_position is False:
|
||||
self.position_offset_x = 0
|
||||
self.position_offset_y = 0
|
||||
self.last_mouse_region_x = event.mouse_region_x
|
||||
self.last_mouse_region_y = event.mouse_region_y
|
||||
self.initial_position = True
|
||||
else:
|
||||
# update_the_shape_coordinates
|
||||
if self.initial_position:
|
||||
for i in range(0, len(self.mouse_path)):
|
||||
l = list(self.mouse_path[i])
|
||||
l[0] += self.position_offset_x
|
||||
l[1] += self.position_offset_y
|
||||
self.mouse_path[i] = tuple(l)
|
||||
|
||||
self.position_offset_x = self.position_offset_y = 0
|
||||
self.initial_position = False
|
||||
|
||||
|
||||
class CarverBase():
|
||||
|
||||
def redraw_region(self, context):
|
||||
"""Redraw region to find the limits of the 3D viewport"""
|
||||
|
||||
region_types = {'WINDOW', 'UI'}
|
||||
for area in context.window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for region in area.regions:
|
||||
if not region_types or region.type in region_types:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
def validate_selection(self, context, shape='BOX'):
|
||||
"""Filters out objects that are not inside the selection shape bounding box"""
|
||||
"""Returns selection state (so operator can be cancelled if there are no objects inside the selection bounding box)"""
|
||||
|
||||
self.selected_objects = selection_fallback(self, context, self.selected_objects, shape=shape, include_cutters=True)
|
||||
|
||||
# silently_fail_if_no_objects_inside_selection_bounding_box
|
||||
if len(self.selected_objects) == 0:
|
||||
selection = False
|
||||
else:
|
||||
selection = True
|
||||
|
||||
return selection
|
||||
|
||||
|
||||
def confirm(self, context):
|
||||
create_cutter_shape(self, context)
|
||||
extrude(self, self.cutter.data)
|
||||
set_object_origin(self.cutter)
|
||||
if self.auto_smooth:
|
||||
shade_smooth_by_angle(self.cutter, angle=math.degrees(self.sharp_angle))
|
||||
|
||||
self.Cut(context)
|
||||
self.cancel(context)
|
||||
|
||||
|
||||
def cancel(self, context):
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||
context.workspace.status_text_set(None)
|
||||
context.window.cursor_set('DEFAULT' if context.mode == 'OBJECT' else 'CROSSHAIR')
|
||||
|
||||
|
||||
def Cut(self, context):
|
||||
# ensure_active_object
|
||||
if not context.active_object:
|
||||
context.view_layer.objects.active = self.selected_objects[0]
|
||||
|
||||
# Add Modifier
|
||||
for obj in self.selected_objects:
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for face in self.cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
mod = add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, pin=self.pin, redo=False)
|
||||
apply_modifiers(context, obj, [mod])
|
||||
|
||||
elif self.mode == 'MODIFIER':
|
||||
add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, pin=self.pin, redo=False)
|
||||
obj.booleans.canvas = True
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
# Remove Cutter
|
||||
delete_cutter(self.cutter)
|
||||
|
||||
elif self.mode == 'MODIFIER':
|
||||
# Set Cutter Properties
|
||||
canvas = None
|
||||
if context.active_object and context.active_object in self.selected_objects:
|
||||
canvas = context.active_object
|
||||
else:
|
||||
canvas = self.selected_objects[0]
|
||||
|
||||
set_cutter_properties(context, canvas, self.cutter, "Difference", parent=self.parent, hide=self.hide)
|
||||
@@ -0,0 +1,133 @@
|
||||
import bpy
|
||||
import math
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class CarverOperatorProperties():
|
||||
# OPERATOR-properties
|
||||
mode: bpy.props.EnumProperty(
|
||||
name = "Mode",
|
||||
items = (('DESTRUCTIVE', "Destructive", "Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0),
|
||||
('MODIFIER', "Modifier", "Cuts are stored as boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
default = 'DESTRUCTIVE',
|
||||
)
|
||||
depth: bpy.props.EnumProperty(
|
||||
name = "Depth",
|
||||
items = (('VIEW', "View", "Depth is automatically calculated from view orientation", 'VIEW_CAMERA_UNSELECTED', 0),
|
||||
('CURSOR', "Cursor", "Depth is derived from 3D cursors location", 'PIVOT_CURSOR', 1)),
|
||||
default = 'VIEW',
|
||||
)
|
||||
|
||||
|
||||
class CarverModifierProperties():
|
||||
# MODIFIER-properties
|
||||
solver: bpy.props.EnumProperty(
|
||||
name = "Solver",
|
||||
items = [('FLOAT', "Float", ""),
|
||||
('EXACT', "Exact", ""),
|
||||
('MANIFOLD', "Manifold", "")],
|
||||
default = 'FLOAT',
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifier",
|
||||
description = ("Boolean modifier will be placed first in modifier stack, above other modifier (if there are any).\n"
|
||||
"NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"),
|
||||
default = True,
|
||||
)
|
||||
|
||||
|
||||
class CarverCutterProperties():
|
||||
# CUTTER-properties
|
||||
hide: bpy.props.BoolProperty(
|
||||
name = "Hide Cutter",
|
||||
description = ("Hide cutter objects in the viewport after they're created."),
|
||||
default = True,
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent to Canvas",
|
||||
description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n"
|
||||
"If there is no active object in selection cutters parent might be chosen seemingly randomly"),
|
||||
default = True,
|
||||
)
|
||||
|
||||
auto_smooth: bpy.props.BoolProperty(
|
||||
name = "Shade Auto Smooth",
|
||||
description = ("Cutter object will be shaded smooth with sharp edges (above specified degrees) marked as sharp\n"
|
||||
"NOTE: This is a one time operator. 'Smooth by Angle' modifier will not be added on cutter"),
|
||||
default = True,
|
||||
)
|
||||
sharp_angle: bpy.props.FloatProperty(
|
||||
name = "Angle",
|
||||
description = "Maximum face angle for sharp edges",
|
||||
subtype = "ANGLE",
|
||||
min = 0, max = math.pi,
|
||||
default = 0.523599,
|
||||
)
|
||||
|
||||
|
||||
class CarverArrayProperties():
|
||||
# ARRAY-properties
|
||||
rows: bpy.props.IntProperty(
|
||||
name = "Rows",
|
||||
description = "Number of times shape is duplicated horizontally",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
rows_gap: bpy.props.FloatProperty(
|
||||
name = "Gap between rows (relative unit)",
|
||||
min = 0, soft_max = 250,
|
||||
default = 50,
|
||||
)
|
||||
rows_direction: bpy.props.EnumProperty(
|
||||
name = "Direction of Rows",
|
||||
items = (('LEFT', "Left", ""),
|
||||
('RIGHT', "Right", "")),
|
||||
default = 'RIGHT',
|
||||
)
|
||||
|
||||
columns: bpy.props.IntProperty(
|
||||
name = "Columns",
|
||||
description = "Number of times shape is duplicated vertically",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
columns_direction: bpy.props.EnumProperty(
|
||||
name = "Direction of Rows",
|
||||
items = (('UP', "Up", ""),
|
||||
('DOWN', "Down", "")),
|
||||
default = 'DOWN',
|
||||
)
|
||||
columns_gap: bpy.props.FloatProperty(
|
||||
name = "Gap between columns (relative unit)",
|
||||
min = 0, soft_max = 250,
|
||||
default = 50,
|
||||
)
|
||||
|
||||
|
||||
class CarverBevelProperties():
|
||||
# BEVEL-properties
|
||||
|
||||
use_bevel: bpy.props.BoolProperty(
|
||||
name = "Bevel Cutter",
|
||||
description = "Bevel each side edge of the cutter",
|
||||
default = False,
|
||||
)
|
||||
bevel_profile: bpy.props.EnumProperty(
|
||||
name = "Bevel Profile",
|
||||
items = (('CONVEX', "Convex", "Outside bevel (rounded corners)"),
|
||||
('CONCAVE', "Concave", "Inside bevel")),
|
||||
default = 'CONVEX',
|
||||
)
|
||||
bevel_segments: bpy.props.IntProperty(
|
||||
name = "Bevel Segments",
|
||||
description = "Segments for curved edge",
|
||||
min = 2, soft_max = 32,
|
||||
default = 8,
|
||||
)
|
||||
bevel_radius: bpy.props.FloatProperty(
|
||||
name = "Bevel Radius",
|
||||
description = "Amout of the bevel (in screen-space units)",
|
||||
min = 0.01, soft_max = 5,
|
||||
default = 1,
|
||||
)
|
||||
@@ -0,0 +1,149 @@
|
||||
import bpy
|
||||
from ... import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ /toolbar/ ------------------------------ ####
|
||||
|
||||
def carver_ui_common(context, layout, props):
|
||||
"""Common tool properties for all Carver tools"""
|
||||
|
||||
layout.prop(props, "mode", text="")
|
||||
layout.prop(props, "depth", text="")
|
||||
layout.prop(props, "solver", expand=True)
|
||||
|
||||
# Popovers
|
||||
layout.popover("TOPBAR_PT_carver_shape", text="Shape")
|
||||
layout.popover("TOPBAR_PT_carver_array", text="Array")
|
||||
layout.popover("TOPBAR_PT_carver_cutter", text="Cutter")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /popovers/ ------------------------------ ####
|
||||
|
||||
class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
bl_label = "Carver Shape"
|
||||
bl_idname = "TOPBAR_PT_carver_shape"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
|
||||
# Box
|
||||
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
|
||||
if tool.idname == "object.carve_circle":
|
||||
layout.prop(props, "subdivision", text="Vertices")
|
||||
layout.prop(props, "rotation")
|
||||
layout.prop(props, "aspect", expand=True)
|
||||
layout.prop(props, "origin", expand=True)
|
||||
|
||||
# bevel
|
||||
if tool.idname == 'object.carve_box':
|
||||
layout.separator()
|
||||
layout.prop(props, "use_bevel", text="Bevel")
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
if prefs.experimental:
|
||||
row.prop(props, "bevel_profile", text="Profile", expand=True)
|
||||
col.prop(props, "bevel_segments", text="Segments")
|
||||
col.prop(props, "bevel_radius", text="Radius")
|
||||
|
||||
if props.use_bevel == False:
|
||||
col.enabled = False
|
||||
|
||||
# Polyline
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
layout.prop(props, "closed")
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_array(bpy.types.Panel):
|
||||
bl_label = "Carver Array"
|
||||
bl_idname = "TOPBAR_PT_carver_array"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
|
||||
# Rows
|
||||
col = layout.column(align=True)
|
||||
col.prop(props, "rows")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "rows_direction", text="Direction", expand=True)
|
||||
col.prop(props, "rows_gap", text="Gap")
|
||||
|
||||
# Columns
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(props, "columns")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "columns_direction", text="Direction", expand=True)
|
||||
col.prop(props, "columns_gap", text="Gap")
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
bl_label = "Carver Cutter"
|
||||
bl_idname = "TOPBAR_PT_carver_cutter"
|
||||
bl_region_type = 'HEADER'
|
||||
bl_space_type = 'TOPBAR'
|
||||
bl_category = 'Tool'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
|
||||
# modifier_&_cutter
|
||||
col = layout.column()
|
||||
col.prop(props, "pin", text="Pin Modifier")
|
||||
if props.mode == 'MODIFIER':
|
||||
col.prop(props, "parent")
|
||||
col.prop(props, "hide")
|
||||
|
||||
# auto_smooth
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(props, "auto_smooth", text="Auto Smooth")
|
||||
col.prop(props, "sharp_angle")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
TOPBAR_PT_carver_shape,
|
||||
TOPBAR_PT_carver_array,
|
||||
TOPBAR_PT_carver_cutter,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -7,9 +7,9 @@ from .functions.list import list_canvas_cutters
|
||||
|
||||
def carve_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.carve", text="Box Carve").shape='BOX'
|
||||
layout.operator("object.carve", text="Circle Carve").shape='CIRCLE'
|
||||
layout.operator("object.carve", text="Polyline Carve").shape='POLYLINE'
|
||||
layout.operator("object.carve_box", text="Box Carve").shape='BOX'
|
||||
layout.operator("object.carve_box", text="Circle Carve").shape='CIRCLE'
|
||||
layout.operator("object.carve_polyline", text="Polyline Carve")
|
||||
|
||||
|
||||
def boolean_operators_menu(self, context):
|
||||
@@ -37,7 +37,7 @@ def boolean_extras_menu(self, context):
|
||||
col = layout.column(align=True)
|
||||
|
||||
if context.active_object:
|
||||
# canvas_operators
|
||||
# Canvas operators
|
||||
active_object = context.active_object
|
||||
if active_object.booleans.canvas == True and any(mod.name.startswith("boolean_") for mod in active_object.modifiers):
|
||||
col.separator()
|
||||
@@ -45,7 +45,7 @@ def boolean_extras_menu(self, context):
|
||||
col.operator("object.boolean_apply_all", text="Apply All Cutters")
|
||||
col.operator("object.boolean_remove_all", text="Remove All Cutters")
|
||||
|
||||
# cutter_operators
|
||||
# Cutter operators
|
||||
if active_object.booleans.cutter:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL'
|
||||
@@ -174,7 +174,7 @@ class VIEW3D_MT_carve(bpy.types.Menu):
|
||||
carve_menu(self, context)
|
||||
|
||||
|
||||
# Object > Menu
|
||||
# 3D Viewport (Object Mode) -> Object
|
||||
class VIEW3D_MT_boolean(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean"
|
||||
|
||||
Reference in New Issue
Block a user