2025-07-01
This commit is contained in:
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
|
||||
<http://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
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,11 @@
|
||||
# addon_common
|
||||
|
||||
This repo contains the CookieCutter Blender add-on framework.
|
||||
|
||||
## Example Add-on
|
||||
|
||||
As an example add-on, see the [ExtruCut](https://github.com/CGCookie/ExtruCut) project.
|
||||
|
||||
## resources
|
||||
|
||||
- Blender Conference 2018 workshop [slides](https://gfx.cse.taylor.edu/courses/bcon18/index.md.html?scale) and [presentation](https://www.youtube.com/watch?v=YSHdSNhMO1c)
|
||||
@@ -0,0 +1,62 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'bezier',
|
||||
'blender',
|
||||
'blender_preferences',
|
||||
'bmesh_render',
|
||||
'boundvar',
|
||||
'colors',
|
||||
'debug',
|
||||
'decorators',
|
||||
'drawing',
|
||||
'fontmanager',
|
||||
'fsm',
|
||||
'globals',
|
||||
'hasher',
|
||||
'irc',
|
||||
'logger',
|
||||
'markdown',
|
||||
'maths',
|
||||
'metaclasses',
|
||||
'parse',
|
||||
'profiler',
|
||||
'shaders',
|
||||
'ui_core',
|
||||
'ui_document',
|
||||
'ui_styling',
|
||||
'ui_core_utilities',
|
||||
'updater_core',
|
||||
'updater_ops',
|
||||
'useractions',
|
||||
'utils',
|
||||
]
|
||||
|
||||
|
||||
import bpy
|
||||
if bpy.app.version >= (3, 2, 0):
|
||||
# import the following only to populate the globals
|
||||
from . import debug as _
|
||||
from . import drawing as _
|
||||
from . import logger as _
|
||||
from . import profiler as _
|
||||
from . import ui_core as _
|
||||
@@ -0,0 +1,636 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import math
|
||||
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .maths import Point, Vec
|
||||
from .utils import iter_running_sum
|
||||
|
||||
|
||||
def compute_quadratic_weights(t):
|
||||
t0, t1 = t, (1-t)
|
||||
return (t1**2, 2*t0*t1, t0**2)
|
||||
|
||||
|
||||
def compute_cubic_weights(t):
|
||||
t0, t1 = t, (1-t)
|
||||
return (t1**3, 3*t0*t1**2, 3*t0**2*t1, t0**3)
|
||||
|
||||
|
||||
def interpolate_cubic(v0, v1, v2, v3, t):
|
||||
b0, b1, b2, b3 = compute_cubic_weights(t)
|
||||
return v0*b0 + v1*b1 + v2*b2 + v3*b3
|
||||
|
||||
|
||||
def compute_cubic_error(v0, v1, v2, v3, l_v, l_t):
|
||||
return math.sqrt(sum(
|
||||
(interpolate_cubic(v0, v1, v2, v3, t) - v)**2
|
||||
for v, t in zip(l_v, l_t)
|
||||
))
|
||||
|
||||
|
||||
def fit_cubicbezier(l_v, l_t):
|
||||
#########################################################
|
||||
# http://nbviewer.ipython.org/gist/anonymous/5688579
|
||||
|
||||
# make the summation functions for A (16 of them)
|
||||
A_fns = [
|
||||
lambda l_t: sum([2*t**0*(t-1)**6 for t in l_t]),
|
||||
lambda l_t: sum([-6*t**1*(t-1)**5 for t in l_t]),
|
||||
lambda l_t: sum([6*t**2*(t-1)**4 for t in l_t]),
|
||||
lambda l_t: sum([-2*t**3*(t-1)**3 for t in l_t]),
|
||||
|
||||
lambda l_t: sum([-6*t**1*(t-1)**5 for t in l_t]),
|
||||
lambda l_t: sum([18*t**2*(t-1)**4 for t in l_t]),
|
||||
lambda l_t: sum([-18*t**3*(t-1)**3 for t in l_t]),
|
||||
lambda l_t: sum([6*t**4*(t-1)**2 for t in l_t]),
|
||||
|
||||
lambda l_t: sum([6*t**2*(t-1)**4 for t in l_t]),
|
||||
lambda l_t: sum([-18*t**3*(t-1)**3 for t in l_t]),
|
||||
lambda l_t: sum([18*t**4*(t-1)**2 for t in l_t]),
|
||||
lambda l_t: sum([-6*t**5*(t-1)**1 for t in l_t]),
|
||||
|
||||
lambda l_t: sum([-2*t**3*(t-1)**3 for t in l_t]),
|
||||
lambda l_t: sum([6*t**4*(t-1)**2 for t in l_t]),
|
||||
lambda l_t: sum([-6*t**5*(t-1)**1 for t in l_t]),
|
||||
lambda l_t: sum([2*t**6*(t-1)**0 for t in l_t])
|
||||
]
|
||||
|
||||
# make the summation functions for b (4 of them)
|
||||
b_fns = [
|
||||
lambda l_t, l_v: sum(v * (-2 * (t**0) * ((t-1)**3))
|
||||
for t, v in zip(l_t, l_v)),
|
||||
lambda l_t, l_v: sum(v * (6 * (t**1) * ((t-1)**2))
|
||||
for t, v in zip(l_t, l_v)),
|
||||
lambda l_t, l_v: sum(v * (-6 * (t**2) * ((t-1)**1))
|
||||
for t, v in zip(l_t, l_v)),
|
||||
lambda l_t, l_v: sum(v * (2 * (t**3) * ((t-1)**0))
|
||||
for t, v in zip(l_t, l_v)),
|
||||
]
|
||||
|
||||
# compute the data we will put into matrix A
|
||||
A_values = [fn(l_t) for fn in A_fns]
|
||||
# fill the A matrix with data
|
||||
A_matrix = Matrix(tuple(zip(*[iter(A_values)]*4)))
|
||||
try:
|
||||
A_inv = A_matrix.inverted()
|
||||
except:
|
||||
return (float('inf'), l_v[0], l_v[0], l_v[0], l_v[0])
|
||||
|
||||
# compute the data we will put into the b vector
|
||||
b_values = [fn(l_t, l_v) for fn in b_fns]
|
||||
# fill the b vector with data
|
||||
b_vector = Vector(b_values)
|
||||
|
||||
# solve for the unknowns in vector x
|
||||
v0, v1, v2, v3 = A_inv @ b_vector
|
||||
|
||||
err = compute_cubic_error(v0, v1, v2, v3, l_v, l_t) #/ len(l_v)
|
||||
|
||||
return (err, v0, v1, v2, v3)
|
||||
|
||||
|
||||
def fit_cubicbezier_spline(
|
||||
l_co, error_scale, depth=0,
|
||||
t0=0, t3=-1, allow_split=True, force_split=False,
|
||||
min_count_split=15, max_depth_split=4,
|
||||
):
|
||||
'''
|
||||
fits cubic bezier to given points
|
||||
returns list of tuples of (t0,t3,p0,p1,p2,p3)
|
||||
that best fits the given points l_co
|
||||
where t0 and t3 are the passed-in t0 and t3
|
||||
and p0,p1,p2,p3 are the control points of bezier
|
||||
'''
|
||||
count = len(l_co)
|
||||
if t3 == -1:
|
||||
t3 = count-1
|
||||
assert count > 2, "Need at least 2 points to fit cubic bezier"
|
||||
if count == 2:
|
||||
# special case: line
|
||||
p0, p3 = l_co[0], l_co[-1]
|
||||
diff = p3 - p0
|
||||
return [(t0, t3, p0, p0+diff*0.33, p0+diff*0.66, p3)]
|
||||
if count == 3:
|
||||
new_co = [
|
||||
l_co[0],
|
||||
Point.average(l_co[:2]),
|
||||
l_co[1],
|
||||
Point.average(l_co[1:]),
|
||||
l_co[2]
|
||||
]
|
||||
return fit_cubicbezier_spline(
|
||||
new_co, error_scale,
|
||||
depth=depth,
|
||||
t0=t0, t3=t3,
|
||||
allow_split=allow_split, force_split=force_split
|
||||
)
|
||||
l_d = [0] + [(v0-v1).length for v0, v1 in zip(l_co[:-1], l_co[1:])]
|
||||
l_ad = [s for d, s in iter_running_sum(l_d)]
|
||||
dist = sum(l_d)
|
||||
if dist <= 0:
|
||||
# print(spc + 'fit_cubicbezier_spline: returning []')
|
||||
return [] # [(t0,t3,l_co[0],l_co[0],l_co[0],l_co[0])]
|
||||
l_t = [ad/dist for ad in l_ad]
|
||||
|
||||
ex, x0, x1, x2, x3 = fit_cubicbezier([co[0] for co in l_co], l_t)
|
||||
ey, y0, y1, y2, y3 = fit_cubicbezier([co[1] for co in l_co], l_t)
|
||||
ez, z0, z1, z2, z3 = fit_cubicbezier([co[2] for co in l_co], l_t)
|
||||
tot_error = ex+ey+ez
|
||||
#print(f'error={tot_error} max={error_scale} force={force_split} allow={allow_split}') #, l=4)
|
||||
|
||||
if not force_split:
|
||||
do_not_split = tot_error < error_scale
|
||||
do_not_split |= depth == max_depth_split
|
||||
do_not_split |= len(l_co) <= min_count_split
|
||||
do_not_split |= not allow_split
|
||||
if do_not_split:
|
||||
p0, p1 = Point((x0, y0, z0)), Point((x1, y1, z1))
|
||||
p2, p3 = Point((x2, y2, z2)), Point((x3, y3, z3))
|
||||
return [(t0, t3, p0, p1, p2, p3)]
|
||||
|
||||
# too much error in fit. split sequence in two, and fit each sub-sequence
|
||||
|
||||
# find a good split point
|
||||
ind_split = -1
|
||||
mindot = 1.0
|
||||
for ind in range(5, len(l_co)-5):
|
||||
if l_t[ind] < 0.4:
|
||||
continue
|
||||
if l_t[ind] > 0.6:
|
||||
break
|
||||
# if l_ad[ind] < 0.1: continue
|
||||
# if l_ad[ind] > dist-0.1: break
|
||||
|
||||
v0 = l_co[ind-4]
|
||||
v1 = l_co[ind+0]
|
||||
v2 = l_co[ind+4]
|
||||
d0 = (v1-v0).normalized()
|
||||
d1 = (v2-v1).normalized()
|
||||
dot01 = d0.dot(d1)
|
||||
if ind_split == -1 or dot01 < mindot:
|
||||
ind_split = ind
|
||||
mindot = dot01
|
||||
|
||||
if ind_split == -1:
|
||||
# did not find a good splitting point!
|
||||
p0, p1, p2, p3 = Point((x0, y0, z0)), Point(
|
||||
(x1, y1, z1)), Point((x2, y2, z2)), Point((x3, y3, z3))
|
||||
#p0,p3 = Point(l_co[0]),Point(l_co[-1])
|
||||
return [(t0, t3, p0, p1, p2, p3)]
|
||||
|
||||
#print(spc + 'splitting at %d' % ind_split)
|
||||
|
||||
l_co0, l_co1 = l_co[:ind_split+1], l_co[ind_split:] # share split point
|
||||
tsplit = ind_split # / (len(l_co)-1)
|
||||
bezier0 = fit_cubicbezier_spline(
|
||||
l_co0, error_scale, depth=depth+1, t0=t0, t3=tsplit)
|
||||
bezier1 = fit_cubicbezier_spline(
|
||||
l_co1, error_scale, depth=depth+1, t0=tsplit, t3=t3)
|
||||
return bezier0 + bezier1
|
||||
|
||||
|
||||
class CubicBezier:
|
||||
split_default = 100
|
||||
segments_default = 100
|
||||
|
||||
@staticmethod
|
||||
def create_from_points(pts_list):
|
||||
'''
|
||||
Estimates best spline to fit given points
|
||||
'''
|
||||
count = len(pts_list)
|
||||
if count == 0:
|
||||
assert False
|
||||
if count == 1:
|
||||
assert False
|
||||
if count == 2:
|
||||
p0, p3 = pts_list
|
||||
diff = p3-p0
|
||||
p1, p2 = p0+diff*0.33, p0+diff*0.66
|
||||
return CubicBezier(p0, p1, p2, p3)
|
||||
if count == 3:
|
||||
p0, p03, p3 = pts_list
|
||||
d003, d303 = (p03-p0), (p03-p3)
|
||||
p1, p2 = p0+d003*0.5, p3+d303*0.5
|
||||
return CubicBezier(p0, p1, p2, p3)
|
||||
l_d = [0] + [(p0-p1).length for p0,
|
||||
p1 in zip(pts_list[:-1], pts_list[1:])]
|
||||
l_ad = [s for d, s in iter_running_sum(l_d)]
|
||||
dist = sum(l_d)
|
||||
if dist <= 0:
|
||||
p0 = pts_list[0]
|
||||
return CubicBezier(p0, p0, p0, p0)
|
||||
l_t = [ad/dist for ad in l_ad]
|
||||
|
||||
ex, x0, x1, x2, x3 = fit_cubicbezier([pt[0] for pt in pts_list], l_t)
|
||||
ey, y0, y1, y2, y3 = fit_cubicbezier([pt[1] for pt in pts_list], l_t)
|
||||
ez, z0, z1, z2, z3 = fit_cubicbezier([pt[2] for pt in pts_list], l_t)
|
||||
p0 = Point((x0, y0, z0))
|
||||
p1 = Point((x1, y1, z1))
|
||||
p2 = Point((x2, y2, z2))
|
||||
p3 = Point((x3, y3, z3))
|
||||
return CubicBezier(p0, p1, p2, p3)
|
||||
|
||||
def __init__(self, p0, p1, p2, p3):
|
||||
self.p0, self.p1, self.p2, self.p3 = p0, p1, p2, p3
|
||||
self.tessellation = []
|
||||
|
||||
def __iter__(self): return iter([self.p0, self.p1, self.p2, self.p3])
|
||||
|
||||
def points(self): return (self.p0, self.p1, self.p2, self.p3)
|
||||
|
||||
def copy(self):
|
||||
''' shallow copy '''
|
||||
return CubicBezier(self.p0, self.p1, self.p2, self.p3)
|
||||
|
||||
def eval(self, t):
|
||||
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
|
||||
b0, b1, b2, b3 = compute_cubic_weights(t)
|
||||
return Point.weighted_average([
|
||||
(b0, p0), (b1, p1), (b2, p2), (b3, p3)
|
||||
])
|
||||
|
||||
def eval_derivative(self, t):
|
||||
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
|
||||
q0, q1, q2 = 3*(p1-p0), 3*(p2-p1), 3*(p3-p2)
|
||||
b0, b1, b2 = compute_quadratic_weights(t)
|
||||
return q0*b0 + q1*b1 + q2*b2
|
||||
|
||||
def subdivide(self, iters=1):
|
||||
if iters == 0:
|
||||
return [self]
|
||||
# de casteljau subdivide
|
||||
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
|
||||
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
|
||||
r0, r1 = (q0+q1)/2, (q1+q2)/2
|
||||
s = (r0+r1)/2
|
||||
cb0, cb1 = CubicBezier(p0, q0, r0, s), CubicBezier(s, r1, q2, p3)
|
||||
if iters == 1:
|
||||
return [cb0, cb1]
|
||||
return cb0.subdivide(iters=iters-1) + cb1.subdivide(iters=iters-1)
|
||||
|
||||
def compute_linearity(self, fn_dist):
|
||||
'''
|
||||
Estimating measure of linearity as ratio of distances
|
||||
of curve mid-point and mid-point of end control points
|
||||
over half the distance between end control points
|
||||
p1 _
|
||||
/ ﹨
|
||||
| ﹨
|
||||
p0 * ﹨ * p3
|
||||
﹨_/
|
||||
p2
|
||||
'''
|
||||
p0, p1, p2, p3 = Vector(self.p0), Vector(
|
||||
self.p1), Vector(self.p2), Vector(self.p3)
|
||||
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
|
||||
r0, r1 = (q0+q1)/2, (q1+q2)/2
|
||||
s = (r0+r1)/2
|
||||
m = (p0+p3)/2
|
||||
d03 = fn_dist(p0, p3)
|
||||
dsm = fn_dist(s, m)
|
||||
return 2 * dsm / d03
|
||||
|
||||
def subdivide_linesegments(self, fn_dist, max_linearity=None):
|
||||
if self.compute_linearity(fn_dist) < (max_linearity or 0.1):
|
||||
return [self]
|
||||
# de casteljau subdivide:
|
||||
p0, p1, p2, p3 = Vector(self.p0), Vector(
|
||||
self.p1), Vector(self.p2), Vector(self.p3)
|
||||
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
|
||||
r0, r1 = (q0+q1)/2, (q1+q2)/2
|
||||
s = (r0+r1)/2
|
||||
cbs = CubicBezier(p0, q0, r0, s), CubicBezier(s, r1, q2, p3)
|
||||
segs0, segs1 = [cb.subdivide_linesegments(
|
||||
fn_dist, max_linearity=max_linearity) for cb in cbs]
|
||||
return segs0 + segs1
|
||||
|
||||
def length(self, fn_dist, max_linearity=None):
|
||||
l = self.subdivide_linesegments(fn_dist, max_linearity=max_linearity)
|
||||
return sum(fn_dist(cb.p0, cb.p3) for cb in l)
|
||||
|
||||
def approximate_length_uniform(self, fn_dist, split=None):
|
||||
split = split or self.split_default
|
||||
p = self.p0
|
||||
d = 0
|
||||
for i in range(split):
|
||||
q = self.eval((i+1) / split)
|
||||
d += fn_dist(p, q)
|
||||
p = q
|
||||
return d
|
||||
|
||||
def approximate_t_at_interval_uniform(self, interval, fn_dist, split=None):
|
||||
split = split or self.split_default
|
||||
p = self.p0
|
||||
d = 0
|
||||
for i in range(split):
|
||||
percent = (i+1) / split
|
||||
q = self.eval(percent)
|
||||
d += fn_dist(p, q)
|
||||
if interval <= d:
|
||||
return percent
|
||||
p = q
|
||||
return 1
|
||||
|
||||
def approximate_ts_at_intervals_uniform(
|
||||
self, intervals, fn_dist, split=None
|
||||
):
|
||||
a = self.approximate_t_at_interval_uniform
|
||||
|
||||
def approx(i): return a(i, fn_dist, split=None)
|
||||
return [approx(interval) for interval in intervals]
|
||||
|
||||
def get_tessellate_uniform(self, fn_dist, split=None):
|
||||
split = split or self.split_default
|
||||
ts = [i/(split-1) for i in range(split)]
|
||||
ps = [self.eval(t) for t in ts]
|
||||
ds = [0] + [fn_dist(p, q) for p, q in zip(ps[:-1], ps[1:])]
|
||||
return [(t, p, d) for t, p, d in zip(ts, ps, ds)]
|
||||
|
||||
def tessellate_uniform_points(self, segments=None):
|
||||
segments = segments or self.segments_default
|
||||
ts = [i/(segments-1) for i in range(segments)]
|
||||
ps = [self.eval(t) for t in ts]
|
||||
return ps
|
||||
|
||||
#########################################
|
||||
# #
|
||||
# the following code **requires** that #
|
||||
# self.tessellate_uniform() is called #
|
||||
# beforehand! #
|
||||
# #
|
||||
#########################################
|
||||
|
||||
def tessellate_uniform(self, fn_dist, split=None):
|
||||
self.tessellation = self.get_tessellate_uniform(fn_dist, split=split)
|
||||
|
||||
def approximate_t_at_point_tessellation(self, point, fn_dist):
|
||||
bd, bt = None, None
|
||||
for t, q, _ in self.tessellation:
|
||||
d = fn_dist(point, q)
|
||||
if bd is None or d < bd:
|
||||
bd, bt = d, t
|
||||
return bt
|
||||
|
||||
def approximate_totlength_tessellation(self):
|
||||
return sum(self.approximate_lengths_tessellation())
|
||||
|
||||
def approximate_lengths_tessellation(self):
|
||||
return [d for _, _, d in self.tessellation]
|
||||
|
||||
|
||||
class CubicBezierSpline:
|
||||
|
||||
@staticmethod
|
||||
def create_from_points(pts_list, max_error, **kwargs):
|
||||
'''
|
||||
Estimates best spline to fit given points
|
||||
'''
|
||||
cbs = []
|
||||
inds = []
|
||||
for pts in pts_list:
|
||||
cbs_pts = fit_cubicbezier_spline(pts, max_error, **kwargs)
|
||||
cbs += [CubicBezier(p0, p1, p2, p3) for _, _, p0, p1, p2, p3 in cbs_pts]
|
||||
inds += [(ind0, ind1) for ind0, ind1, _, _, _, _ in cbs_pts]
|
||||
return CubicBezierSpline(cbs=cbs, inds=inds)
|
||||
|
||||
def __init__(self, cbs=None, inds=None):
|
||||
if cbs is None:
|
||||
cbs = []
|
||||
if inds is None:
|
||||
inds = []
|
||||
if type(cbs) is CubicBezierSpline:
|
||||
cbs = [cb.copy() for cb in cbs.cbs]
|
||||
assert type(cbs) is list, "expected list"
|
||||
self.cbs = cbs
|
||||
self.inds = inds
|
||||
self.tessellation = []
|
||||
|
||||
def copy(self):
|
||||
return CubicBezierSpline(
|
||||
cbs=[cb.copy() for cb in self.cbs],
|
||||
inds=list(self.inds)
|
||||
)
|
||||
|
||||
def __add__(self, other):
|
||||
t = type(other)
|
||||
if t is CubicBezierSpline:
|
||||
return CubicBezierSpline(
|
||||
self.cbs + other.cbs,
|
||||
self.inds + other.inds
|
||||
)
|
||||
if t is CubicBezier:
|
||||
return CubicBezierSpline(self.cbs + [other])
|
||||
if t is list:
|
||||
return CubicBezierSpline(self.cbs + other)
|
||||
assert False, "unhandled type: %s (%s)" % (str(other), str(t))
|
||||
|
||||
def __iadd__(self, other):
|
||||
t = type(other)
|
||||
if t is CubicBezierSpline:
|
||||
self.cbs += other.cbs
|
||||
self.inds += other.inds
|
||||
elif t is CubicBezier:
|
||||
self.cbs += [other]
|
||||
self.inds = []
|
||||
elif t is list:
|
||||
self.cbs += other
|
||||
self.inds = []
|
||||
else:
|
||||
assert False, "unhandled type: %s (%s)" % (str(other), str(t))
|
||||
|
||||
def __len__(self): return len(self.cbs)
|
||||
|
||||
def __iter__(self): return self.cbs.__iter__()
|
||||
|
||||
def __getitem__(self, idx): return self.cbs[idx]
|
||||
|
||||
def eval(self, t):
|
||||
if t < 0.0:
|
||||
t = 0
|
||||
idx = 0
|
||||
elif t >= len(self):
|
||||
t = 1
|
||||
idx = len(self)-1
|
||||
else:
|
||||
idx = int(t)
|
||||
t = t - idx
|
||||
return self.cbs[idx].eval(t)
|
||||
|
||||
def eval_derivative(self, t):
|
||||
if t < 0.0:
|
||||
t = 0
|
||||
idx = 0
|
||||
elif t >= len(self):
|
||||
t = 1
|
||||
idx = len(self)-1
|
||||
else:
|
||||
idx = int(t)
|
||||
t = t - idx
|
||||
return self.cbs[idx].eval_derivative(t)
|
||||
|
||||
def approximate_totlength_uniform(self, fn_dist, split=None):
|
||||
return sum(self.approximate_lengths_uniform(fn_dist, split=split))
|
||||
|
||||
def approximate_lengths_uniform(self, fn_dist, split=None):
|
||||
return [
|
||||
cb.approximate_length_uniform(fn_dist, split=split)
|
||||
for cb in self.cbs
|
||||
]
|
||||
|
||||
def approximate_ts_at_intervals_uniform(
|
||||
self, intervals, fn_dist, split=None
|
||||
):
|
||||
lengths = self.approximate_lengths_uniform(fn_dist, split=split)
|
||||
totlength = sum(lengths)
|
||||
ts = []
|
||||
for interval in intervals:
|
||||
if interval < 0:
|
||||
ts.append(0)
|
||||
continue
|
||||
if interval >= totlength:
|
||||
ts.append(len(self.cbs))
|
||||
continue
|
||||
for i, length in enumerate(lengths):
|
||||
if interval <= length:
|
||||
t = self.cbs[i].approximate_t_at_interval_uniform(
|
||||
interval, fn_dist, split=split)
|
||||
ts.append(i + t)
|
||||
break
|
||||
interval -= length
|
||||
else:
|
||||
assert False
|
||||
return ts
|
||||
|
||||
def subdivide_linesegments(self, fn_dist, max_linearity=None):
|
||||
return CubicBezierSpline(cbi
|
||||
for cb in self.cbs
|
||||
for cbi in cb.subdivide_linesegments(
|
||||
fn_dist,
|
||||
max_linearity=max_linearity
|
||||
))
|
||||
|
||||
#########################################
|
||||
# #
|
||||
# the following code **requires** that #
|
||||
# self.tessellate_uniform() is called #
|
||||
# beforehand! #
|
||||
# #
|
||||
#########################################
|
||||
|
||||
def tessellate_uniform(self, fn_dist, split=None):
|
||||
self.tessellation.clear()
|
||||
for i, cb in enumerate(self.cbs):
|
||||
cb_tess = cb.get_tessellate_uniform(fn_dist, split=split)
|
||||
self.tessellation.append(cb_tess)
|
||||
|
||||
def approximate_totlength_tessellation(self):
|
||||
return sum(self.approximate_lengths_tessellation())
|
||||
|
||||
def approximate_lengths_tessellation(self):
|
||||
return [sum(d for _, _, d in cb_tess) for cb_tess in self.tessellation]
|
||||
|
||||
def approximate_ts_at_intervals_tessellation(self, intervals):
|
||||
lengths = self.approximate_lengths_tessellation()
|
||||
totlength = sum(lengths)
|
||||
ts = []
|
||||
for interval in intervals:
|
||||
if interval < 0:
|
||||
ts.append(0)
|
||||
continue
|
||||
if interval >= totlength:
|
||||
ts.append(len(self.cbs))
|
||||
continue
|
||||
for i, length in enumerate(lengths):
|
||||
if interval > length:
|
||||
interval -= length
|
||||
continue
|
||||
cb_tess = self.tessellation[i]
|
||||
for t, p, d in cb_tess:
|
||||
if interval > d:
|
||||
interval -= d
|
||||
continue
|
||||
ts.append(i+t)
|
||||
break
|
||||
else:
|
||||
assert False
|
||||
break
|
||||
else:
|
||||
assert False
|
||||
return ts
|
||||
|
||||
def approximate_ts_at_points_tessellation(self, points, fn_dist):
|
||||
ts = []
|
||||
for p in points:
|
||||
bd, bt = None, None
|
||||
for i, cb_tess in enumerate(self.tessellation):
|
||||
for t, q, _ in cb_tess:
|
||||
d = fn_dist(p, q)
|
||||
if bd is None or d < bd:
|
||||
bd, bt = d, i+t
|
||||
ts.append(bt)
|
||||
return ts
|
||||
|
||||
def approximate_t_at_point_tessellation(self, point, fn_dist):
|
||||
bd, bt = None, None
|
||||
for i, cb_tess in enumerate(self.tessellation):
|
||||
for t, q, _ in cb_tess:
|
||||
d = fn_dist(point, q)
|
||||
if bd is None or d < bd:
|
||||
bd, bt = d, i+t
|
||||
return bt
|
||||
|
||||
|
||||
class GenVector(list):
|
||||
'''
|
||||
Generalized Vector, allows for some simple ordered items to be linearly combined
|
||||
which is useful for interpolating arbitrary points of Bezier Spline.
|
||||
'''
|
||||
|
||||
def __mul__(self, scalar: float): # ->GVector:
|
||||
for idx in range(len(self)):
|
||||
self[idx] *= scalar
|
||||
return self
|
||||
|
||||
def __rmul__(self, scalar: float): # ->GVector:
|
||||
return self.__mul__(scalar)
|
||||
|
||||
def __add__(self, other: list): # ->GVector:
|
||||
for idx in range(len(self)):
|
||||
self[idx] += other[idx]
|
||||
return self
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# run tests
|
||||
|
||||
print('-'*50)
|
||||
l = GenVector([Vector((1, 2, 3)), 23])
|
||||
print(l)
|
||||
print(l * 2)
|
||||
print(4 * l)
|
||||
|
||||
l2 = GenVector([Vector((0, 0, 1)), 10])
|
||||
print(l + l2)
|
||||
print(2 * l + l2 * 4)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
|
||||
from .globals import Globals
|
||||
|
||||
class Cursors:
|
||||
# https://docs.blender.org/api/current/bpy.types.Window.html#bpy.types.Window.cursor_set
|
||||
_cursors = {
|
||||
|
||||
# blender cursors
|
||||
'DEFAULT': 'DEFAULT',
|
||||
'NONE': 'NONE',
|
||||
'WAIT': 'WAIT',
|
||||
'CROSSHAIR': 'CROSSHAIR',
|
||||
'MOVE_X': 'MOVE_X',
|
||||
'MOVE_Y': 'MOVE_Y',
|
||||
'KNIFE': 'KNIFE',
|
||||
'TEXT': 'TEXT',
|
||||
'PAINT_BRUSH': 'PAINT_BRUSH',
|
||||
'HAND': 'HAND',
|
||||
'SCROLL_X': 'SCROLL_X',
|
||||
'SCROLL_Y': 'SCROLL_Y',
|
||||
'EYEDROPPER': 'EYEDROPPER',
|
||||
|
||||
# lower case version of blender cursors
|
||||
'default': 'DEFAULT',
|
||||
'none': 'NONE',
|
||||
'wait': 'WAIT',
|
||||
'crosshair': 'CROSSHAIR',
|
||||
'move_x': 'MOVE_X',
|
||||
'move_y': 'MOVE_Y',
|
||||
'knife': 'KNIFE',
|
||||
'text': 'TEXT',
|
||||
'paint_brush': 'PAINT_BRUSH',
|
||||
'hand': 'HAND',
|
||||
'scroll_x': 'SCROLL_X',
|
||||
'scroll_y': 'SCROLL_Y',
|
||||
'eyedropper': 'EYEDROPPER',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def __getattr__(cursor):
|
||||
assert cursor in Cursors._cursors
|
||||
return Cursors._cursors.get(cursor, 'DEFAULT')
|
||||
|
||||
@staticmethod
|
||||
def set(cursor):
|
||||
# print('Cursors.set', cursor)
|
||||
cursor = Cursors._cursors.get(cursor, 'DEFAULT')
|
||||
for wm in bpy.data.window_managers:
|
||||
for win in wm.windows:
|
||||
win.cursor_modal_set(cursor)
|
||||
|
||||
@staticmethod
|
||||
def restore():
|
||||
for wm in bpy.data.window_managers:
|
||||
for win in wm.windows:
|
||||
win.cursor_modal_restore()
|
||||
|
||||
@property
|
||||
@staticmethod
|
||||
def cursor(): return 'DEFAULT' # TODO: how to get??
|
||||
@cursor.setter
|
||||
@staticmethod
|
||||
def cursor(cursor): Cursors.set(cursor)
|
||||
|
||||
@staticmethod
|
||||
def warp(x, y): bpy.context.window.cursor_warp(x, y)
|
||||
|
||||
Globals.set(Cursors())
|
||||
@@ -0,0 +1,61 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def get_preferences(ctx=None):
|
||||
return (ctx if ctx else bpy.context).preferences
|
||||
|
||||
|
||||
def mouse_doubleclick():
|
||||
# time/delay (in seconds) for a double click
|
||||
return bpy.context.preferences.inputs.mouse_double_click_time / 1000
|
||||
|
||||
def mouse_drag():
|
||||
# number of pixels to drag before tweak/drag event is triggered
|
||||
return bpy.context.preferences.inputs.drag_threshold_mouse
|
||||
|
||||
def mouse_move():
|
||||
# number of pixels to move before the cursor is considered to have moved
|
||||
# (used for cycling selected items on successive clicks)
|
||||
return bpy.context.preferences.inputs.move_threshold
|
||||
|
||||
def mouse_select():
|
||||
# returns 'LEFT' if LMB is used for selection or 'RIGHT' if RMB is used for selection
|
||||
|
||||
user_keyconfigs = bpy.context.window_manager.keyconfigs.user
|
||||
map_select_type = {'LEFTMOUSE': 'LEFT', 'RIGHTMOUSE': 'RIGHT'}
|
||||
|
||||
try:
|
||||
select_type = user_keyconfigs.keymaps['3D View'].keymap_items['view3d.select'].type
|
||||
return map_select_type[select_type]
|
||||
except Exception as e:
|
||||
if hasattr(mouse_select, 'reported'): return # already reported
|
||||
mouse_select.reported = True
|
||||
print('Addon Common: Exception caught in mouse_select')
|
||||
print('NOTE: only reporting this once')
|
||||
print(f'Exception: {e}')
|
||||
|
||||
return 'LEFT' # fallback to 'LEFT'
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
notes: something is really wrong here to have such poor performance
|
||||
|
||||
Below are some related, interesting links
|
||||
|
||||
- https://machinesdontcare.wordpress.com/2008/02/02/glsl-discard-z-fighting-supersampling/
|
||||
- https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/BestPracticesforShaders/BestPracticesforShaders.html
|
||||
- https://stackoverflow.com/questions/16415037/opengl-core-profile-incredible-slowdown-on-os-x
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import ctypes
|
||||
import random
|
||||
import traceback
|
||||
|
||||
import gpu
|
||||
import bpy
|
||||
from bpy_extras.view3d_utils import region_2d_to_origin_3d
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from mathutils.bvhtree import BVHTree
|
||||
|
||||
from . import gpustate
|
||||
from .debug import dprint
|
||||
from .decorators import blender_version_wrapper, add_cache, only_in_blender_version
|
||||
from .drawing import Drawing
|
||||
from .maths import (Point, Direction, Frame, XForm, invert_matrix, matrix_normal)
|
||||
from .profiler import profiler
|
||||
from .utils import shorten_floats
|
||||
|
||||
|
||||
|
||||
|
||||
def glSetDefaultOptions():
|
||||
gpustate.blend('ALPHA')
|
||||
gpustate.depth_test('LESS_EQUAL')
|
||||
|
||||
|
||||
def glSetMirror(symmetry=None, view=None, effect=0.0, frame: Frame=None):
|
||||
mirroring = (0, 0, 0)
|
||||
if symmetry and frame:
|
||||
mx = 1.0 if 'x' in symmetry else 0.0
|
||||
my = 1.0 if 'y' in symmetry else 0.0
|
||||
mz = 1.0 if 'z' in symmetry else 0.0
|
||||
mirroring = (mx, my, mz)
|
||||
bmeshShader.assign('mirror_o', frame.o)
|
||||
bmeshShader.assign('mirror_x', frame.x)
|
||||
bmeshShader.assign('mirror_y', frame.y)
|
||||
bmeshShader.assign('mirror_z', frame.z)
|
||||
bmeshShader.assign('mirror_view', {'Edge': 1, 'Face': 2}.get(view, 0))
|
||||
bmeshShader.assign('mirror_effect', effect)
|
||||
bmeshShader.assign('mirroring', mirroring)
|
||||
|
||||
def triangulateFace(verts):
|
||||
l = len(verts)
|
||||
if l < 3: return
|
||||
if l == 3:
|
||||
yield verts
|
||||
return
|
||||
if l == 4:
|
||||
v0,v1,v2,v3 = verts
|
||||
yield (v0,v1,v2)
|
||||
yield (v0,v2,v3)
|
||||
return
|
||||
iv = iter(verts)
|
||||
v0, v2 = next(iv), next(iv)
|
||||
for v3 in iv:
|
||||
v1, v2 = v2, v3
|
||||
yield (v0, v1, v2)
|
||||
|
||||
#############################################################################################################
|
||||
#############################################################################################################
|
||||
#############################################################################################################
|
||||
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
if not bpy.app.background:
|
||||
Drawing.glCheckError(f'Pre-compile check: bmesh render shader')
|
||||
verts_vs, verts_fs = gpustate.shader_parse_file('bmesh_render_verts.glsl', includeVersion=False)
|
||||
verts_shader, verts_ubos = gpustate.gpu_shader('bmesh render: verts', verts_vs, verts_fs)
|
||||
edges_vs, edges_fs = gpustate.shader_parse_file('bmesh_render_edges.glsl', includeVersion=False)
|
||||
edges_shader, edges_ubos = gpustate.gpu_shader('bmesh render: edges', edges_vs, edges_fs)
|
||||
faces_vs, faces_fs = gpustate.shader_parse_file('bmesh_render_faces.glsl', includeVersion=False)
|
||||
faces_shader, faces_ubos = gpustate.gpu_shader('bmesh render: faces', faces_vs, faces_fs)
|
||||
Drawing.glCheckError(f'Compiled bmesh render shader')
|
||||
|
||||
|
||||
class BufferedRender_Batch:
|
||||
_quarantine = {}
|
||||
|
||||
POINTS = 1
|
||||
LINES = 2
|
||||
TRIANGLES = 3
|
||||
|
||||
def __init__(self, drawtype):
|
||||
global faces_shader, edges_shader, verts_shader
|
||||
self.count = 0
|
||||
self.drawtype = drawtype
|
||||
self.shader, self.shader_ubos, self.shader_type, self.drawtype_name, self.gl_count, self.options_prefix = {
|
||||
self.POINTS: (verts_shader, verts_ubos, 'POINTS', 'points', 1, 'point'),
|
||||
self.LINES: (edges_shader, edges_ubos, 'LINES', 'lines', 2, 'line'),
|
||||
self.TRIANGLES: (faces_shader, faces_ubos, 'TRIS', 'triangles', 3, 'poly'),
|
||||
}[self.drawtype]
|
||||
self.batch = None
|
||||
self._quarantine.setdefault(self.shader, set())
|
||||
|
||||
def buffer(self, pos, norm, sel, warn, pin, seam):
|
||||
if self.shader == None: return
|
||||
if self.shader_type == 'POINTS':
|
||||
data = {
|
||||
# repeat each value 6 times
|
||||
'vert_pos': [p for p in pos for __ in range(6)],
|
||||
'vert_norm': [n for n in norm for __ in range(6)],
|
||||
'selected': [s for s in sel for __ in range(6)],
|
||||
'warning': [w for w in warn for __ in range(6)],
|
||||
'pinned': [p for p in pin for __ in range(6)],
|
||||
'seam': [p for p in seam for __ in range(6)],
|
||||
'vert_offset': [o for _ in pos for o in [(0,0), (1,0), (0,1), (0,1), (1,0), (1,1)]],
|
||||
}
|
||||
elif self.shader_type == 'LINES':
|
||||
data = {
|
||||
# repeat each value 6 times
|
||||
'vert_pos0': [p0 for p0 in pos [0::2] for __ in range(6)],
|
||||
'vert_pos1': [p1 for p1 in pos [1::2] for __ in range(6)],
|
||||
'vert_norm': [n for n in norm[0::2] for __ in range(6)],
|
||||
'selected': [s for s in sel [0::2] for __ in range(6)],
|
||||
'warning': [w for w in warn[0::2] for __ in range(6)],
|
||||
'pinned': [p for p in pin [0::2] for __ in range(6)],
|
||||
'seam': [s for s in seam[0::2] for __ in range(6)],
|
||||
'vert_offset': [o for _ in pos[0::2] for o in [(0,0), (0,1), (1,1), (0,0), (1,1), (1,0)]],
|
||||
}
|
||||
elif self.shader_type == 'TRIS':
|
||||
data = {
|
||||
'vert_pos': pos,
|
||||
'vert_norm': norm,
|
||||
'selected': sel,
|
||||
'pinned': pin,
|
||||
# 'seam': seam,
|
||||
}
|
||||
else: assert False, f'BufferedRender_Batch.buffer: Unhandled type: {self.shader_type}'
|
||||
self.batch = batch_for_shader(self.shader, 'TRIS', data)
|
||||
self.count = len(pos)
|
||||
|
||||
def set_options(self, prefix, opts):
|
||||
if not opts: return
|
||||
|
||||
prefix = f'{prefix} ' if prefix else ''
|
||||
|
||||
def set_if_set(opt, cb):
|
||||
opt = f'{prefix}{opt}'
|
||||
if opt not in opts: return
|
||||
cb(opts[opt])
|
||||
Drawing.glCheckError(f'setting {opt} to {opts[opt]}')
|
||||
|
||||
Drawing.glCheckError('BufferedRender_Batch.set_options: start')
|
||||
dpi_mult = opts.get('dpi mult', 1.0)
|
||||
set_if_set('color', lambda v: self.set_shader_option('color_normal', v))
|
||||
set_if_set('color selected', lambda v: self.set_shader_option('color_selected', v))
|
||||
set_if_set('color warning', lambda v: self.set_shader_option('color_warning', v))
|
||||
set_if_set('color pinned', lambda v: self.set_shader_option('color_pinned', v))
|
||||
set_if_set('color seam', lambda v: self.set_shader_option('color_seam', v))
|
||||
set_if_set('hidden', lambda v: self.set_shader_option('hidden', (v, 0, 0, 0)))
|
||||
set_if_set('offset', lambda v: self.set_shader_option('offset', (v, 0, 0, 0)))
|
||||
set_if_set('dotoffset', lambda v: self.set_shader_option('dotoffset', (v, 0, 0, 0)))
|
||||
if self.shader_type == 'POINTS':
|
||||
set_if_set('size', lambda v: self.set_shader_option('radius', (v*dpi_mult, 0, 0, 0)))
|
||||
elif self.shader_type == 'LINES':
|
||||
set_if_set('width', lambda v: self.set_shader_option('radius', (v*dpi_mult, 2*dpi_mult, 0, 0)))
|
||||
|
||||
def _draw(self, sx, sy, sz):
|
||||
self.set_shader_option('vert_scale', (sx, sy, sz, 0))
|
||||
self.shader_ubos.update_shader()
|
||||
self.batch.draw(self.shader)
|
||||
|
||||
def is_quarantined(self, k):
|
||||
return k in self._quarantine[self.shader]
|
||||
def quarantine(self, k):
|
||||
# dprint(f'BufferedRender_Batch: quarantining {k} for {self.shader}')
|
||||
pass
|
||||
self._quarantine[self.shader].add(k)
|
||||
def set_shader_option(self, k, v):
|
||||
if self.is_quarantined(k): return
|
||||
try: self.shader_ubos.options.assign(k, v)
|
||||
except Exception as e: self.quarantine(k)
|
||||
|
||||
def draw(self, opts):
|
||||
if self.shader == None or self.count == 0: return
|
||||
if self.drawtype == self.LINES and opts.get('line width', 1.0) <= 0: return
|
||||
if self.drawtype == self.POINTS and opts.get('point size', 1.0) <= 0: return
|
||||
|
||||
ctx = bpy.context
|
||||
area, spc, r3d = ctx.area, ctx.space_data, ctx.space_data.region_3d
|
||||
rgn = ctx.region
|
||||
|
||||
if 'blend' in opts: gpustate.blend(opts['blend'])
|
||||
if 'depth test' in opts: gpustate.depth_test(opts['depth test'])
|
||||
if 'depth mask' in opts: gpustate.depth_mask(opts['depth mask'])
|
||||
|
||||
self.shader.bind()
|
||||
|
||||
# set defaults
|
||||
self.set_shader_option('color_normal', (1.0, 1.0, 1.0, 0.5))
|
||||
self.set_shader_option('color_selected', (0.5, 1.0, 0.5, 0.5))
|
||||
self.set_shader_option('color_warning', (1.0, 0.5, 0.0, 0.5))
|
||||
self.set_shader_option('color_pinned', (1.0, 0.0, 0.5, 0.5))
|
||||
self.set_shader_option('color_seam', (1.0, 0.0, 0.5, 0.5))
|
||||
self.set_shader_option('hidden', (0.9, 0, 0, 0))
|
||||
self.set_shader_option('offset', (0.0, 0, 0, 0))
|
||||
self.set_shader_option('dotoffset', (0.0, 0, 0, 0))
|
||||
self.set_shader_option('vert_scale', (1.0, 1.0, 1.0))
|
||||
self.set_shader_option('radius', (1.0, 0, 0, 0))
|
||||
|
||||
use0 = [
|
||||
1.0 if (not opts.get('no selection', False)) else 0.0,
|
||||
1.0 if (not opts.get('no warning', False)) else 0.0,
|
||||
1.0 if (not opts.get('no pinned', False)) else 0.0,
|
||||
1.0 if (not opts.get('no seam', False)) else 0.0,
|
||||
]
|
||||
use1 = [
|
||||
1.0 if (self.drawtype == self.POINTS) else 0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
self.set_shader_option('use_settings0', use0)
|
||||
self.set_shader_option('use_settings1', use1)
|
||||
|
||||
self.set_shader_option('matrix_m', opts['matrix model'])
|
||||
self.set_shader_option('matrix_mn', opts['matrix normal'])
|
||||
self.set_shader_option('matrix_t', opts['matrix target'])
|
||||
self.set_shader_option('matrix_ti', opts['matrix target inverse'])
|
||||
self.set_shader_option('matrix_v', opts['matrix view'])
|
||||
self.set_shader_option('matrix_vn', opts['matrix view normal'])
|
||||
self.set_shader_option('matrix_p', opts['matrix projection'])
|
||||
|
||||
mx, my, mz = opts.get('mirror x', False), opts.get('mirror y', False), opts.get('mirror z', False)
|
||||
symmetry = opts.get('symmetry', None)
|
||||
symmetry_frame = opts.get('symmetry frame', None)
|
||||
symmetry_view = opts.get('symmetry view', None)
|
||||
symmetry_effect = opts.get('symmetry effect', 0.0)
|
||||
mirroring = (0, 0, 0, 0)
|
||||
if symmetry and symmetry_frame:
|
||||
mirroring = (
|
||||
1 if 'x' in symmetry else 0,
|
||||
1 if 'y' in symmetry else 0,
|
||||
1 if 'z' in symmetry else 0,
|
||||
)
|
||||
self.set_shader_option('mirror_o', symmetry_frame.o)
|
||||
self.set_shader_option('mirror_x', symmetry_frame.x)
|
||||
self.set_shader_option('mirror_y', symmetry_frame.y)
|
||||
self.set_shader_option('mirror_z', symmetry_frame.z)
|
||||
mirror_settings = [
|
||||
{'Edge': 1.0, 'Face': 2.0}.get(symmetry_view, 0.0),
|
||||
symmetry_effect,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
self.set_shader_option('mirror_settings', mirror_settings)
|
||||
self.set_shader_option('mirroring', mirroring)
|
||||
|
||||
view_settings0 = [
|
||||
r3d.view_distance,
|
||||
0.0 if (r3d.view_perspective == 'ORTHO') else 1.0,
|
||||
opts.get('focus mult', 1.0),
|
||||
opts.get('alpha backface', 0.5),
|
||||
]
|
||||
view_settings1 = [
|
||||
1.0 if opts.get('cull backfaces', False) else 0.0,
|
||||
opts['unit scaling factor'],
|
||||
opts.get('normal offset', 0.0) if symmetry_view is None else 0.05,
|
||||
1.0 if opts.get('constrain offset', True) else 0.0,
|
||||
]
|
||||
view_settings2 = [
|
||||
0.99 if symmetry_view is None else 1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
self.set_shader_option('view_settings0', view_settings0)
|
||||
self.set_shader_option('view_settings1', view_settings1)
|
||||
self.set_shader_option('view_settings2', view_settings2)
|
||||
self.set_shader_option('view_position', region_2d_to_origin_3d(rgn, r3d, (area.width/2, area.height/2)))
|
||||
|
||||
self.set_shader_option('clip', (spc.clip_start, spc.clip_end, 0.0, 0.0))
|
||||
self.set_shader_option('screen_size', (area.width, area.height, 0.0, 0.0))
|
||||
|
||||
self.set_options(self.options_prefix, opts)
|
||||
self._draw(1, 1, 1)
|
||||
|
||||
if opts['draw mirrored'] and (mx or my or mz):
|
||||
self.set_options(f'{self.options_prefix} mirror', opts)
|
||||
if mx: self._draw(-1, 1, 1)
|
||||
if my: self._draw( 1, -1, 1)
|
||||
if mz: self._draw( 1, 1, -1)
|
||||
if mx and my: self._draw(-1, -1, 1)
|
||||
if mx and mz: self._draw(-1, 1, -1)
|
||||
if my and mz: self._draw( 1, -1, -1)
|
||||
if mx and my and mz: self._draw(-1, -1, -1)
|
||||
|
||||
gpu.shader.unbind()
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import copy
|
||||
import math
|
||||
import inspect
|
||||
|
||||
class IgnoreChange(Exception): pass
|
||||
|
||||
class BoundVar:
|
||||
def __init__(self, value_str, *, on_change=None, frame_depth=1, frames_deep=1, f_globals=None, f_locals=None, callbacks=None, validators=None, disabled=False, pre_wrap=None, post_wrap=None, wrap=None):
|
||||
assert type(value_str) is str, f'BoundVar: constructor needs value as string, but received {value_str} instead!'
|
||||
if f_globals is None or f_locals is None:
|
||||
frame = inspect.currentframe()
|
||||
ff_globals, ff_locals = {}, {}
|
||||
for i in range(frame_depth + frames_deep):
|
||||
if i >= frame_depth:
|
||||
ff_globals = frame.f_globals | ff_globals
|
||||
ff_locals = frame.f_locals | ff_locals
|
||||
frame = frame.f_back
|
||||
self._f_globals = f_globals or ff_globals
|
||||
self._f_locals = dict(f_locals or ff_locals)
|
||||
else:
|
||||
self._f_globals = f_globals
|
||||
self._f_locals = dict(f_locals)
|
||||
try:
|
||||
exec(value_str, self._f_globals, self._f_locals)
|
||||
except Exception as e:
|
||||
print(f'Caught exception when trying to bind to variable')
|
||||
print(f'exception: {e}')
|
||||
print(f'globals: {self._f_globals}')
|
||||
print(f'locals: {self._f_locals}')
|
||||
assert False, f'BoundVar: value string ("{value_str}") must be a valid variable!'
|
||||
self._f_locals.update({'boundvar_interface': self._boundvar_interface})
|
||||
self._value_str = value_str
|
||||
self._callbacks = callbacks or []
|
||||
self._validators = validators or []
|
||||
self._disabled = disabled
|
||||
self._pre_wrap = wrap if wrap is not None else pre_wrap if pre_wrap is not None else ''
|
||||
self._post_wrap = wrap if wrap is not None else post_wrap if post_wrap is not None else ''
|
||||
if on_change: self.on_change(on_change)
|
||||
|
||||
def clone_with_overrides(self, **overrides):
|
||||
# perform SHALLOW copy (shared attribs, such as _callbacks!) and override attribs as given
|
||||
other = copy.copy(self)
|
||||
for k, v in overiddes.iteritems():
|
||||
try:
|
||||
setattr(other, k, v)
|
||||
except AttributeError:
|
||||
setattr(other, f'_{k}', v)
|
||||
return other
|
||||
|
||||
def _boundvar_interface(self, v): self._v = v
|
||||
def _call_callbacks(self):
|
||||
for cb in self._callbacks: cb()
|
||||
|
||||
def __str__(self): return str(self.value)
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
return self._disabled
|
||||
@disabled.setter
|
||||
def disabled(self, v):
|
||||
self._disabled = bool(v)
|
||||
self._call_callbacks()
|
||||
|
||||
def get_value(self):
|
||||
exec(f'boundvar_interface({self._value_str})', self._f_globals, self._f_locals)
|
||||
return self._v
|
||||
def set_value(self, value):
|
||||
try:
|
||||
for validator in self._validators: value = validator(value)
|
||||
except IgnoreChange:
|
||||
return
|
||||
if self.value == value: return
|
||||
exec(f'{self._value_str} = {self._pre_wrap}{value}{self._post_wrap}', self._f_globals, self._f_locals)
|
||||
self._call_callbacks()
|
||||
|
||||
@property
|
||||
def value(self): return self.get_value()
|
||||
@value.setter
|
||||
def value(self, value): self.set_value(value)
|
||||
@property
|
||||
def value_as_str(self): return str(self)
|
||||
|
||||
@property
|
||||
def is_bounded(self): return False
|
||||
|
||||
def on_change(self, fn): self._callbacks.append(fn)
|
||||
|
||||
def add_validator(self, fn): self._validators.append(fn)
|
||||
|
||||
|
||||
class BoundString(BoundVar):
|
||||
def __init__(self, value_str, *, frame_depth=2, **kwargs):
|
||||
super().__init__(value_str, frame_depth=frame_depth, wrap='"', **kwargs)
|
||||
|
||||
class BoundStringToBool(BoundVar):
|
||||
def __init__(self, value_str, true_str, *, frame_depth=2, **kwargs):
|
||||
self._true_str = true_str
|
||||
super().__init__(value_str, frame_depth=frame_depth, wrap='"', **kwargs)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.get_value() == self._true_str
|
||||
@value.setter
|
||||
def value(self, v):
|
||||
if bool(v): self.set_value(self._true_str)
|
||||
@property
|
||||
def checked(self):
|
||||
return self.get_value() == self._true_str
|
||||
@checked.setter
|
||||
def checked(self, v):
|
||||
# sets to a true str iff v is True
|
||||
# assuming that some other BoundStringToBool will get set to True
|
||||
if bool(v): self.value = self._true_str
|
||||
|
||||
|
||||
class BoundBool(BoundVar):
|
||||
def __init__(self, value_str, *, frame_depth=2, **kwargs):
|
||||
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
|
||||
@property
|
||||
def checked(self): return self.get_value()
|
||||
@checked.setter
|
||||
def checked(self,v): self.set_value(v)
|
||||
|
||||
|
||||
class BoundInt(BoundVar):
|
||||
def __init__(self, value_str, *, min_value=None, max_value=None, step_size=None, frame_depth=2, **kwargs):
|
||||
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
|
||||
self._min_value = min_value
|
||||
self._max_value = max_value
|
||||
self._step_size = step_size or 0
|
||||
self.add_validator(self.int_validator)
|
||||
|
||||
@property
|
||||
def min_value(self): return self._min_value
|
||||
|
||||
@property
|
||||
def max_value(self): return self._max_value
|
||||
|
||||
@property
|
||||
def step_size(self): return self._step_size
|
||||
|
||||
@property
|
||||
def is_bounded(self):
|
||||
return self._min_value is not None and self._max_value is not None
|
||||
|
||||
@property
|
||||
def bounded_ratio(self):
|
||||
assert self.is_bounded, f'Cannot compute bounded_ratio of unbounded BoundInt'
|
||||
return (self.value - self.min_value) / (self.max_value - self.min_value)
|
||||
|
||||
def int_validator(self, value):
|
||||
try:
|
||||
t = type(value)
|
||||
if t is str: nv = int(re.sub(r'[^\d.-]', '', value))
|
||||
elif t is int: nv = value
|
||||
elif t is float: nv = int(value)
|
||||
else: assert False, 'Unhandled type of value: %s (%s)' % (str(value), str(t))
|
||||
if self._min_value is not None: nv = max(nv, self._min_value)
|
||||
if self._max_value is not None: nv = min(nv, self._max_value)
|
||||
if self._step_size and self._min_value is not None:
|
||||
nv = math.floor((nv - self._min_value) / self._step_size) * self._step_size + self._min_value
|
||||
return nv
|
||||
except ValueError as e:
|
||||
raise IgnoreChange()
|
||||
except Exception:
|
||||
# ignoring all exceptions?
|
||||
raise IgnoreChange()
|
||||
|
||||
def add_delta(self, scale):
|
||||
self.value += self.step_size * scale
|
||||
|
||||
|
||||
class BoundFloat(BoundVar):
|
||||
def __init__(self, value_str, *, min_value=None, max_value=None, step_size=None, frame_depth=2, format_str=None, **kwargs):
|
||||
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
|
||||
self._min_value = min_value
|
||||
self._max_value = max_value
|
||||
self._step_size = step_size or 0
|
||||
self.add_validator(self.float_validator)
|
||||
self._format_str = '%0.5f' if format_str is None else format_str
|
||||
|
||||
def __str__(self):
|
||||
return self._format_str % self.value
|
||||
|
||||
@property
|
||||
def min_value(self): return self._min_value
|
||||
|
||||
@property
|
||||
def max_value(self): return self._max_value
|
||||
|
||||
@property
|
||||
def step_size(self): return self._step_size
|
||||
|
||||
@property
|
||||
def is_bounded(self):
|
||||
return self._min_value is not None and self._max_value is not None
|
||||
|
||||
@property
|
||||
def bounded_ratio(self):
|
||||
assert self.is_bounded, f'Cannot compute bounded_ratio of unbounded BoundFloat'
|
||||
return (self.value - self.min_value) / (self.max_value - self.min_value)
|
||||
|
||||
def float_validator(self, value):
|
||||
try:
|
||||
t = type(value)
|
||||
if t is str: nv = float(re.sub(r'[^\d.-]', '', value))
|
||||
elif t is int: nv = float(value)
|
||||
elif t is float: nv = value
|
||||
else: assert False, 'Unhandled type of value: %s (%s)' % (str(value), str(t))
|
||||
if self._min_value is not None: nv = max(nv, self._min_value)
|
||||
if self._max_value is not None: nv = min(nv, self._max_value)
|
||||
if self._step_size and self._min_value is not None:
|
||||
nv = math.floor((nv - self._min_value) / self._step_size) * self._step_size + self._min_value
|
||||
return nv
|
||||
except ValueError as e:
|
||||
raise IgnoreChange()
|
||||
except Exception:
|
||||
# ignoring all exceptions?
|
||||
raise IgnoreChange()
|
||||
|
||||
def add_delta(self, scale):
|
||||
self.value += self.step_size * scale
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
#####################################################################################
|
||||
# below are various token converters
|
||||
|
||||
# dictionary to convert color name to color values, either (R,G,B) or (R,G,B,a)
|
||||
# https://www.quackit.com/css/css_color_codes.cfm
|
||||
|
||||
colorname_to_color = {
|
||||
'transparent': (0, 0, 0, 0),
|
||||
|
||||
# https://www.quackit.com/css/css_color_codes.cfm
|
||||
'indianred': (205,92,92),
|
||||
'lightcoral': (240,128,128),
|
||||
'salmon': (250,128,114),
|
||||
'darksalmon': (233,150,122),
|
||||
'lightsalmon': (255,160,122),
|
||||
'crimson': (220,20,60),
|
||||
'red': (255,0,0),
|
||||
'firebrick': (178,34,34),
|
||||
'darkred': (139,0,0),
|
||||
'pink': (255,192,203),
|
||||
'lightpink': (255,182,193),
|
||||
'hotpink': (255,105,180),
|
||||
'deeppink': (255,20,147),
|
||||
'mediumvioletred': (199,21,133),
|
||||
'palevioletred': (219,112,147),
|
||||
'coral': (255,127,80),
|
||||
'tomato': (255,99,71),
|
||||
'orangered': (255,69,0),
|
||||
'darkorange': (255,140,0),
|
||||
'orange': (255,165,0),
|
||||
'gold': (255,215,0),
|
||||
'yellow': (255,255,0),
|
||||
'lightyellow': (255,255,224),
|
||||
'lemonchiffon': (255,250,205),
|
||||
'lightgoldenrodyellow': (250,250,210),
|
||||
'papayawhip': (255,239,213),
|
||||
'moccasin': (255,228,181),
|
||||
'peachpuff': (255,218,185),
|
||||
'palegoldenrod': (238,232,170),
|
||||
'khaki': (240,230,140),
|
||||
'darkkhaki': (189,183,107),
|
||||
'lavender': (230,230,250),
|
||||
'thistle': (216,191,216),
|
||||
'plum': (221,160,221),
|
||||
'violet': (238,130,238),
|
||||
'orchid': (218,112,214),
|
||||
'fuchsia': (255,0,255),
|
||||
'magenta': (255,0,255),
|
||||
'mediumorchid': (186,85,211),
|
||||
'mediumpurple': (147,112,219),
|
||||
'blueviolet': (138,43,226),
|
||||
'darkviolet': (148,0,211),
|
||||
'darkorchid': (153,50,204),
|
||||
'darkmagenta': (139,0,139),
|
||||
'purple': (128,0,128),
|
||||
'rebeccapurple': (102,51,153),
|
||||
'indigo': (75,0,130),
|
||||
'mediumslateblue': (123,104,238),
|
||||
'slateblue': (106,90,205),
|
||||
'darkslateblue': (72,61,139),
|
||||
'greenyellow': (173,255,47),
|
||||
'chartreuse': (127,255,0),
|
||||
'lawngreen': (124,252,0),
|
||||
'lime': (0,255,0),
|
||||
'limegreen': (50,205,50),
|
||||
'palegreen': (152,251,152),
|
||||
'lightgreen': (144,238,144),
|
||||
'mediumspringgreen': (0,250,154),
|
||||
'springgreen': (0,255,127),
|
||||
'mediumseagreen': (60,179,113),
|
||||
'seagreen': (46,139,87),
|
||||
'forestgreen': (34,139,34),
|
||||
'green': (0,128,0),
|
||||
'darkgreen': (0,100,0),
|
||||
'yellowgreen': (154,205,50),
|
||||
'olivedrab': (107,142,35),
|
||||
'olive': (128,128,0),
|
||||
'darkolivegreen': (85,107,47),
|
||||
'mediumaquamarine': (102,205,170),
|
||||
'darkseagreen': (143,188,143),
|
||||
'lightseagreen': (32,178,170),
|
||||
'darkcyan': (0,139,139),
|
||||
'teal': (0,128,128),
|
||||
'aqua': (0,255,255),
|
||||
'cyan': (0,255,255),
|
||||
'lightcyan': (224,255,255),
|
||||
'paleturquoise': (175,238,238),
|
||||
'aquamarine': (127,255,212),
|
||||
'turquoise': (64,224,208),
|
||||
'mediumturquoise': (72,209,204),
|
||||
'darkturquoise': (0,206,209),
|
||||
'cadetblue': (95,158,160),
|
||||
'steelblue': (70,130,180),
|
||||
'lightsteelblue': (176,196,222),
|
||||
'powderblue': (176,224,230),
|
||||
'lightblue': (173,216,230),
|
||||
'skyblue': (135,206,235),
|
||||
'lightskyblue': (135,206,250),
|
||||
'deepskyblue': (0,191,255),
|
||||
'dodgerblue': (30,144,255),
|
||||
'cornflowerblue': (100,149,237),
|
||||
'royalblue': (65,105,225),
|
||||
'blue': (0,0,255),
|
||||
'mediumblue': (0,0,205),
|
||||
'darkblue': (0,0,139),
|
||||
'navy': (0,0,128),
|
||||
'midnightblue': (25,25,112),
|
||||
'cornsilk': (255,248,220),
|
||||
'blanchedalmond': (255,235,205),
|
||||
'bisque': (255,228,196),
|
||||
'navajowhite': (255,222,173),
|
||||
'wheat': (245,222,179),
|
||||
'burlywood': (222,184,135),
|
||||
'tan': (210,180,140),
|
||||
'rosybrown': (188,143,143),
|
||||
'sandybrown': (244,164,96),
|
||||
'goldenrod': (218,165,32),
|
||||
'darkgoldenrod': (184,134,11),
|
||||
'peru': (205,133,63),
|
||||
'chocolate': (210,105,30),
|
||||
'saddlebrown': (139,69,19),
|
||||
'sienna': (160,82,45),
|
||||
'brown': (165,42,42),
|
||||
'maroon': (128,0,0),
|
||||
'white': (255,255,255),
|
||||
'snow': (255,250,250),
|
||||
'honeydew': (240,255,240),
|
||||
'mintcream': (245,255,250),
|
||||
'azure': (240,255,255),
|
||||
'aliceblue': (240,248,255),
|
||||
'ghostwhite': (248,248,255),
|
||||
'whitesmoke': (245,245,245),
|
||||
'seashell': (255,245,238),
|
||||
'beige': (245,245,220),
|
||||
'oldlace': (253,245,230),
|
||||
'floralwhite': (255,250,240),
|
||||
'ivory': (255,255,240),
|
||||
'antiquewhite': (250,235,215),
|
||||
'linen': (250,240,230),
|
||||
'lavenderblush': (255,240,245),
|
||||
'mistyrose': (255,228,225),
|
||||
'gainsboro': (220,220,220),
|
||||
'lightgray': (211,211,211),
|
||||
'lightgrey': (211,211,211),
|
||||
'silver': (192,192,192),
|
||||
'darkgray': (169,169,169),
|
||||
'darkgrey': (169,169,169),
|
||||
'gray': (128,128,128),
|
||||
'grey': (128,128,128),
|
||||
'dimgray': (105,105,105),
|
||||
'dimgrey': (105,105,105),
|
||||
'lightslategray': (119,136,153),
|
||||
'lightslategrey': (119,136,153),
|
||||
'slategray': (112,128,144),
|
||||
'slategrey': (112,128,144),
|
||||
'darkslategray': (47,79,79),
|
||||
'darkslategrey': (47,79,79),
|
||||
'black': (0,0,0),
|
||||
}
|
||||
@@ -0,0 +1,940 @@
|
||||
/*
|
||||
|
||||
https://en.wikipedia.org/wiki/Flat_design#/media/File:Flat_widgets.png
|
||||
https://bulma.io/documentation/elements/button/
|
||||
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 2px 2px;
|
||||
padding: 4px 8px;
|
||||
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: auto;
|
||||
max-height: auto;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 4px;
|
||||
|
||||
overflow: hidden;
|
||||
font: normal normal 12pt sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 4px rgba(0,0,0,0.25);
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
text::text {
|
||||
display: inline;
|
||||
background: transparent;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
button {
|
||||
white-space: normal;
|
||||
cursor: default;
|
||||
display: inline;
|
||||
border-width: 0px;
|
||||
border-radius: 4px;
|
||||
border-color: white rgb(96,96,96) rgb(32,32,32) rgb(224,224,224);
|
||||
background: rgba(160,160,160, 0.50);
|
||||
color: black;
|
||||
}
|
||||
button:focus {
|
||||
/*background: yellow;*/
|
||||
/*background: lightcyan;*/
|
||||
background: rgba(160,160,160, 1.00);
|
||||
}
|
||||
button:active {
|
||||
/*background: rgb(192,128,128);*/
|
||||
background: hsla(210, 100%, 45%, 1.0);
|
||||
}
|
||||
button:hover {
|
||||
background: rgba(160, 160, 160, 1.00); /* hsla(200, 100%, 62.5%, 1.0); /* rgb(64,192,255); */
|
||||
}
|
||||
button:active:hover {
|
||||
background: hsla(210, 60%, 35%, 1.0);
|
||||
}
|
||||
button:disabled {
|
||||
background-color: rgb(128,128,128);
|
||||
color: rgb(192,192,192);
|
||||
/*border-width: 1px;*/
|
||||
border-color: rgb(96,96,96);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
width: 100%;
|
||||
/*margin: 2px 0px;*/
|
||||
padding: 2px;
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
p span {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
font-size: 10pt;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
/*margin: 2px 0px;*/
|
||||
padding: 4px;
|
||||
background-color: rgba(255,255,255,0.9);
|
||||
border: 1px black;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: rgba(0,0,0,0.25);
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 2px 0px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
br {
|
||||
display: block;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
img {
|
||||
/*max-width: 100%;*/
|
||||
object-fit: contain;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0,0,0,0.25);
|
||||
background-color: hsla(200, 100%, 62.5%, 0.0); /* rgba(255,0,0,0); */
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: white;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
/* make sure we handle cases correctly! */
|
||||
input {
|
||||
background: pink;
|
||||
}
|
||||
|
||||
|
||||
ul {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
padding: 0px 0px 0px 12px;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
ul > li::marker {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
margin: 5px 5px 0px 5px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-image: url('radio.png');
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
padding: 0px 0px 0px 12px;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
ol > li::marker {
|
||||
color: white;
|
||||
position: absolute;
|
||||
/*left: -8px;*/
|
||||
left: 0px;
|
||||
/*margin: 5px 5px 0px 5px;*/
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
width: 15px;
|
||||
/*height: 10px;*/
|
||||
/*background-image: url('radio.png');*/
|
||||
}
|
||||
|
||||
|
||||
dialog {
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
border: 1px black;
|
||||
background: rgba(32, 32, 32, 0.75);
|
||||
/*overflow-x: scroll;*/
|
||||
overflow-y: scroll;
|
||||
color: white;
|
||||
width: 500px;
|
||||
/*max-width: 750px;*/
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
dialog.framed {
|
||||
border: 2px rgba(0,0,0,0.75);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
padding: 0px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
dialog div.contents {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
div.dialog-header {
|
||||
background: hsla(200, 0%, 25%, 0.75); /* hsla(0, 0%, 25%, 0.75); */
|
||||
margin: 0px;
|
||||
padding: 2px;
|
||||
border: 1px rgba(0,0,0,0.25) rgba(0,0,0,0.25) rgba(0,0,0,1) rgba(0,0,0,0.25);
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
dialog.framed.moveable div.dialog-header {
|
||||
cursor: grab;
|
||||
}
|
||||
dialog.framed.moveable div.dialog-header:hover {
|
||||
background-color: hsla(200, 25%, 40%, 0.75); /* hsla(0,0%,40%,0.75);*/
|
||||
}
|
||||
dialog.framed.moveable div.dialog-header:active {
|
||||
background-color: hsla(200, 100%, 60%, 0.75); /*hsla(0,0%,40%,0.5);*/
|
||||
}
|
||||
|
||||
span.dialog-title {
|
||||
margin: 0px;
|
||||
border: 0px white;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
/*font-weight: bold;*/
|
||||
white-space: pre;
|
||||
cursor: grab;
|
||||
text-shadow: 2px 2px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
button.dialog-close {
|
||||
margin: 0px;
|
||||
padding: 2px;
|
||||
border: 0px;
|
||||
display: inline;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('close.png');
|
||||
}
|
||||
|
||||
dialog.framed > div.inside {
|
||||
margin: 0px;
|
||||
border-width: 1px;
|
||||
border-radius: 0px;
|
||||
border-color: rgba(0,0,0,1.0) rgba(0,0,0,0.25) rgba(0,0,0,0.25) rgba(0,0,0,0.25);
|
||||
padding: 5px 2px 2px 2px;
|
||||
}
|
||||
|
||||
div.dialog-footer {
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: 50px;
|
||||
top: -200px;
|
||||
width: 100%;
|
||||
/*bottom: 0px;*/
|
||||
background: hsla(200, 0%, 25%, 0.75); /* hsla(0, 0%, 25%, 0.75); */
|
||||
margin: 0px;
|
||||
padding: 2px;
|
||||
border: 1px rgba(0,0,0,1) rgba(0,0,0,0.25) rgba(0,0,0,0.25) rgba(0,0,0,0.25);
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
div.dialog-footer > * {
|
||||
margin: 0px;
|
||||
border: 0px white;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************/
|
||||
/* TABLES */
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
display: table;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border: 1px rgba(0,0,0,1);
|
||||
margin: 0px 10px;
|
||||
padding: 4px;
|
||||
}
|
||||
tr {
|
||||
width: 100%;
|
||||
display: table-row;
|
||||
margin: 0px 0px;
|
||||
border: 0px;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
th {
|
||||
display: table-cell;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 2px;
|
||||
}
|
||||
td {
|
||||
display: table-cell;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************/
|
||||
/* MARKDOWN */
|
||||
|
||||
|
||||
article.mdown {
|
||||
margin: 2px;
|
||||
border: 1px rgba(0,0,0,0.5);
|
||||
padding: 4px 4px 4px 4px;
|
||||
/*padding: 4px 4px 16px 4px;*/
|
||||
background: rgba(48,48,48,0.75);
|
||||
}
|
||||
|
||||
article.mdown div {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
article.mdown span {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
article.mdown h1 {
|
||||
width: 100%;
|
||||
margin: 0px 4px 4px 4px;
|
||||
padding: 4px 4px 4px 12px;
|
||||
border: 0px;
|
||||
font-size: 24;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
article.mdown h2 {
|
||||
width: 100%;
|
||||
margin: 8px 4px 4px 4px;
|
||||
padding: 4px 4px 4px 12px;
|
||||
border: 1px transparent;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 0px;
|
||||
font-size: 18;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
article.mdown h3 {
|
||||
width: 100%;
|
||||
margin: 8px 16px 4px 16px;
|
||||
padding: 4px 4px 4px 12px;
|
||||
border: 1px transparent;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.125);
|
||||
border-radius: 0px;
|
||||
font-size: 15;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
article.mdown img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px rgba(0, 0, 0, 0.50);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
article.mdown p { }
|
||||
|
||||
article.mdown i {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
article.mdown b {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
article.mdown pre {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 0px;
|
||||
padding: 0px 4px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
article.mdown code {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 0px;
|
||||
padding: 0px 4px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
|
||||
/*article.mdown ul {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
article.mdown ul > li {
|
||||
/*margin: 8px 0px;* /
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
padding: 0px 0px 0px 8px;
|
||||
display: block;
|
||||
}
|
||||
article.mdown ul > li > img.dot {
|
||||
display: inline;
|
||||
margin: 5px 10px 0px 5px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
}
|
||||
article.mdown ul > li > span.text {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: inline;
|
||||
}*/
|
||||
|
||||
|
||||
/*article.mdown ol {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
article.mdown ol > li {
|
||||
/*margin: 8px 0px;* /
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
padding: 0px 0px 0px 8px;
|
||||
display: block;
|
||||
}
|
||||
article.mdown ol > li > span.number {
|
||||
display: inline;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
width: 20px;
|
||||
/*height: 10px;* /
|
||||
}
|
||||
article.mdown ol > li > span.text {
|
||||
margin: 0px 0px 0px 8px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: inline;
|
||||
}
|
||||
*/
|
||||
|
||||
/* background-color: hsla(200, 25%, 25%, 0.75); */
|
||||
/* background-color: hsla(200, 25%, 40%, 0.75); */
|
||||
/* background-color: hsla(200, 25%, 60%, 0.75); */
|
||||
|
||||
article.mdown a {
|
||||
padding: 0px -1px 0px 0px;
|
||||
margin: 0px;
|
||||
background-color: transparent; /* hsla(200, 25%, 25%, 0.75); */
|
||||
border: 1px transparent; /* hsla(200, 25%, 25%, 0.75); */
|
||||
border-radius: 0px;
|
||||
border-bottom-color: rgba(255,255,255,0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
article.mdown a:hover {
|
||||
background-color: hsla(200, 25%, 40%, 0.75);
|
||||
border: 1px hsla(200, 25%, 40%, 0.75);
|
||||
border-bottom-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
article.mdown img.inline {
|
||||
display: inline;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************/
|
||||
/* CHECKBOX INPUT */
|
||||
|
||||
input[type="checkbox"] {
|
||||
background-color: transparent;
|
||||
border-width: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] > img.checkbox {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
margin-right: 5px;
|
||||
border-width: 0px;
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
background-color: rgba(160, 160, 160, 0.50); /* hsla(200, 0%, 62.5%, 1.0);*/
|
||||
background-image: none;
|
||||
}
|
||||
input[type="checkbox"]:hover > img.checkbox {
|
||||
background-color: rgba(160, 160, 160, 1.00); /* hsla(200, 0%, 75%, 1.0); */
|
||||
}
|
||||
input[type="checkbox"]:checked > img.checkbox {
|
||||
background-color: hsla(200, 100%, 62.5%, 1.0);
|
||||
background-image: url('checkmark.png');
|
||||
}
|
||||
input[type="checkbox"]:active > img.checkbox {
|
||||
background-color: hsla(200, 100%, 75%, 1.0);
|
||||
}
|
||||
|
||||
input[type="checkbox"] > label {
|
||||
color: rgba(255, 255, 255, 0.50);
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-top: 3px;
|
||||
padding-right: 10px;
|
||||
border-width: 0px;
|
||||
}
|
||||
input[type="checkbox"]:hover > label {
|
||||
color: rgba(255, 255, 255, 1.00);
|
||||
}
|
||||
input[type="checkbox"]:checked > label {
|
||||
color: rgba(255, 255, 255, 1.00);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************/
|
||||
/* RADIO INPUT */
|
||||
|
||||
input[type="radio"] {
|
||||
background-color: transparent;
|
||||
border-width: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
input[type="radio"] > img.radio {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
margin-right: 5px;
|
||||
border: 0px;
|
||||
border-radius: 12px;
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
background-color: rgba(160, 160, 160, 0.50); /* hsla(200, 0%, 62.5%, 1.0);*/
|
||||
background-image: none;
|
||||
}
|
||||
input[type="radio"]:hover > img.radio {
|
||||
background-color: rgba(160, 160, 160, 1.00); /* hsla(200, 0%, 75%, 1.0); */
|
||||
}
|
||||
input[type="radio"]:active > img.radio {
|
||||
background-color: hsla(200, 100%, 75%, 1.0);
|
||||
}
|
||||
input[type="radio"]:checked > img.radio {
|
||||
background: hsla(200, 100%, 62.5%, 1.0) url('radio.png');
|
||||
}
|
||||
|
||||
input[type="radio"] > label {
|
||||
color: rgba(255, 255, 255, 0.50);
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-top: 3px;
|
||||
padding-right: 10px;
|
||||
border-width: 0px;
|
||||
}
|
||||
input[type="radio"]:hover > label {
|
||||
color: rgba(255, 255, 255, 1.00);
|
||||
}
|
||||
input[type="radio"]:checked > label {
|
||||
color: rgba(255, 255, 255, 1.00);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************/
|
||||
/* COLLAPSIBLE COLLECTION */
|
||||
|
||||
/*div.collapsible > input.header {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
div.collapsible > input.header > img {
|
||||
background: transparent url('collapse_open.png');
|
||||
}
|
||||
div.collapsible > input.header:checked > img {
|
||||
background: transparent url('collapse_close.png');
|
||||
}
|
||||
div.collapsible > div.inside {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
div.collapsible > div.inside.collapsed {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
|
||||
details > div.header {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
details > div.header > img.marker {
|
||||
background: transparent url('collapse_close.png');
|
||||
}
|
||||
details[open] > div.header > img.marker {
|
||||
background: transparent url('collapse_open.png');
|
||||
}
|
||||
details > div.inside {
|
||||
display: none;
|
||||
}
|
||||
details[open] > div.inside {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************/
|
||||
/* TEXT INPUT */
|
||||
|
||||
/*
|
||||
div.inputtext-container
|
||||
input.inputtext-input
|
||||
span.inputtext-cursor
|
||||
*/
|
||||
|
||||
/*.inputtext-container {
|
||||
margin: 0px;
|
||||
/*position: relative;* /
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
background: rgba(160,160,160, 0.50);
|
||||
/*background-color: rgba(255,255,255,0.5);* /
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: black;
|
||||
}
|
||||
*.inputtext-container:hover {
|
||||
background-color: rgba(160,160,160,1.00);
|
||||
}*/
|
||||
|
||||
/**.inputtext-container > */
|
||||
*.inputtext-input {
|
||||
position: relative; /* necessary for cursor! */
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
margin: 0px;
|
||||
padding: 3px;
|
||||
border-width: 2px;
|
||||
border-color: transparent;
|
||||
color: black;
|
||||
overflow-x: scroll;
|
||||
cursor: text;
|
||||
}
|
||||
/**.inputtext-container > */
|
||||
*.inputtext-input:focus {
|
||||
background-color: rgba(255, 255, 255, 1.0);
|
||||
border-color: hsla(200, 100%, 62.5%, 1.0);
|
||||
}
|
||||
|
||||
/**.inputtext-cursor {
|
||||
position: absolute;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-width: 0px;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
display: none;
|
||||
color: hsla(200, 100%, 12.5%, 1.0); /* l=62.5% * /
|
||||
}*/
|
||||
|
||||
/*input[type="text"]:focus *.inputtext-cursor {
|
||||
display: span;
|
||||
background: pink;
|
||||
}*/
|
||||
|
||||
input[type="text"] {
|
||||
position: relative; /* necessary for cursor! */
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
margin: 0px;
|
||||
padding: 3px;
|
||||
border: 2px transparent;
|
||||
color: black;
|
||||
overflow-x: scroll;
|
||||
cursor: text;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
background-color: rgba(255, 255, 255, 1.0);
|
||||
border-color: hsla(200, 100%, 62.5%, 1.0);
|
||||
}
|
||||
|
||||
input[type="text"]::marker {
|
||||
position: absolute;
|
||||
margin: -1px 0px 0px 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
background: transparent;
|
||||
color: white;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
content: "|";
|
||||
}
|
||||
|
||||
|
||||
input[type="number"] {
|
||||
position: relative; /* necessary for cursor! */
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
margin: 0px;
|
||||
padding: 3px;
|
||||
border: 2px transparent;
|
||||
color: black;
|
||||
overflow-x: scroll;
|
||||
cursor: text;
|
||||
}
|
||||
input[type="number"]:focus {
|
||||
background-color: rgba(255, 255, 255, 1.0);
|
||||
border-color: hsla(200, 100%, 62.5%, 1.0);
|
||||
}
|
||||
|
||||
input[type="number"]::marker {
|
||||
position: absolute;
|
||||
margin: -1px 0px 0px 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
background: transparent;
|
||||
color: white;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
content: "|";
|
||||
}
|
||||
|
||||
|
||||
/***********************/
|
||||
/* LABELED TEXT INPUT */
|
||||
|
||||
/*
|
||||
div.labeledinputtext-container
|
||||
div.labeledinputtext-label-container
|
||||
label.labeledinputtext-label
|
||||
div.labeledinputtext-input-container
|
||||
div.inputtext-container
|
||||
input.inputtext-input
|
||||
span.inputtext-cursor
|
||||
*/
|
||||
|
||||
*.labeledinputtext-container {
|
||||
margin: 2px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
*.labeledinputtext-container > *.labeledinputtext-label-container {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 4px 4px 0px 0px;
|
||||
display: inline;
|
||||
width: 50%;
|
||||
background: transparent;
|
||||
}
|
||||
*.labeledinputtext-container > *.labeledinputtext-label-container > *.labeledinputtext-label {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
*.labeledinputtext-container > *.labeledinputtext-input-container {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: inline;
|
||||
width: 50%;
|
||||
}
|
||||
*.labeledinputtext-container > *.labeledinputtext-input-container > *.inputtext-container {
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
/*max-height: 22px;*/
|
||||
height: 26px;
|
||||
}
|
||||
*.labeledinputtext-container > *.labeledinputtext-input-container > *.inputtext-container > *.inputtext-input {
|
||||
margin: 0px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
||||
/*************************/
|
||||
/* INPUT RANGE */
|
||||
|
||||
input[type="range"] {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0px 0px 0px 0px;
|
||||
padding: 0px 8px 0px 0px;
|
||||
border: 0px;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input[type="range"] > * {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input[type="range"] > *.inputrange-left {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
height: 8px;
|
||||
border: 1px white;
|
||||
background-color: rgba(0, 0, 255, 1);
|
||||
}
|
||||
input[type="range"] > *.inputrange-right {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
height: 8px;
|
||||
border: 1px white;
|
||||
background-color: rgba(255, 0, 255, 1);
|
||||
}
|
||||
|
||||
input[type="range"] > *.inputrange-handle {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px black;
|
||||
background-color: rgba(128, 128, 128, 1);
|
||||
}
|
||||
input[type="range"]:hover > *.inputrange-handle {
|
||||
background-color: rgba(192, 192, 192, 1);
|
||||
}
|
||||
input[type="range"]:active > *.inputrange-handle {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
|
||||
dialog.tooltip {
|
||||
z-index: 100000;
|
||||
position: fixed;
|
||||
/*display: block;*/
|
||||
border: 1px black;
|
||||
background: hsla(200, 25%, 25%, 0.95); /*rgba(32,32,32,0.8);*/
|
||||
color: white;
|
||||
margin: 2px;
|
||||
padding: 4px;
|
||||
width: auto;
|
||||
min-width: 0px;
|
||||
max-width: 300px;
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
'''
|
||||
Copyright (C) 2014 Plasmasolutions
|
||||
software@plasmasolutions.de
|
||||
|
||||
Created by Thomas Beck
|
||||
Donated to CGCookie and the world
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
'''
|
||||
Note: not all of the following code was provided by Plasmasolutions
|
||||
TODO: split into separate files?
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import inspect
|
||||
import itertools
|
||||
import linecache
|
||||
import traceback
|
||||
from math import floor
|
||||
from hashlib import md5
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
|
||||
from .blender import show_blender_popup
|
||||
from .functools import find_fns
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
|
||||
|
||||
class Debugger:
|
||||
_error_level = 1
|
||||
_exception_count = 0
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def set_error_level(l):
|
||||
Debugger._error_level = max(0, min(5, int(l)))
|
||||
|
||||
@staticmethod
|
||||
def get_error_level():
|
||||
return Debugger._error_level
|
||||
|
||||
@staticmethod
|
||||
def dprint(*objects, sep=' ', end='\n', file=sys.stdout, flush=True, l=2):
|
||||
if Debugger._error_level < l: return
|
||||
sobjects = sep.join(str(o) for o in objects)
|
||||
print(
|
||||
f'DEBUG({l}): {sobjects}',
|
||||
end=end, file=file, flush=flush
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcallstack(l=2):
|
||||
''' print out the calling stack, skipping the first (call to dcallstack) '''
|
||||
Debugger.dprint('Call Stack Dump:', l=l)
|
||||
for i, entry in enumerate(inspect.stack()):
|
||||
if i > 0:
|
||||
Debugger.dprint(' %s' % str(entry), l=l)
|
||||
|
||||
@staticmethod
|
||||
def call_stack():
|
||||
return traceback.format_stack()
|
||||
|
||||
|
||||
# http://stackoverflow.com/questions/14519177/python-exception-handling-line-number
|
||||
@staticmethod
|
||||
def get_exception_info_and_hash():
|
||||
'''
|
||||
this function is a duplicate of the one above, but this will attempt
|
||||
to create a hash to make searching for duplicate bugs on github easier (?)
|
||||
'''
|
||||
|
||||
exc_type, exc_obj, tb = sys.exc_info()
|
||||
pathabs, pathdir = os.path.abspath, os.path.dirname
|
||||
pathjoin, pathsplit = os.path.join, os.path.split
|
||||
base_path = pathabs(pathjoin(pathdir(__file__), '..'))
|
||||
|
||||
hasher = Hasher()
|
||||
errormsg = ['EXCEPTION (%s): %s' % (exc_type, exc_obj)]
|
||||
hasher.add(errormsg[0])
|
||||
# errormsg += ['Base: %s' % base_path]
|
||||
|
||||
etb = traceback.extract_tb(tb)
|
||||
pfilename = None
|
||||
for i,entry in enumerate(reversed(etb)):
|
||||
filename,lineno,funcname,line = entry
|
||||
if pfilename is None:
|
||||
# only hash in details of where the exception occurred
|
||||
hasher.add(os.path.split(filename)[1])
|
||||
# hasher.add(lineno)
|
||||
hasher.add(funcname)
|
||||
hasher.add(line.strip())
|
||||
if filename != pfilename:
|
||||
pfilename = filename
|
||||
if filename.startswith(base_path):
|
||||
filename = '.../%s' % filename[len(base_path)+1:]
|
||||
errormsg += [' %s' % (filename, )]
|
||||
errormsg += ['%03d %04d:%s() %s' % (i, lineno, funcname, line.strip())]
|
||||
|
||||
return ('\n'.join(errormsg), hasher.get_hash())
|
||||
|
||||
@staticmethod
|
||||
def print_exception():
|
||||
Debugger._exception_count += 1
|
||||
errormsg, errorhash = Debugger.get_exception_info_and_hash()
|
||||
message = []
|
||||
message += ['Exception Info']
|
||||
message += ['- Time: %s' % datetime.today().isoformat(' ')]
|
||||
message += ['- Count: %d' % Debugger._exception_count]
|
||||
message += ['- Hash: %s' % str(errorhash)]
|
||||
message += ['- Info:']
|
||||
message += [' - %s' % s for s in errormsg.splitlines()]
|
||||
message = '\n'.join(message)
|
||||
print('%s\n%s\n%s' % ('_' * 100, message, '^' * 100))
|
||||
logger = Globals.logger
|
||||
if logger: logger.add(message) # write error to log text object
|
||||
# if Debugger._exception_count < 10:
|
||||
# show_blender_popup(
|
||||
# message,
|
||||
# title='Exception Info',
|
||||
# icon='ERROR',
|
||||
# wrap=240
|
||||
# )
|
||||
return message
|
||||
|
||||
# @staticmethod
|
||||
# def print_exception2():
|
||||
# exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
# print("*** print_tb:")
|
||||
# traceback.print_tb(exc_traceback, limit=1, file=sys.stdout)
|
||||
# print("*** print_exception:")
|
||||
# traceback.print_exception(exc_type, exc_value, exc_traceback,
|
||||
# limit=2, file=sys.stdout)
|
||||
# print("*** print_exc:")
|
||||
# traceback.print_exc()
|
||||
# print("*** format_exc, first and last line:")
|
||||
# formatted_lines = traceback.format_exc().splitlines()
|
||||
# print(formatted_lines[0])
|
||||
# print(formatted_lines[-1])
|
||||
# print("*** format_exception:")
|
||||
# print(repr(traceback.format_exception(exc_type, exc_value,exc_traceback)))
|
||||
# print("*** extract_tb:")
|
||||
# print(repr(traceback.extract_tb(exc_traceback)))
|
||||
# print("*** format_tb:")
|
||||
# print(repr(traceback.format_tb(exc_traceback)))
|
||||
# if exc_traceback:
|
||||
# print("*** tb_lineno:", exc_traceback.tb_lineno)
|
||||
|
||||
start_time = time.time()
|
||||
last_time = time.time()
|
||||
@staticmethod
|
||||
def tprint(*args):
|
||||
t = time.time()
|
||||
td = t - Debugger.last_time
|
||||
lbar = min(25, floor(td*20))
|
||||
bar = '%s%s' % ('X' * lbar, '_' * (25-lbar))
|
||||
print(bar, '%8.4f' % td, *args)
|
||||
sys.stdout.flush()
|
||||
Debugger.last_time = t
|
||||
|
||||
|
||||
class ExceptionHandler:
|
||||
_universal = []
|
||||
|
||||
@staticmethod
|
||||
def on_exception(fn):
|
||||
fn._exceptionhandler_on_exception = True
|
||||
return fn
|
||||
|
||||
def __init__(self, obj=None, *, universal=False):
|
||||
# print(f'ExceptionHandler.__init__({self})')
|
||||
self._single = []
|
||||
self._um = []
|
||||
self._universal_only = universal
|
||||
self.collect_callbacks(obj)
|
||||
|
||||
def __del__(self):
|
||||
# print(f'ExceptionHandler.__del__({self})')
|
||||
for fn in getattr(self, '_um', []):
|
||||
self.remove_universal_callback(fn)
|
||||
|
||||
def collect_callbacks(self, obj):
|
||||
if not obj: return
|
||||
for (_, fn) in find_fns(obj, '_exceptionhandler_on_exception'):
|
||||
self.add_callback(fn)
|
||||
|
||||
@staticmethod
|
||||
def add_universal_callback(fn):
|
||||
ExceptionHandler._universal += [fn]
|
||||
|
||||
@staticmethod
|
||||
def remove_universal_callback(fn):
|
||||
if fn not in ExceptionHandler._universal: return
|
||||
ExceptionHandler._universal.remove(fn)
|
||||
|
||||
@staticmethod
|
||||
def clear_universal_callbacks():
|
||||
ExceptionHandler._universal = []
|
||||
|
||||
def add_callback(self, fn, universal=None):
|
||||
# print(f'ExceptionHandler.add_callback({self}, {fn}, {universal})')
|
||||
if getattr(fn, '_exceptionhandler_collected', False): return
|
||||
fn._exceptionhandler_collected = True
|
||||
if universal is None and self._universal_only: universal = True
|
||||
if universal:
|
||||
self._universal += [fn]
|
||||
self._um += [fn]
|
||||
else:
|
||||
self._single += [fn]
|
||||
|
||||
def wrap(self, def_val, only=Exception):
|
||||
def wrapper(fn):
|
||||
def wrapped(*args, **kwargs):
|
||||
ret = def_val
|
||||
try:
|
||||
ret = fn(*args, **kwargs)
|
||||
except only as e:
|
||||
self.handle_exception(e)
|
||||
return ret
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
def handle_exception(self, e):
|
||||
# print(f'ExceptionHandler: calling back these fns')
|
||||
# for fn in itertools.chain(self._universal, self._single):
|
||||
# print(f' {fn}')
|
||||
for fn in itertools.chain(self._universal, self._single):
|
||||
try:
|
||||
fn(e)
|
||||
except Exception as e2:
|
||||
print(f'ExceptionHandler: Caught exception while calling back exception callbacks: {fn.__name__}')
|
||||
print(f' original: {e}')
|
||||
print(f' additional: {e2}')
|
||||
debugger.print_exception()
|
||||
|
||||
|
||||
debugger = Debugger()
|
||||
dprint = debugger.dprint
|
||||
tprint = debugger.tprint
|
||||
exceptionhandler = ExceptionHandler(universal=True)
|
||||
Globals.set(debugger)
|
||||
Globals.dprint = dprint
|
||||
Globals.exceptionhandler = exceptionhandler
|
||||
@@ -0,0 +1,419 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import inspect
|
||||
from functools import wraps
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def run(*args, **kwargs):
|
||||
if len(args) == 1 and not kwargs and inspect.isfunction(args[0]):
|
||||
# call right away
|
||||
return args[0]()
|
||||
def wrapper(fn):
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
debug_run_test_calls = False
|
||||
def debug_test_call(*args, **kwargs):
|
||||
def wrapper(fn):
|
||||
if debug_run_test_calls:
|
||||
ret = str(fn(*args,*kwargs))
|
||||
print('TEST: %s()' % fn.__name__)
|
||||
if args:
|
||||
print(' arg:', args)
|
||||
if kwargs:
|
||||
print(' kwa:', kwargs)
|
||||
print(' ret:', ret)
|
||||
return fn
|
||||
return wrapper
|
||||
|
||||
|
||||
def ignore_exceptions(*exceptions, default=None, warn=False):
|
||||
def wrap(fn):
|
||||
@wraps(fn)
|
||||
def run_with_ignore_exceptions(*args, **kwargs):
|
||||
ret = default
|
||||
try:
|
||||
ret = fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if not any(isinstance(e, ex) for ex in exceptions):
|
||||
# this exception should not be ignored
|
||||
raise e
|
||||
# ignoring thrown exception!
|
||||
if warn:
|
||||
print(f'Addon Common: ignoring exception')
|
||||
print(f' Function: {fn.__name__}')
|
||||
print(f' Exception: {e}')
|
||||
return ret
|
||||
return run_with_ignore_exceptions
|
||||
return wrap
|
||||
|
||||
|
||||
def stats_wrapper(fn):
|
||||
return fn
|
||||
|
||||
if not hasattr(stats_report, 'stats'):
|
||||
stats_report.stats = dict()
|
||||
frame = inspect.currentframe().f_back
|
||||
f_locals = frame.f_locals
|
||||
|
||||
filename = os.path.basename(frame.f_code.co_filename)
|
||||
clsname = f_locals['__qualname__'] if '__qualname__' in f_locals else ''
|
||||
linenum = frame.f_lineno
|
||||
fnname = fn.__name__
|
||||
key = '%s%s (%s:%d)' % (
|
||||
clsname + ('.' if clsname else ''),
|
||||
fnname, filename, linenum
|
||||
)
|
||||
stats = stats_report.stats
|
||||
stats[key] = {
|
||||
'filename': filename,
|
||||
'clsname': clsname,
|
||||
'linenum': linenum,
|
||||
'fileline': '%s:%d' % (filename, linenum),
|
||||
'fnname': fnname,
|
||||
'count': 0,
|
||||
'total time': 0,
|
||||
'average time': 0,
|
||||
}
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
time_beg = time.time()
|
||||
ret = fn(*args, **kwargs)
|
||||
time_end = time.time()
|
||||
time_delta = time_end - time_beg
|
||||
d = stats[key]
|
||||
d['count'] += 1
|
||||
d['total time'] += time_delta
|
||||
d['average time'] = d['total time'] / d['count']
|
||||
return ret
|
||||
return wrapped
|
||||
|
||||
|
||||
def stats_report():
|
||||
return
|
||||
|
||||
stats = stats_report.stats if hasattr(stats_report, 'stats') else dict()
|
||||
l = max(len(k) for k in stats)
|
||||
|
||||
def fmt(s):
|
||||
return s + ' ' * (l - len(s))
|
||||
|
||||
print()
|
||||
print('Call Statistics Report')
|
||||
|
||||
cols = [
|
||||
('class', 'clsname', '%s'),
|
||||
('func', 'fnname', '%s'),
|
||||
('location', 'fileline', '%s'),
|
||||
# ('line','linenum','% 10d'),
|
||||
('count', 'count', '% 8d'),
|
||||
('total (sec)', 'total time', '% 10.4f'),
|
||||
('avg (sec)', 'average time', '% 10.6f'),
|
||||
]
|
||||
data = [stats[k] for k in sorted(stats)]
|
||||
data = [[h] + [f % row[c] for row in data] for (h, c, f) in cols]
|
||||
colwidths = [max(len(d) for d in col) for col in data]
|
||||
totwidth = sum(colwidths) + len(colwidths) - 1
|
||||
|
||||
def rpad(s, l):
|
||||
return '%s%s' % (s, ' ' * (l - len(s)))
|
||||
|
||||
def printrow(i_row):
|
||||
row = [col[i_row] for col in data]
|
||||
print(' '.join(rpad(d, w) for (d, w) in zip(row, colwidths)))
|
||||
|
||||
printrow(0)
|
||||
print('-' * totwidth)
|
||||
for i in range(1, len(data[0])):
|
||||
printrow(i)
|
||||
|
||||
|
||||
|
||||
def add_cache(attr, default):
|
||||
def wrapper(fn):
|
||||
setattr(fn, attr, default)
|
||||
return fn
|
||||
return wrapper
|
||||
|
||||
|
||||
class LimitRecursion:
|
||||
def __init__(self, count, def_ret):
|
||||
self.count = count
|
||||
self.def_ret = def_ret
|
||||
self.calls = 0
|
||||
|
||||
def __call__(self, fn):
|
||||
def wrapped(*args, **kwargs):
|
||||
ret = self.def_ret
|
||||
if self.calls < self.count:
|
||||
try:
|
||||
self.calls += 1
|
||||
ret = fn(*args, **kwargs)
|
||||
finally:
|
||||
self.calls -= 1
|
||||
return ret
|
||||
return wrapped
|
||||
|
||||
|
||||
@add_cache('data', {'nested':0, 'last':None})
|
||||
def timed_call(label):
|
||||
def wrapper(fn):
|
||||
def wrapped(*args, **kwargs):
|
||||
data = timed_call.data
|
||||
if data['last']: print(data['last'])
|
||||
data['last'] = f'''{" " * data['nested']}Timing {label}'''
|
||||
data['nested'] += 1
|
||||
time_beg = time.time()
|
||||
ret = fn(*args, **kwargs)
|
||||
time_end = time.time()
|
||||
time_delta = time_end - time_beg
|
||||
if data['last']:
|
||||
print(f'''{data['last']}: {time_delta:0.4f}s''')
|
||||
data['last'] = None
|
||||
else:
|
||||
print(f'''{" " * data['nested']}{time_delta:0.4f}s''')
|
||||
data['nested'] -= 1
|
||||
return ret
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
# corrected bug in previous version of blender_version fn wrapper
|
||||
# https://github.com/CGCookie/retopoflow/commit/135746c7b4ee0052ad0c1842084b9ab983726b33#diff-d4260a97dcac93f76328dfaeb5c87688
|
||||
def blender_version_wrapper(op, ver):
|
||||
self = blender_version_wrapper
|
||||
if not hasattr(self, 'fns'):
|
||||
major, minor, rev = bpy.app.version
|
||||
self.blenderver = '%d.%02d' % (major, minor)
|
||||
self.fns = fns = {}
|
||||
self.ops = {
|
||||
'<': lambda v: self.blenderver < v,
|
||||
'>': lambda v: self.blenderver > v,
|
||||
'<=': lambda v: self.blenderver <= v,
|
||||
'==': lambda v: self.blenderver == v,
|
||||
'>=': lambda v: self.blenderver >= v,
|
||||
'!=': lambda v: self.blenderver != v,
|
||||
}
|
||||
|
||||
update_fn = self.ops[op](ver)
|
||||
def wrapit(fn):
|
||||
nonlocal self, update_fn
|
||||
fn_name = fn.__name__
|
||||
fns = self.fns
|
||||
error_msg = "Could not find appropriate function named %s for version Blender %s" % (fn_name, self.blenderver)
|
||||
|
||||
if update_fn: fns[fn_name] = fn
|
||||
|
||||
def callit(*args, **kwargs):
|
||||
nonlocal fns, fn_name, error_msg
|
||||
fn = fns.get(fn_name, None)
|
||||
assert fn, error_msg
|
||||
ret = fn(*args, **kwargs)
|
||||
return ret
|
||||
|
||||
return callit
|
||||
return wrapit
|
||||
|
||||
def only_in_blender_version(*args, ignore_others=False, ignore_return=None):
|
||||
self = only_in_blender_version
|
||||
if not hasattr(self, 'fns'):
|
||||
major, minor, rev = bpy.app.version
|
||||
self.blenderver = f'{major}.{minor:02d}'
|
||||
self.fns = {}
|
||||
self.ignores = {}
|
||||
self.ops = {
|
||||
'<': lambda v: self.blenderver < v,
|
||||
'>': lambda v: self.blenderver > v,
|
||||
'<=': lambda v: self.blenderver <= v,
|
||||
'==': lambda v: self.blenderver == v,
|
||||
'>=': lambda v: self.blenderver >= v,
|
||||
'!=': lambda v: self.blenderver != v,
|
||||
}
|
||||
self.re_blender_version = re.compile(r'^(?P<comparison><|<=|==|!=|>=|>) *(?P<version>\d\.\d+)$')
|
||||
|
||||
def ver(mver):
|
||||
major, minor = map(int, mver.split('.'))
|
||||
return f'{major}.{minor:02d}'
|
||||
|
||||
matches = [self.re_blender_version.match(arg) for arg in args]
|
||||
assert all(match is not None for match in matches), f'At least one arg did not match version comparison: {args}'
|
||||
results = [self.ops[match.group('comparison')](ver(match.group('version'))) for match in matches]
|
||||
version_matches = all(results)
|
||||
|
||||
def wrapit(fn):
|
||||
fn_name = fn.__name__
|
||||
|
||||
if version_matches:
|
||||
assert fn_name not in self.fns, f'Multiple functions {fn_name} match the Blender version {self.blenderver}'
|
||||
self.fns[fn_name] = fn
|
||||
|
||||
if ignore_others and fn_name not in self.ignores:
|
||||
self.ignores[fn_name] = ignore_return
|
||||
|
||||
@wraps(fn)
|
||||
def callit(*args, **kwargs):
|
||||
fn = self.fns.get(fn_name, None)
|
||||
if fn_name not in self.ignores:
|
||||
assert fn, f'Could not find appropriate function named {fn_name} for version Blender version {self.blenderver}'
|
||||
elif fn is None:
|
||||
return self.ignores[fn_name]
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return callit
|
||||
return wrapit
|
||||
|
||||
def warn_once(warning):
|
||||
def wrapper(fn):
|
||||
nonlocal warning
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal warning
|
||||
if warning:
|
||||
print(warning)
|
||||
warning = None
|
||||
return fn(*args, **kwargs)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
class PersistentOptions:
|
||||
class WrappedDict:
|
||||
def __init__(self, cls, filename, version, defaults, update_external):
|
||||
self._dirty = False
|
||||
self._last_save = time.time()
|
||||
self._write_delay = 2.0
|
||||
self._defaults = defaults
|
||||
self._update_external = update_external
|
||||
self._defaults['persistent options version'] = version
|
||||
self._dict = {}
|
||||
if filename:
|
||||
src = inspect.getsourcefile(cls)
|
||||
path = os.path.split(os.path.abspath(src))[0]
|
||||
self._fndb = os.path.join(path, filename)
|
||||
else:
|
||||
self._fndb = None
|
||||
self.read()
|
||||
if self._dict.get('persistent options version', None) != version:
|
||||
self.reset()
|
||||
self.update_external()
|
||||
def update_external(self):
|
||||
upd = self._update_external
|
||||
if upd:
|
||||
upd()
|
||||
def dirty(self):
|
||||
self._dirty = True
|
||||
self.update_external()
|
||||
def clean(self, force=False):
|
||||
if not force:
|
||||
if not self._dirty:
|
||||
return
|
||||
if time.time() < self._last_save + self._write_delay:
|
||||
return
|
||||
if self._fndb:
|
||||
json.dump(self._dict, open(self._fndb, 'wt'), indent=2, sort_keys=True)
|
||||
self._dirty = False
|
||||
self._last_save = time.time()
|
||||
def read(self):
|
||||
self._dict = {}
|
||||
if self._fndb and os.path.exists(self._fndb):
|
||||
try:
|
||||
self._dict = json.load(open(self._fndb, 'rt'))
|
||||
except Exception as e:
|
||||
print('Exception caught while trying to read options from "%s"' % self._fndb)
|
||||
print(str(e))
|
||||
for k in set(self._dict.keys()) - set(self._defaults.keys()):
|
||||
print('Deleting extraneous key "%s" from options' % k)
|
||||
del self._dict[k]
|
||||
self.update_external()
|
||||
self._dirty = False
|
||||
def keys(self):
|
||||
return self._defaults.keys()
|
||||
def reset(self):
|
||||
keys = list(self._dict.keys())
|
||||
for k in keys:
|
||||
del self._dict[k]
|
||||
self._dict['persistent options version'] = self['persistent options version']
|
||||
self.dirty()
|
||||
self.clean()
|
||||
def __getitem__(self, key):
|
||||
return self._dict[key] if key in self._dict else self._defaults[key]
|
||||
def __setitem__(self, key, val):
|
||||
assert key in self._defaults, 'Attempting to write "%s":"%s" to options, but key does not exist in defaults' % (str(key), str(val))
|
||||
if self[key] == val: return
|
||||
self._dict[key] = val
|
||||
self.dirty()
|
||||
self.clean()
|
||||
def gettersetter(self, key, fn_get_wrap=None, fn_set_wrap=None):
|
||||
if not fn_get_wrap: fn_get_wrap = lambda v: v
|
||||
if not fn_set_wrap: fn_set_wrap = lambda v: v
|
||||
oself = self
|
||||
class GetSet:
|
||||
def get(self):
|
||||
return fn_get_wrap(oself[key])
|
||||
def set(self, v):
|
||||
v = fn_set_wrap(v)
|
||||
if oself[key] != v:
|
||||
oself[key] = v
|
||||
return GetSet()
|
||||
|
||||
def __init__(self, filename=None, version=None):
|
||||
self._filename = filename
|
||||
self._version = version
|
||||
self._db = None
|
||||
|
||||
def __call__(self, cls):
|
||||
upd = getattr(cls, 'update', None)
|
||||
if upd:
|
||||
u = upd
|
||||
def wrap():
|
||||
def upd_wrap(*args, **kwargs):
|
||||
u(None)
|
||||
return upd_wrap
|
||||
upd = wrap()
|
||||
self._db = PersistentOptions.WrappedDict(cls, self._filename, self._version, cls.defaults, upd)
|
||||
db = self._db
|
||||
class WrappedClass:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._db = db
|
||||
self._def = cls.defaults
|
||||
def __getitem__(self, key):
|
||||
return self._db[key]
|
||||
def __setitem__(self, key, val):
|
||||
self._db[key] = val
|
||||
def keys(self):
|
||||
return self._db.keys()
|
||||
def reset(self):
|
||||
self._db.reset()
|
||||
def clean(self):
|
||||
self._db.clean()
|
||||
def gettersetter(self, key, fn_get_wrap=None, fn_set_wrap=None):
|
||||
return self._db.gettersetter(key, fn_get_wrap=fn_get_wrap, fn_set_wrap=fn_set_wrap)
|
||||
return WrappedClass
|
||||
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import time
|
||||
import ctypes
|
||||
import random
|
||||
from typing import List
|
||||
import traceback
|
||||
import functools
|
||||
import contextlib
|
||||
import urllib.request
|
||||
from functools import wraps
|
||||
from itertools import chain
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
from bpy.types import BoolProperty
|
||||
from mathutils import Matrix, Vector
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d
|
||||
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_origin_3d
|
||||
|
||||
from .blender import bversion, get_path_from_addon_root, get_path_from_addon_common
|
||||
from .blender_cursors import Cursors
|
||||
from .blender_preferences import get_preferences
|
||||
from .debug import dprint, debugger
|
||||
from .decorators import blender_version_wrapper, add_cache, only_in_blender_version
|
||||
from .fontmanager import FontManager as fm
|
||||
from .functools import find_fns
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Point2D, Vec2D, Point, Ray, Direction, mid, Color, Normal, Frame
|
||||
from .profiler import profiler
|
||||
from .utils import iter_pairs
|
||||
from . import gpustate
|
||||
|
||||
|
||||
class Drawing:
|
||||
_instance = None
|
||||
_dpi_mult = 1
|
||||
_custom_dpi_mult = 1
|
||||
_prefs = get_preferences()
|
||||
_error_check = True
|
||||
_error_count = 0
|
||||
_error_limit = 10 # after this many check errors, no more will be reported to console
|
||||
|
||||
@staticmethod
|
||||
def get_custom_dpi_mult():
|
||||
return Drawing._custom_dpi_mult
|
||||
@staticmethod
|
||||
def set_custom_dpi_mult(v):
|
||||
Drawing._custom_dpi_mult = v
|
||||
Drawing.update_dpi()
|
||||
|
||||
@staticmethod
|
||||
def update_dpi():
|
||||
# print(f'view.ui_scale={Drawing._prefs.view.ui_scale}, system.ui_scale={Drawing._prefs.system.ui_scale}, system.dpi={Drawing._prefs.system.dpi}')
|
||||
Drawing._dpi_mult = (
|
||||
1.0
|
||||
* Drawing._custom_dpi_mult
|
||||
# * Drawing._prefs.view.ui_scale
|
||||
* max(0.25, Drawing._prefs.system.ui_scale) # math.floor(Drawing._prefs.system.ui_scale))
|
||||
# * (72.0 / Drawing._prefs.system.dpi)
|
||||
# * Drawing._prefs.system.pixel_size
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def initialize():
|
||||
Drawing.update_dpi()
|
||||
if Globals.is_set('drawing'): return
|
||||
Drawing._creating = True
|
||||
Globals.set(Drawing())
|
||||
del Drawing._creating
|
||||
Drawing._instance = Globals.drawing
|
||||
|
||||
def __init__(self):
|
||||
assert hasattr(self, '_creating'), "Do not instantiate directly. Use Drawing.get_instance()"
|
||||
|
||||
self.area,self.space,self.rgn,self.r3d,self.window = None,None,None,None,None
|
||||
# self.font_id = 0
|
||||
self.last_font_key = None
|
||||
self.fontid = 0
|
||||
self.fontsize = None
|
||||
self.fontsize_scaled = None
|
||||
self.line_cache = {}
|
||||
self.size_cache = {}
|
||||
self.set_font_size(12)
|
||||
self._pixel_matrix = None
|
||||
|
||||
def set_region(self, area, space, rgn, r3d, window):
|
||||
self.area = area
|
||||
self.space = space
|
||||
self.rgn = rgn
|
||||
self.r3d = r3d
|
||||
self.window = window
|
||||
|
||||
@staticmethod
|
||||
def set_cursor(cursor): Cursors.set(cursor)
|
||||
|
||||
def scale(self, s): return s * self._dpi_mult if s is not None else None
|
||||
def unscale(self, s): return s / self._dpi_mult if s is not None else None
|
||||
def get_dpi_mult(self): return self._dpi_mult
|
||||
def get_pixel_size(self): return self._pixel_size
|
||||
def line_width(self, width): gpustate.line_width(max(1, self.scale(width)))
|
||||
def point_size(self, size): gpustate.point_size(max(1, self.scale(size)))
|
||||
|
||||
def set_font_color(self, fontid, color):
|
||||
fm.color(color, fontid=fontid)
|
||||
|
||||
def set_font_size(self, fontsize, fontid=None, force=False):
|
||||
if fontid is None: fontid = fm._last_fontid
|
||||
else: fontid = fm.load(fontid)
|
||||
fontsize_prev = self.fontsize
|
||||
fontsize, fontsize_scaled = int(fontsize), int(int(fontsize) * self._dpi_mult)
|
||||
cache_key = (fontid, fontsize_scaled)
|
||||
if self.last_font_key == cache_key and not force: return fontsize_prev
|
||||
fm.size(fontsize_scaled, fontid=fontid)
|
||||
if cache_key not in self.line_cache:
|
||||
# cache away useful details about font (line height, line base)
|
||||
# dprint('Caching new scaled font size:', cache_key)
|
||||
pass
|
||||
all_chars = ''.join([
|
||||
'abcdefghijklmnopqrstuvwxyz',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'0123456789',
|
||||
'!@#$%%^&*()`~[}{]/?=+\\|-_\'",<.>',
|
||||
'ΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩω',
|
||||
])
|
||||
all_caps = all_chars.upper()
|
||||
self.line_cache[cache_key] = {
|
||||
'line height': math.ceil(fm.dimensions(all_chars, fontid=fontid)[1] + self.scale(4)),
|
||||
'line base': math.ceil(fm.dimensions(all_caps, fontid=fontid)[1]),
|
||||
}
|
||||
info = self.line_cache[cache_key]
|
||||
self.line_height = info['line height']
|
||||
self.line_base = info['line base']
|
||||
self.fontid = fontid
|
||||
self.fontsize = fontsize
|
||||
self.fontsize_scaled = fontsize_scaled
|
||||
self.last_font_key = cache_key
|
||||
|
||||
return fontsize_prev
|
||||
|
||||
def get_text_size_info(self, text, item, fontsize=None, fontid=None):
|
||||
if fontsize or fontid: size_prev = self.set_font_size(fontsize, fontid=fontid)
|
||||
|
||||
if text is None: text, lines = '', []
|
||||
elif type(text) is list: text, lines = '\n'.join(text), text
|
||||
else: text, lines = text, text.splitlines()
|
||||
|
||||
fontid = fm.load(fontid)
|
||||
key = (text, self.fontsize_scaled, fontid)
|
||||
# key = (text, self.fontsize_scaled, self.font_id)
|
||||
if key not in self.size_cache:
|
||||
d = {}
|
||||
if not text:
|
||||
d['width'] = 0
|
||||
d['height'] = 0
|
||||
d['line height'] = self.line_height
|
||||
else:
|
||||
get_width = lambda t: math.ceil(fm.dimensions(t, fontid=fontid)[0])
|
||||
get_height = lambda t: math.ceil(fm.dimensions(t, fontid=fontid)[1])
|
||||
d['width'] = max(get_width(l) for l in lines)
|
||||
d['height'] = get_height(text)
|
||||
d['line height'] = self.line_height * len(lines)
|
||||
self.size_cache[key] = d
|
||||
if False:
|
||||
print('')
|
||||
print('--------------------------------------')
|
||||
print('> computed new size')
|
||||
print('> key: %s' % str(key))
|
||||
print('> size: %s' % str(d))
|
||||
print('--------------------------------------')
|
||||
print('')
|
||||
if fontsize: self.set_font_size(size_prev, fontid=fontid)
|
||||
return self.size_cache[key][item]
|
||||
|
||||
def get_text_width(self, text, fontsize=None, fontid=None):
|
||||
return self.get_text_size_info(text, 'width', fontsize=fontsize, fontid=fontid)
|
||||
def get_text_height(self, text, fontsize=None, fontid=None):
|
||||
return self.get_text_size_info(text, 'height', fontsize=fontsize, fontid=fontid)
|
||||
def get_line_height(self, text=None, fontsize=None, fontid=None):
|
||||
return self.get_text_size_info(text, 'line height', fontsize=fontsize, fontid=fontid)
|
||||
|
||||
def set_clipping(self, xmin, ymin, xmax, ymax, fontid=None):
|
||||
fm.clipping((xmin, ymin), (xmax, ymax), fontid=fontid)
|
||||
# blf.clipping(self.font_id, xmin, ymin, xmax, ymax)
|
||||
self.enable_clipping()
|
||||
def enable_clipping(self, fontid=None):
|
||||
fm.enable_clipping(fontid=fontid)
|
||||
# blf.enable(self.font_id, blf.CLIPPING)
|
||||
def disable_clipping(self, fontid=None):
|
||||
fm.disable_clipping(fontid=fontid)
|
||||
# blf.disable(self.font_id, blf.CLIPPING)
|
||||
|
||||
def text_color_set(self, color, fontid):
|
||||
if color is not None: fm.color(color, fontid=fontid)
|
||||
|
||||
def text_draw2D(self, text, pos:Point2D, *, color=None, dropshadow=None, fontsize=None, fontid=None, lineheight=True):
|
||||
if fontsize: size_prev = self.set_font_size(fontsize, fontid=fontid)
|
||||
|
||||
lines = str(text).splitlines()
|
||||
l,t = round(pos[0]),round(pos[1])
|
||||
lh,lb = self.line_height,self.line_base
|
||||
|
||||
if dropshadow:
|
||||
self.text_draw2D(text, (l+1,t-1), color=dropshadow, fontsize=fontsize, fontid=fontid, lineheight=lineheight)
|
||||
|
||||
gpustate.blend('ALPHA')
|
||||
self.text_color_set(color, fontid)
|
||||
for line in lines:
|
||||
fm.draw(line, xyz=(l, t - lb, 0), fontid=fontid)
|
||||
t -= lh if lineheight else self.get_text_height(line)
|
||||
|
||||
if fontsize: self.set_font_size(size_prev, fontid=fontid)
|
||||
|
||||
def text_draw2D_simple(self, text, pos:Point2D):
|
||||
l,t = round(pos[0]),round(pos[1])
|
||||
lb = self.line_base
|
||||
fm.draw_simple(text, xyz=(l, t - lb, 0))
|
||||
|
||||
|
||||
def get_mvp_matrix(self, view3D=True):
|
||||
'''
|
||||
if view3D == True: returns MVP for 3D view
|
||||
else: returns MVP for pixel view
|
||||
TODO: compute separate M,V,P matrices
|
||||
'''
|
||||
if not self.r3d: return None
|
||||
if view3D:
|
||||
# 3D view
|
||||
return self.r3d.perspective_matrix
|
||||
else:
|
||||
# pixel view
|
||||
return self.get_pixel_matrix()
|
||||
|
||||
mat_model = Matrix()
|
||||
mat_view = Matrix()
|
||||
mat_proj = Matrix()
|
||||
|
||||
view_loc = self.r3d.view_location # vec
|
||||
view_rot = self.r3d.view_rotation # quat
|
||||
view_per = self.r3d.view_perspective # 'PERSP' or 'ORTHO'
|
||||
|
||||
return mat_model,mat_view,mat_proj
|
||||
|
||||
def get_pixel_matrix_list(self):
|
||||
if not self.r3d: return None
|
||||
x,y = self.rgn.x,self.rgn.y
|
||||
w,h = self.rgn.width,self.rgn.height
|
||||
ww,wh = self.window.width,self.window.height
|
||||
return [[2/w,0,0,-1], [0,2/h,0,-1], [0,0,1,0], [0,0,0,1]]
|
||||
|
||||
def load_pixel_matrix(self, m):
|
||||
self._pixel_matrix = m
|
||||
|
||||
@add_cache('_cache', {'w':-1, 'h':-1, 'm':None})
|
||||
def get_pixel_matrix(self):
|
||||
'''
|
||||
returns MVP for pixel view
|
||||
TODO: compute separate M,V,P matrices
|
||||
'''
|
||||
if not self.r3d: return None
|
||||
if self._pixel_matrix: return self._pixel_matrix
|
||||
w,h = self.rgn.width,self.rgn.height
|
||||
cache = self.get_pixel_matrix._cache
|
||||
if cache['w'] != w or cache['h'] != h:
|
||||
mx, my, mw, mh = -1, -1, 2 / w, 2 / h
|
||||
cache['w'],cache['h'] = w,h
|
||||
cache['m'] = Matrix([
|
||||
[ mw, 0, 0, mx],
|
||||
[ 0, mh, 0, my],
|
||||
[ 0, 0, 1, 0],
|
||||
[ 0, 0, 0, 1]
|
||||
])
|
||||
return cache['m']
|
||||
|
||||
def get_view_matrix_list(self):
|
||||
return list(self.get_view_matrix()) if self.r3d else None
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.r3d.perspective_matrix if self.r3d else None
|
||||
|
||||
def get_view_version(self):
|
||||
if not self.r3d: return None
|
||||
return Hasher(self.r3d.view_matrix, self.space.lens, self.r3d.view_distance)
|
||||
|
||||
@staticmethod
|
||||
def glCheckError(title, **kwargs):
|
||||
return gpustate.get_glerror(title, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def glCheckError_wrap(title, *, stop_on_error=False):
|
||||
if Drawing.glCheckError(f'addon common: pre {title}') and stop_on_error: return True
|
||||
yield None
|
||||
if Drawing.glCheckError(f'addon common: post {title}') and stop_on_error: return True
|
||||
return False
|
||||
|
||||
def get_view_origin(self, *, orthographic_distance=1000):
|
||||
focus = self.r3d.view_location
|
||||
rot = self.r3d.view_rotation
|
||||
dist = self.r3d.view_distance if self.r3d.is_perspective else orthographic_distance
|
||||
return focus + (rot @ Vector((0, 0, dist)))
|
||||
|
||||
# # the following fails in weird ways when in orthographic projection
|
||||
# center = Point2D((self.area.width / 2, self.area.height / 2))
|
||||
# return Point(region_2d_to_origin_3d(self.rgn, self.r3d, center))
|
||||
|
||||
def Point2D_to_Ray(self, p2d):
|
||||
o = Point(region_2d_to_origin_3d(self.rgn, self.r3d, p2d))
|
||||
d = Direction(region_2d_to_vector_3d(self.rgn, self.r3d, p2d))
|
||||
return Ray(o, d)
|
||||
|
||||
def Point_to_Point2D(self, p3d):
|
||||
return Point2D(location_3d_to_region_2d(self.rgn, self.r3d, p3d))
|
||||
|
||||
@blender_version_wrapper('>=', '2.80')
|
||||
def draw2D_point(self, pt:Point2D, color:Color, *, radius=1, border=0, borderColor=None):
|
||||
radius = self.scale(radius)
|
||||
border = self.scale(border)
|
||||
if borderColor is None: borderColor = (0,0,0,0)
|
||||
shader_2D_point.bind()
|
||||
ubos_2D_point.options.screensize = (self.area.width, self.area.height, 0, 0)
|
||||
ubos_2D_point.options.mvpmatrix = self.get_pixel_matrix()
|
||||
ubos_2D_point.options.radius_border = (radius, border, 0, 0)
|
||||
ubos_2D_point.options.color = color
|
||||
ubos_2D_point.options.colorBorder = borderColor
|
||||
ubos_2D_point.options.center = (*pt, 0, 1)
|
||||
ubos_2D_point.update_shader()
|
||||
batch_2D_point.draw(shader_2D_point)
|
||||
gpu.shader.unbind()
|
||||
|
||||
@blender_version_wrapper('>=', '2.80')
|
||||
def draw2D_points(self, pts:[Point2D], color:Color, *, radius=1, border=0, borderColor=None):
|
||||
radius = self.scale(radius)
|
||||
border = self.scale(border)
|
||||
if borderColor is None: borderColor = (0,0,0,0)
|
||||
shader_2D_point.bind()
|
||||
ubos_2D_point.options.screensize = (self.area.width, self.area.height, 0, 0)
|
||||
ubos_2D_point.options.mvpmatrix = self.get_pixel_matrix()
|
||||
ubos_2D_point.options.radius_border = (radius, border, 0, 0)
|
||||
ubos_2D_point.options.color = color
|
||||
ubos_2D_point.options.colorBorder = borderColor
|
||||
for pt in pts:
|
||||
ubos_2D_point.options.center = (*pt, 0, 1)
|
||||
ubos_2D_point.update_shader()
|
||||
batch_2D_point.draw(shader_2D_point)
|
||||
gpu.shader.unbind()
|
||||
|
||||
# draw line segment in screen space
|
||||
def draw2D_line(self, p0:Point2D, p1:Point2D, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
||||
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
||||
width = self.scale(width)
|
||||
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
||||
offset = self.scale(offset)
|
||||
shader_2D_lineseg.bind()
|
||||
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
||||
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height, 0, 0)
|
||||
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
||||
ubos_2D_lineseg.options.color0 = color0
|
||||
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
||||
ubos_2D_lineseg.options.color1 = color1
|
||||
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
gpu.shader.unbind()
|
||||
|
||||
def draw2D_lines(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
||||
self.glCheckError('starting draw2D_lines')
|
||||
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
||||
width = self.scale(width)
|
||||
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
||||
offset = self.scale(offset)
|
||||
shader_2D_lineseg.bind()
|
||||
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
||||
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height, 0, 0)
|
||||
ubos_2D_lineseg.options.color0 = color0
|
||||
ubos_2D_lineseg.options.color1 = color1
|
||||
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
||||
for i in range(len(points)//2):
|
||||
p0,p1 = points[i*2:i*2+2]
|
||||
if p0 is None or p1 is None: continue
|
||||
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
gpu.shader.unbind()
|
||||
self.glCheckError('done with draw2D_lines')
|
||||
|
||||
def draw3D_lines(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
||||
self.glCheckError('starting draw3D_lines')
|
||||
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
||||
width = self.scale(width)
|
||||
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
||||
offset = self.scale(offset)
|
||||
shader_2D_lineseg.bind()
|
||||
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height)
|
||||
ubos_2D_lineseg.options.color0 = color0
|
||||
ubos_2D_lineseg.options.color1 = color1
|
||||
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
||||
ubos_2D_lineseg.options.MVPMatrix = self.get_view_matrix()
|
||||
for i in range(len(points)//2):
|
||||
p0,p1 = points[i*2:i*2+2]
|
||||
if p0 is None or p1 is None: continue
|
||||
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
gpu.shader.unbind()
|
||||
self.glCheckError('done with draw3D_lines')
|
||||
|
||||
def draw2D_linestrip(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
||||
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
||||
width = self.scale(width)
|
||||
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
||||
offset = self.scale(offset)
|
||||
shader_2D_lineseg.bind()
|
||||
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
||||
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height)
|
||||
ubos_2D_lineseg.options.color0 = color0
|
||||
ubos_2D_lineseg.options.color1 = color1
|
||||
for p0,p1 in iter_pairs(points, False):
|
||||
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
||||
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
offset += (p1 - p0).length
|
||||
gpu.shader.unbind()
|
||||
|
||||
# draw circle in screen space
|
||||
def draw2D_circle(self, center:Point2D, radius:float, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
||||
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
||||
radius = self.scale(radius)
|
||||
width = self.scale(width)
|
||||
stipple = [self.scale(v) for v in stipple] if stipple else [1,0]
|
||||
offset = self.scale(offset)
|
||||
shader_2D_circle.bind()
|
||||
ubos_2D_circle.options.MVPMatrix = self.get_pixel_matrix()
|
||||
ubos_2D_circle.options.screensize = (self.area.width, self.area.height, 0.0, 0.0)
|
||||
ubos_2D_circle.options.center = (center.x, center.y, 0.0, 0.0)
|
||||
ubos_2D_circle.options.color0 = color0
|
||||
ubos_2D_circle.options.color1 = color1
|
||||
ubos_2D_circle.options.radius_width = (radius, width, 0.0, 0.0)
|
||||
ubos_2D_circle.options.stipple_data = (*stipple, offset, 0.0)
|
||||
ubos_2D_circle.update_shader()
|
||||
batch_2D_circle.draw(shader_2D_circle)
|
||||
gpu.shader.unbind()
|
||||
|
||||
def draw3D_circle(self, center:Point, radius:float, color:Color, *, width=1, n:Normal=None, x:Direction=None, y:Direction=None, depth_near=0, depth_far=1):
|
||||
assert n is not None or x is not None or y is not None, 'Must specify at least one of n,x,y'
|
||||
f = Frame(o=center, x=x, y=y, z=n)
|
||||
radius = self.scale(radius)
|
||||
width = self.scale(width)
|
||||
shader_3D_circle.bind()
|
||||
ubos_3D_circle.options.MVPMatrix = self.get_view_matrix()
|
||||
ubos_3D_circle.options.screensize = (self.area.width, self.area.height, 0.0, 0.0)
|
||||
ubos_3D_circle.options.center = f.o
|
||||
ubos_3D_circle.options.color = color
|
||||
ubos_3D_circle.options.plane_x = f.x
|
||||
ubos_3D_circle.options.plane_y = f.y
|
||||
ubos_3D_circle.options.settings = (radius, width, depth_near, depth_far)
|
||||
ubos_3D_circle.update_shader()
|
||||
batch_3D_circle.draw(shader_3D_circle)
|
||||
gpu.shader.unbind()
|
||||
|
||||
def draw3D_triangles(self, points:[Point], colors:[Color]):
|
||||
self.glCheckError('starting draw3D_triangles')
|
||||
shader_3D_triangle.bind()
|
||||
ubos_3D_triangle.options.MVPMatrix = self.get_view_matrix()
|
||||
for i in range(0, len(points), 3):
|
||||
p0,p1,p2 = points[i:i+3]
|
||||
c0,c1,c2 = colors[i:i+3]
|
||||
if p0 is None or p1 is None or p2 is None: continue
|
||||
if c0 is None or c1 is None or c2 is None: continue
|
||||
ubos_3D_triangle.options.pos0 = p0
|
||||
ubos_3D_triangle.options.color0 = c0
|
||||
ubos_3D_triangle.options.pos1 = p1
|
||||
ubos_3D_triangle.options.color1 = c1
|
||||
ubos_3D_triangle.options.pos2 = p2
|
||||
ubos_3D_triangle.options.color2 = c2
|
||||
ubos_3D_triangle.update_shader()
|
||||
batch_3D_triangle.draw(shader_3D_triangle)
|
||||
gpu.shader.unbind()
|
||||
self.glCheckError('done with draw3D_triangles')
|
||||
|
||||
@contextlib.contextmanager
|
||||
def draw(self, draw_type:"CC_DRAW"):
|
||||
assert getattr(self, '_draw', None) is None, 'Cannot nest Drawing.draw calls'
|
||||
self._draw = draw_type
|
||||
self.glCheckError('starting draw')
|
||||
try:
|
||||
draw_type.begin()
|
||||
yield draw_type
|
||||
draw_type.end()
|
||||
except Exception as e:
|
||||
print('Exception caught while in Drawing.draw with %s' % str(draw_type))
|
||||
debugger.print_exception()
|
||||
self.glCheckError('done with draw')
|
||||
self._draw = None
|
||||
|
||||
if not bpy.app.background:
|
||||
Drawing.glCheckError(f'pre-init check: Drawing')
|
||||
Drawing.initialize()
|
||||
Drawing.glCheckError(f'post-init check: Drawing')
|
||||
|
||||
|
||||
|
||||
|
||||
if not bpy.app.background and bpy.app.version >= (3, 2, 0):
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
# https://docs.blender.org/api/blender2.8/gpu.html#triangle-with-custom-shader
|
||||
|
||||
def create_shader(fn_glsl):
|
||||
path_glsl = get_path_from_addon_common('common', 'shaders', fn_glsl)
|
||||
txt = open(path_glsl, 'rt').read()
|
||||
vert_source, frag_source = gpustate.shader_parse_string(txt)
|
||||
try:
|
||||
Drawing.glCheckError(f'pre-compile check: {fn_glsl}')
|
||||
ret = gpustate.gpu_shader(f'drawing {fn_glsl}', vert_source, frag_source)
|
||||
Drawing.glCheckError(f'post-compile check: {fn_glsl}')
|
||||
return ret
|
||||
except Exception as e:
|
||||
print('ERROR WHILE COMPILING SHADER %s' % fn_glsl)
|
||||
assert False
|
||||
|
||||
Drawing.glCheckError(f'Pre-compile check: point, lineseg, circle, triangle shaders')
|
||||
|
||||
# 2D point
|
||||
shader_2D_point, ubos_2D_point = create_shader('point_2D.glsl')
|
||||
batch_2D_point = batch_for_shader(shader_2D_point, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]})
|
||||
|
||||
# 2D line segment
|
||||
shader_2D_lineseg, ubos_2D_lineseg = create_shader('lineseg_2D.glsl')
|
||||
batch_2D_lineseg = batch_for_shader(shader_2D_lineseg, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]})
|
||||
|
||||
# 2D circle
|
||||
shader_2D_circle, ubos_2D_circle = create_shader('circle_2D.glsl')
|
||||
# create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1)
|
||||
cnt = 100
|
||||
pts = [
|
||||
p for i0 in range(cnt)
|
||||
for p in [
|
||||
((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1),
|
||||
((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1),
|
||||
]
|
||||
]
|
||||
batch_2D_circle = batch_for_shader(shader_2D_circle, 'TRIS', {"pos": pts})
|
||||
|
||||
# 3D circle
|
||||
shader_3D_circle, ubos_3D_circle = create_shader('circle_3D.glsl')
|
||||
# create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1)
|
||||
cnt = 100
|
||||
pts = [
|
||||
p for i0 in range(cnt)
|
||||
for p in [
|
||||
((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1),
|
||||
((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1),
|
||||
]
|
||||
]
|
||||
batch_3D_circle = batch_for_shader(shader_3D_circle, 'TRIS', {"pos": pts})
|
||||
|
||||
# 3D triangle
|
||||
shader_3D_triangle, ubos_3D_triangle = create_shader('triangle_3D.glsl')
|
||||
batch_3D_triangle = batch_for_shader(shader_3D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]})
|
||||
|
||||
# 3D triangle
|
||||
shader_2D_triangle, ubos_2D_triangle = create_shader('triangle_2D.glsl')
|
||||
batch_2D_triangle = batch_for_shader(shader_2D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]})
|
||||
|
||||
Drawing.glCheckError(f'Compiled point, lineseg, circle shaders')
|
||||
|
||||
|
||||
######################################################################################################
|
||||
# The following classes mimic the immediate mode for (old-school way of) drawing geometry
|
||||
# glBegin(GL_TRIANGLES)
|
||||
# glColor3f(p)
|
||||
# glVertex3f(p)
|
||||
# glEnd()
|
||||
|
||||
class CC_DRAW:
|
||||
_point_size:float = 1
|
||||
_line_width:float = 1
|
||||
_border_width:float = 0
|
||||
_border_color:Color = Color((0, 0, 0, 0))
|
||||
_stipple_pattern:List[float] = [1,0]
|
||||
_stipple_offset:float = 0
|
||||
_stipple_color:Color = Color((0, 0, 0, 0))
|
||||
|
||||
_default_color = Color((1, 1, 1, 1))
|
||||
_default_point_size = 1
|
||||
_default_line_width = 1
|
||||
_default_border_width = 0
|
||||
_default_border_color = Color((0, 0, 0, 0))
|
||||
_default_stipple_pattern = [1,0]
|
||||
_default_stipple_color = Color((0, 0, 0, 0))
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
s = Drawing._instance.scale
|
||||
CC_DRAW._point_size = s(CC_DRAW._default_point_size)
|
||||
CC_DRAW._line_width = s(CC_DRAW._default_line_width)
|
||||
CC_DRAW._border_width = s(CC_DRAW._default_border_width)
|
||||
CC_DRAW._border_color = CC_DRAW._default_border_color
|
||||
CC_DRAW._stipple_offset = 0
|
||||
CC_DRAW._stipple_pattern = [s(v) for v in CC_DRAW._default_stipple_pattern]
|
||||
CC_DRAW._stipple_color = CC_DRAW._default_stipple_color
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def update(cls): pass
|
||||
|
||||
@classmethod
|
||||
def point_size(cls, size):
|
||||
s = Drawing._instance.scale
|
||||
CC_DRAW._point_size = s(size)
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def line_width(cls, width):
|
||||
s = Drawing._instance.scale
|
||||
CC_DRAW._line_width = s(width)
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def border(cls, *, width=None, color=None):
|
||||
s = Drawing._instance.scale
|
||||
if width is not None:
|
||||
CC_DRAW._border_width = s(width)
|
||||
if color is not None:
|
||||
CC_DRAW._border_color = color
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def stipple(cls, *, pattern=None, offset=None, color=None):
|
||||
s = Drawing._instance.scale
|
||||
if pattern is not None:
|
||||
CC_DRAW._stipple_pattern = [s(v) for v in pattern]
|
||||
if offset is not None:
|
||||
CC_DRAW._stipple_offset = s(offset)
|
||||
if color is not None:
|
||||
CC_DRAW._stipple_color = color
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def end(cls):
|
||||
gpu.shader.unbind()
|
||||
|
||||
if not bpy.app.background:
|
||||
CC_DRAW.reset()
|
||||
|
||||
|
||||
class CC_2D_POINTS(CC_DRAW):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
shader_2D_point.bind()
|
||||
ubos_2D_point.options.mvpmatrix = Drawing._instance.get_pixel_matrix()
|
||||
ubos_2D_point.options.screensize = (Drawing._instance.area.width, Drawing._instance.area.height, 0, 0)
|
||||
ubos_2D_point.options.color = cls._default_color
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
ubos_2D_point.options.radius_border = (cls._point_size, cls._border_width, 0, 0)
|
||||
ubos_2D_point.options.colorBorder = cls._border_color
|
||||
|
||||
@classmethod
|
||||
def color(cls, c:Color):
|
||||
ubos_2D_point.options.color = c
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if p:
|
||||
ubos_2D_point.options.center = (*p, 0, 1)
|
||||
ubos_2D_point.options.update_shader()
|
||||
batch_2D_point.draw(shader_2D_point)
|
||||
|
||||
|
||||
class CC_2D_LINES(CC_DRAW):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
shader_2D_lineseg.bind()
|
||||
mvpmatrix = Drawing._instance.get_pixel_matrix()
|
||||
ubos_2D_lineseg.options.MVPMatrix = mvpmatrix
|
||||
ubos_2D_lineseg.options.screensize = (Drawing._instance.area.width, Drawing._instance.area.height, 0, 0)
|
||||
ubos_2D_lineseg.options.color0 = cls._default_color
|
||||
cls.stipple(offset=0)
|
||||
cls._c = 0
|
||||
cls._last_p = None
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
ubos_2D_lineseg.options.color1 = cls._stipple_color
|
||||
ubos_2D_lineseg.options.stipple_width = (cls._stipple_pattern[0], cls._stipple_pattern[1], cls._stipple_offset, cls._line_width)
|
||||
|
||||
@classmethod
|
||||
def color(cls, c:Color):
|
||||
ubos_2D_lineseg.options.color0 = c
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if p: ubos_2D_lineseg.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
||||
cls._c = (cls._c + 1) % 2
|
||||
if cls._c == 0 and cls._last_p and p:
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
cls._last_p = p
|
||||
|
||||
class CC_2D_LINE_STRIP(CC_2D_LINES):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
super().begin()
|
||||
cls._last_p = None
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if cls._last_p is None:
|
||||
cls._last_p = p
|
||||
else:
|
||||
if cls._last_p and p:
|
||||
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*p, 0, 1)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
cls._last_p = p
|
||||
|
||||
class CC_2D_LINE_LOOP(CC_2D_LINES):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
super().begin()
|
||||
cls._first_p = None
|
||||
cls._last_p = None
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if cls._first_p is None:
|
||||
cls._first_p = cls._last_p = p
|
||||
else:
|
||||
if cls._last_p and p:
|
||||
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*p, 0, 1)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
cls._last_p = p
|
||||
|
||||
@classmethod
|
||||
def end(cls):
|
||||
if cls._last_p and cls._first_p:
|
||||
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
||||
ubos_2D_lineseg.options.pos1 = (*cls._first_p, 0, 1)
|
||||
ubos_2D_lineseg.update_shader()
|
||||
batch_2D_lineseg.draw(shader_2D_lineseg)
|
||||
super().end()
|
||||
|
||||
|
||||
class CC_2D_TRIANGLES(CC_DRAW):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
shader_2D_triangle.bind()
|
||||
#shader_2D_triangle.uniform_float('screensize', (Drawing._instance.area.width, Drawing._instance.area.height))
|
||||
ubos_2D_triangle.options.MVPMatrix = Drawing._instance.get_pixel_matrix()
|
||||
cls._c = 0
|
||||
cls._last_color = None
|
||||
cls._last_p0 = None
|
||||
cls._last_p1 = None
|
||||
|
||||
@classmethod
|
||||
def color(cls, c:Color):
|
||||
if c is None: return
|
||||
ubos_2D_triangle.options.assign(f'color{cls._c}', c)
|
||||
cls._last_color = c
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if p: ubos_2D_triangle.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
||||
cls._c = (cls._c + 1) % 3
|
||||
if cls._c == 0 and p and cls._last_p0 and cls._last_p1:
|
||||
ubos_2D_triangle.update_shader()
|
||||
batch_2D_triangle.draw(shader_2D_triangle)
|
||||
cls.color(cls._last_color)
|
||||
cls._last_p1 = cls._last_p0
|
||||
cls._last_p0 = p
|
||||
|
||||
class CC_2D_TRIANGLE_FAN(CC_DRAW):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
shader_2D_triangle.bind()
|
||||
ubos_2D_triangle.options.MVPMatrix = Drawing._instance.get_pixel_matrix()
|
||||
cls._c = 0
|
||||
cls._last_color = None
|
||||
cls._first_p = None
|
||||
cls._last_p = None
|
||||
cls._is_first = True
|
||||
|
||||
@classmethod
|
||||
def color(cls, c:Color):
|
||||
if c is None: return
|
||||
ubos_2D_triangle.options.assign(f'color{cls._c}', c)
|
||||
cls._last_color = c
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point2D):
|
||||
if p: ubos_2D_triangle.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
||||
cls._c += 1
|
||||
if cls._c == 3:
|
||||
if p and cls._first_p and cls._last_p:
|
||||
ubos_2D_triangle.update_shader()
|
||||
batch_2D_triangle.draw(shader_2D_triangle)
|
||||
cls._c = 1
|
||||
cls.color(cls._last_color)
|
||||
if cls._is_first:
|
||||
cls._first_p = p
|
||||
cls._is_first = False
|
||||
else: cls._last_p = p
|
||||
|
||||
class CC_3D_TRIANGLES(CC_DRAW):
|
||||
@classmethod
|
||||
def begin(cls):
|
||||
shader_3D_triangle.bind()
|
||||
ubos_3D_triangle.options.MVPMatrix = Drawing._instance.get_view_matrix()
|
||||
cls._c = 0
|
||||
cls._last_color = None
|
||||
cls._last_p0 = None
|
||||
cls._last_p1 = None
|
||||
|
||||
@classmethod
|
||||
def color(cls, c:Color):
|
||||
if c is None: return
|
||||
ubos_3D_triangle.options.assign(f'color{cls._c}', c)
|
||||
cls._last_color = c
|
||||
|
||||
@classmethod
|
||||
def vertex(cls, p:Point):
|
||||
if p: ubos_3D_triangle.options.assign(f'pos{cls._c}', p)
|
||||
cls._c = (cls._c + 1) % 3
|
||||
if cls._c == 0 and p and cls._last_p0 and cls._last_p1:
|
||||
ubos_3D_triangle.update_shader()
|
||||
batch_3D_triangle.draw(shader_3D_triangle)
|
||||
cls.color(cls._last_color)
|
||||
cls._last_p1 = cls._last_p0
|
||||
cls._last_p0 = p
|
||||
|
||||
|
||||
class DrawCallbacks:
|
||||
@staticmethod
|
||||
def on_draw(mode):
|
||||
def wrapper(fn):
|
||||
nonlocal mode
|
||||
assert mode in {'predraw', 'pre3d', 'post3d', 'post2d'}, f'DrawCallbacks: unexpected draw mode {mode} for {fn}'
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f'DrawCallbacks: caught exception in on_draw with {fn}')
|
||||
debugger.print_exception()
|
||||
print(e)
|
||||
return
|
||||
setattr(wrapped, f'_on_{mode}', True)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def on_predraw():
|
||||
return DrawCallbacks.on_draw('predraw')
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self._fns = {
|
||||
'pre': [ fn for (_, fn) in find_fns(obj, '_on_predraw') ],
|
||||
'pre3d': [ fn for (_, fn) in find_fns(obj, '_on_pre3d' ) ],
|
||||
'post3d': [ fn for (_, fn) in find_fns(obj, '_on_post3d' ) ],
|
||||
'post2d': [ fn for (_, fn) in find_fns(obj, '_on_post2d' ) ],
|
||||
}
|
||||
self.reset_pre()
|
||||
|
||||
def reset_pre(self):
|
||||
self._called_pre = False
|
||||
|
||||
def _call(self, n, *, call_predraw=True):
|
||||
if not self._called_pre:
|
||||
self._called_pre = True
|
||||
for fn in self._fns['pre']: fn(self.obj)
|
||||
for fn in self._fns[n]: fn(self.obj)
|
||||
|
||||
def pre3d(self): self._call('pre3d')
|
||||
def post3d(self): self._call('post3d')
|
||||
def post2d(self): self._call('post2d')
|
||||
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import time
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
|
||||
import bpy
|
||||
|
||||
from .debug import dprint
|
||||
from .decorators import blender_version_wrapper
|
||||
from .human_readable import convert_actions_to_human_readable, convert_human_readable_to_actions
|
||||
from .maths import Point2D, Vec2D
|
||||
from .timerhandler import TimerHandler
|
||||
from . import blender_preferences as bprefs
|
||||
|
||||
|
||||
###
|
||||
###
|
||||
### The classes here will _eventually_ replace those in useractions.py
|
||||
###
|
||||
###
|
||||
|
||||
|
||||
'''
|
||||
copied from:
|
||||
- https://docs.blender.org/api/current/bpy.types.Event.html
|
||||
- https://docs.blender.org/api/current/bpy.types.KeyMapItem.html
|
||||
|
||||
direction: { 'ANY', 'NORTH', 'NORTH_EAST', 'EAST', 'SOUTH_EAST', 'SOUTH', 'SOUTH_WEST', 'WEST', 'NORTH_WEST' }
|
||||
type: {
|
||||
'NONE',
|
||||
|
||||
# System
|
||||
'WINDOW_DEACTIVATE', # window lost focus (minimized, switch away from, etc.)
|
||||
'ACTIONZONE_AREA', 'ACTIONZONE_REGION', 'ACTIONZONE_FULLSCREEN',
|
||||
|
||||
# Mouse
|
||||
'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE', 'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE',
|
||||
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
|
||||
'MOUSEROTATE', 'MOUSESMARTZOOM', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE',
|
||||
'PEN', 'ERASER',
|
||||
'TRACKPADPAN', 'TRACKPADZOOM',
|
||||
|
||||
# Keyboard
|
||||
'LEFT_CTRL', 'LEFT_ALT', 'LEFT_SHIFT', 'RIGHT_ALT', 'RIGHT_CTRL', 'RIGHT_SHIFT', 'OSKEY',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE',
|
||||
'SEMI_COLON', 'PERIOD', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'MINUS', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET',
|
||||
'GRLESS',
|
||||
'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9',
|
||||
'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_0', 'NUMPAD_MINUS', 'NUMPAD_ENTER', 'NUMPAD_PLUS',
|
||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24',
|
||||
'PAUSE', 'INSERT',
|
||||
'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END',
|
||||
'MEDIA_PLAY', 'MEDIA_STOP', 'MEDIA_FIRST', 'MEDIA_LAST',
|
||||
'ESC', 'TAB', 'RET', 'SPACE', 'LINE_FEED', 'BACK_SPACE', 'DEL',
|
||||
'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW',
|
||||
|
||||
# ???
|
||||
'APP',
|
||||
|
||||
# Text Input
|
||||
'TEXTINPUT',
|
||||
|
||||
# Timer
|
||||
'TIMER', 'TIMER0', 'TIMER1', 'TIMER2', 'TIMER_JOBS', 'TIMER_AUTOSAVE', 'TIMER_REPORT', 'TIMERREGION',
|
||||
|
||||
# NDOF
|
||||
'NDOF_MOTION', 'NDOF_BUTTON_MENU', 'NDOF_BUTTON_FIT', 'NDOF_BUTTON_TOP', 'NDOF_BUTTON_BOTTOM', 'NDOF_BUTTON_LEFT', 'NDOF_BUTTON_RIGHT',
|
||||
'NDOF_BUTTON_FRONT', 'NDOF_BUTTON_BACK', 'NDOF_BUTTON_ISO1', 'NDOF_BUTTON_ISO2', 'NDOF_BUTTON_ROLL_CW', 'NDOF_BUTTON_ROLL_CCW',
|
||||
'NDOF_BUTTON_SPIN_CW', 'NDOF_BUTTON_SPIN_CCW', 'NDOF_BUTTON_TILT_CW', 'NDOF_BUTTON_TILT_CCW', 'NDOF_BUTTON_ROTATE', 'NDOF_BUTTON_PANZOOM',
|
||||
'NDOF_BUTTON_DOMINANT', 'NDOF_BUTTON_PLUS', 'NDOF_BUTTON_MINUS',
|
||||
'NDOF_BUTTON_1', 'NDOF_BUTTON_2', 'NDOF_BUTTON_3', 'NDOF_BUTTON_4', 'NDOF_BUTTON_5', 'NDOF_BUTTON_6', 'NDOF_BUTTON_7', 'NDOF_BUTTON_8', 'NDOF_BUTTON_9', 'NDOF_BUTTON_10',
|
||||
'NDOF_BUTTON_A', 'NDOF_BUTTON_B', 'NDOF_BUTTON_C',
|
||||
|
||||
# ???
|
||||
'XR_ACTION'
|
||||
}
|
||||
value: { 'ANY', 'PRESS', 'RELEASE', 'CLICK', 'DOUBLE_CLICK', 'CLICK_DRAG', 'NOTHING' }
|
||||
|
||||
class bpy.types.Event:
|
||||
alt True when the Alt/Option key is held (unless both alt keys pressed and one is released)
|
||||
ascii single ASCII character for this event
|
||||
ctrl True when Ctrl key is held (unless both ctrl keys pressed and one is released)
|
||||
direction drag direction (never used?)
|
||||
is_mouse_absolute last motion event was an absolute input
|
||||
is_repeat event is generated by holding a key down
|
||||
is_tablet event has tablet data
|
||||
mouse_prev_press_x window relative location of the last press event (most recent press)
|
||||
mouse_prev_press_y
|
||||
mouse_prev_x window relative location of mouse (in last event?)
|
||||
mouse_prev_y
|
||||
mouse_region_x region relative location of mouse
|
||||
mouse_region_y
|
||||
mouse_x window relative location of mouse
|
||||
mouse_y
|
||||
oskey True when Cmd key is held
|
||||
pressure pressure of tablet or 1.0 if no tablet present
|
||||
shift True when Shift key is held (unless both shift keys pressed and one is released)
|
||||
tilt pressure (tilt?) of tablet or zeros if no tablet present ([float, float])
|
||||
type (Type of event?)
|
||||
type_prev: (type of last event?)
|
||||
unicode: single unicode character for this event
|
||||
value: type of event, only applies to some
|
||||
value_prev: type of (last?) event, only applies to some
|
||||
xr: XR event data
|
||||
|
||||
class bpy.types.KeyMapItem:
|
||||
active True when KMI is active
|
||||
alt Alt key pressed (int), -1 for any state
|
||||
alt_ui (bool)
|
||||
any any modifier keys pressed
|
||||
ctrl Control key pressed (int), -1 for any state
|
||||
ctrl_ui (bool)
|
||||
direction drag direction
|
||||
id ID of item (int [-32768, 32767], default 0)
|
||||
idname identifier of operator to call on input event
|
||||
is_user_defined True if KMI is user defined (doesn't just replace a builtin item)
|
||||
is_user_modified True if KMI is modified by user
|
||||
key_modifier Regular key pressed as a modifier (see type above)
|
||||
map_type type of event mapping, { 'KEYBOARD', 'MOUSE', 'NDOF', 'TEXTINPUT', 'TIMER' }
|
||||
name name of operator (translated) to call on input event
|
||||
oskey Operating System Key pressed (int), -1 for any state
|
||||
oskey_ui (bool)
|
||||
properties Properties to set when the operator is called
|
||||
propvalue the value this event translates to in a modal keymap
|
||||
repeat active on key-repeat events (when key is held)
|
||||
shift Shift key pressed (int), -1 for any state
|
||||
shift_ui (bool)
|
||||
show_expanded Show key map event and property details in the user interface
|
||||
type type of event
|
||||
value (value of event?)
|
||||
|
||||
bprefs.mouse_doubleclick()
|
||||
bprefs.mouse_drag()
|
||||
bprefs.mouse_move()
|
||||
bprefs.mouse_select()
|
||||
|
||||
notes:
|
||||
|
||||
* if lshift is pressed, then shift is True. if rshift is pressed, then shift will still be True.
|
||||
if lshift or rshift are released, shift will be False! but, this isn't an issue, as blender handles it in the same way.
|
||||
|
||||
* if modal operator invokes another operator on action, modal operator will only see the release of the action in (type_prev, value_prev)
|
||||
|
||||
* mouse_prev_press_* will hold location of mouse at most recent press (keyboard, mouse, anything!)
|
||||
|
||||
'''
|
||||
|
||||
class EventHandler:
|
||||
keyboard_modifier_types = {
|
||||
'LEFT_CTRL', 'LEFT_ALT', 'LEFT_SHIFT', 'RIGHT_ALT', 'RIGHT_CTRL', 'RIGHT_SHIFT', 'OSKEY',
|
||||
}
|
||||
keyboard_alpha_types = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
}
|
||||
keyboard_number_types = {
|
||||
'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE',
|
||||
'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9', 'NUMPAD_0',
|
||||
}
|
||||
keyboard_numpad_types = {
|
||||
'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9', 'NUMPAD_0',
|
||||
'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_MINUS', 'NUMPAD_PLUS',
|
||||
'NUMPAD_ENTER',
|
||||
}
|
||||
keyboard_function_types = {
|
||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
||||
'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24',
|
||||
}
|
||||
keyboard_symbols_types = {
|
||||
'SEMI_COLON', 'PERIOD', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'MINUS', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET',
|
||||
'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_MINUS', 'NUMPAD_PLUS',
|
||||
'GRLESS',
|
||||
}
|
||||
keyboard_media_types = {
|
||||
'MEDIA_PLAY', 'MEDIA_STOP', 'MEDIA_FIRST', 'MEDIA_LAST',
|
||||
'PAUSE', # ???
|
||||
}
|
||||
keyboard_movement_types = {
|
||||
'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END',
|
||||
'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW',
|
||||
}
|
||||
keyboard_escape_types = {
|
||||
'ESC',
|
||||
# 'TAB', ???
|
||||
}
|
||||
keyboard_edit_types = {
|
||||
'INSERT', 'TAB', 'RET', 'SPACE', 'LINE_FEED', 'BACK_SPACE', 'DEL',
|
||||
}
|
||||
keyboard_drag_types = {
|
||||
*keyboard_alpha_types,
|
||||
*keyboard_number_types,
|
||||
*keyboard_numpad_types,
|
||||
*keyboard_symbols_types,
|
||||
}
|
||||
keyboard_types = {
|
||||
*keyboard_modifier_types,
|
||||
*keyboard_alpha_types,
|
||||
*keyboard_number_types,
|
||||
*keyboard_numpad_types,
|
||||
*keyboard_function_types,
|
||||
*keyboard_symbols_types,
|
||||
*keyboard_media_types,
|
||||
*keyboard_movement_types,
|
||||
*keyboard_edit_types,
|
||||
}
|
||||
|
||||
mouse_button_types = {
|
||||
'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE', 'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE',
|
||||
'MOUSEROTATE', 'MOUSESMARTZOOM', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE',
|
||||
'PEN', 'ERASER',
|
||||
'TRACKPADPAN', 'TRACKPADZOOM',
|
||||
}
|
||||
mouse_move_types = {
|
||||
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
|
||||
}
|
||||
mouse_types = { *mouse_button_types, *mouse_move_types, }
|
||||
|
||||
ndof_types = {
|
||||
'NDOF_MOTION',
|
||||
'NDOF_BUTTON_MENU', 'NDOF_BUTTON_FIT', 'NDOF_BUTTON_TOP', 'NDOF_BUTTON_BOTTOM', 'NDOF_BUTTON_LEFT', 'NDOF_BUTTON_RIGHT',
|
||||
'NDOF_BUTTON_FRONT', 'NDOF_BUTTON_BACK', 'NDOF_BUTTON_ISO1', 'NDOF_BUTTON_ISO2', 'NDOF_BUTTON_ROLL_CW', 'NDOF_BUTTON_ROLL_CCW',
|
||||
'NDOF_BUTTON_SPIN_CW', 'NDOF_BUTTON_SPIN_CCW', 'NDOF_BUTTON_TILT_CW', 'NDOF_BUTTON_TILT_CCW', 'NDOF_BUTTON_ROTATE', 'NDOF_BUTTON_PANZOOM',
|
||||
'NDOF_BUTTON_DOMINANT', 'NDOF_BUTTON_PLUS', 'NDOF_BUTTON_MINUS',
|
||||
'NDOF_BUTTON_1', 'NDOF_BUTTON_2', 'NDOF_BUTTON_3', 'NDOF_BUTTON_4', 'NDOF_BUTTON_5', 'NDOF_BUTTON_6', 'NDOF_BUTTON_7', 'NDOF_BUTTON_8', 'NDOF_BUTTON_9', 'NDOF_BUTTON_10',
|
||||
'NDOF_BUTTON_A', 'NDOF_BUTTON_B', 'NDOF_BUTTON_C',
|
||||
}
|
||||
|
||||
timer_types = {
|
||||
'TIMER', 'TIMER0', 'TIMER1', 'TIMER2', 'TIMER_JOBS', 'TIMER_AUTOSAVE', 'TIMER_REPORT', 'TIMERREGION',
|
||||
}
|
||||
|
||||
|
||||
scrollable_types = {
|
||||
'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END',
|
||||
'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW',
|
||||
'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE',
|
||||
'TRACKPADPAN',
|
||||
}
|
||||
|
||||
pressable_types = {
|
||||
# pressable also means releasable, clickable, double-clickable
|
||||
*keyboard_types,
|
||||
*mouse_button_types,
|
||||
*ndof_types
|
||||
}
|
||||
|
||||
special_types = {
|
||||
'mousemove': { 'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE' },
|
||||
'timer': { 'TIMER', 'TIMER_REPORT', 'TIMERREGION' },
|
||||
'deactivate': { 'WINDOW_DEACTIVATE' },
|
||||
}
|
||||
|
||||
modifier_keys = {
|
||||
'alt', 'ctrl', 'shift', 'oskey',
|
||||
}
|
||||
|
||||
def __init__(self, context, *, allow_keyboard_dragging=False):
|
||||
self._allow_keyboard_dragging = allow_keyboard_dragging
|
||||
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
# current states
|
||||
self.mods = { mod: False for mod in self.modifier_keys }
|
||||
self.mouse = None
|
||||
self.mouse_prev = None
|
||||
self._held = {} # types that are currently held. {event.type: time of first held}
|
||||
self._is_dragging = False
|
||||
self.is_navigating = False # <- need this???
|
||||
|
||||
# memory
|
||||
self._first_held = None # contains details of when first held action happened (held type, mouse loc, time)
|
||||
self._last_event_type = None
|
||||
self._just_released = None # keep track of last pressed for double click
|
||||
|
||||
|
||||
|
||||
# these properties are for very temporal state changes
|
||||
@property
|
||||
def is_mousemove(self):
|
||||
return self._last_event_type in self.special_types['mousemove']
|
||||
@property
|
||||
def is_timer(self):
|
||||
return self._last_event_type in self.special_types['timer']
|
||||
@property
|
||||
def is_deactivate(self):
|
||||
return self._last_event_type in self.special_types['deactivate']
|
||||
|
||||
|
||||
def is_draggable(self, event):
|
||||
if self._allow_keyboard_dragging and event.type in self.keyboard_drag_types:
|
||||
return True
|
||||
if event.type in self.mouse_button_types:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_double_click(self, *, event=None):
|
||||
if event and event.type != self.get_just_held('type'):
|
||||
return False
|
||||
delta = self.get_just_held('time') - time.time()
|
||||
return delta < prefs.mouse_doubleclick()
|
||||
|
||||
def is_dragging(self, *, event=None):
|
||||
return get_held(event.type, prop='dragging') if event else self.get_first_held(prop='dragging')
|
||||
|
||||
def holding_non_modifiers(self):
|
||||
return bool(t for t in self._held if t not in self.keyboard_modifier_types)
|
||||
|
||||
def get_held(self, etype, *, prop=None, default=None):
|
||||
if etype not in self._held: return default
|
||||
d = self._held[etype]
|
||||
return d[prop] if prop else d
|
||||
|
||||
def get_first_held(self, *, ignore_mods=True, prop=None, default=None):
|
||||
held = self._held
|
||||
if ignore_mods:
|
||||
held = {htype:held[htype] for htype in held if htype not in self.keyboard_modifier_types}
|
||||
if not held: return default
|
||||
d = min(held, key=lambda htype: held[htype]['time'])
|
||||
return d[prop] if prop else d
|
||||
|
||||
def get_just_held(self, *, prop=None, default=None):
|
||||
return self._first_held[prop] if self._first_held else default
|
||||
|
||||
def _update_press(self, event):
|
||||
# ignore non-pressable events
|
||||
if event.type not in self.pressable_types:
|
||||
return
|
||||
|
||||
# FIRST, if nothing is held (ignoring modifiers), record first held details
|
||||
if not self.holding_non_modifiers():
|
||||
self._first_held = {
|
||||
'type': event.type,
|
||||
'time': time.time(),
|
||||
'mouse': self.mouse,
|
||||
'dragging': False,
|
||||
'can drag': self.is_type_draggable(event),
|
||||
'double': self.is_double_click(event),
|
||||
}
|
||||
|
||||
self._held[event.type] = {
|
||||
'type': event.type,
|
||||
'time': time.time(),
|
||||
'mouse': self.mouse,
|
||||
'dragging': False,
|
||||
'can drag': self.is_type_draggable(event),
|
||||
'double': self.is_double_click(event),
|
||||
}
|
||||
|
||||
def _update_release(self, event, *, prev=False):
|
||||
etype = event.type if not prev else event.prev_type
|
||||
|
||||
if etype == self.get_first_held(prop='type'):
|
||||
self._just_released = self._first_held
|
||||
self._first_held = None
|
||||
|
||||
if etype in self.held:
|
||||
del self._held[etype]
|
||||
|
||||
def _update_drag(self, event):
|
||||
first_held = self.get_first_held()
|
||||
if first_held['dragging'] or not first_held['can drag']:
|
||||
return
|
||||
|
||||
# has mouse moved far enough?
|
||||
mouse_travel = (first_held['mouse'] - self.mouse).length
|
||||
if mouse_travel > bprefs.mouse_drag():
|
||||
self._first_held['dragging'] = True
|
||||
|
||||
fhtype = self._first_held['type']
|
||||
if self._allow_keyboard_dragging and fhtype in self.keyboard_drag_types:
|
||||
self._first_held['dragging'] = True
|
||||
elif fhtype in self.mouse_button_types:
|
||||
self._first_held['dragging'] = True
|
||||
|
||||
def update(self, context, event):
|
||||
self._last_event_type = event.type
|
||||
|
||||
if self.is_deactivate:
|
||||
# any time these actions are received, all action states will be flushed
|
||||
self.reset()
|
||||
|
||||
self.mods['alt'] = event.alt
|
||||
self.mods['ctrl'] = event.ctrl
|
||||
self.mods['oskey'] = event.oskey
|
||||
self.mods['shift'] = event.shift
|
||||
self.mouse = Point2D((event.mouse_x, event.mouse_y))
|
||||
self.mouse_prev = Point2D((event.mouse_prev_x, event.mouse_prev_y))
|
||||
|
||||
if event.value_prev == 'RELEASE':
|
||||
self._update_release(event, prev=True)
|
||||
|
||||
if event.value == 'PRESS':
|
||||
self._update_press(event)
|
||||
elif event.value == 'RELEASE':
|
||||
self._update_release(event)
|
||||
elif event.value == 'NOTHING':
|
||||
if event.type == 'MOUSEMOVE':
|
||||
pass
|
||||
|
||||
if event.type not in self.mouse_move_types:
|
||||
self._update_drag(event)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from . import gpustate
|
||||
from .blender import get_path_from_addon_root, get_path_shortened_from_addon_root
|
||||
from .blender_preferences import get_preferences
|
||||
from .debug import dprint
|
||||
from .decorators import blender_version_wrapper, only_in_blender_version
|
||||
from .profiler import profiler
|
||||
|
||||
# https://docs.blender.org/api/current/blf.html
|
||||
|
||||
class FontManager:
|
||||
_cache = {0:0}
|
||||
_last_fontid = 0
|
||||
_prefs = get_preferences()
|
||||
|
||||
@staticmethod
|
||||
@property
|
||||
def last_fontid(): return FontManager._last_fontid
|
||||
|
||||
@staticmethod
|
||||
def get_dpi():
|
||||
ui_scale = FontManager._prefs.view.ui_scale
|
||||
pixel_size = FontManager._prefs.system.pixel_size
|
||||
dpi = 72 # FontManager._prefs.system.dpi
|
||||
return int(dpi * ui_scale * pixel_size)
|
||||
|
||||
@staticmethod
|
||||
def load(val, load_callback=None):
|
||||
if val is None:
|
||||
fontid = FontManager._last_fontid
|
||||
else:
|
||||
if val not in FontManager._cache:
|
||||
# note: loading the same file multiple times is not a problem.
|
||||
# blender is smart enough to cache
|
||||
fontid = blf.load(val)
|
||||
print(f'Addon Common: Loaded font id={fontid}: {val}')
|
||||
FontManager._cache[val] = fontid
|
||||
FontManager._cache[fontid] = fontid
|
||||
if load_callback: load_callback(fontid)
|
||||
fontid = FontManager._cache[val]
|
||||
FontManager._last_fontid = fontid
|
||||
return fontid
|
||||
|
||||
@staticmethod
|
||||
def unload_fontids():
|
||||
unloaded = []
|
||||
for name,fontid in FontManager._cache.items():
|
||||
if type(name) is str:
|
||||
blf.unload(name)
|
||||
unloaded += [name]
|
||||
for name in unloaded:
|
||||
del FontManager._cache[name]
|
||||
FontManager._last_fontid = 0
|
||||
|
||||
@staticmethod
|
||||
def unload(filename):
|
||||
assert filename in FontManager._cache
|
||||
fontid = FontManager._cache[filename]
|
||||
# dprint('Unloading font "%s" as id %d' % (filename, fontid))
|
||||
pass
|
||||
blf.unload(filename)
|
||||
del FontManager._cache[filename]
|
||||
if fontid == FontManager._last_fontid:
|
||||
FontManager._last_fontid = 0
|
||||
|
||||
@staticmethod
|
||||
def aspect(aspect, fontid=None):
|
||||
return blf.aspect(FontManager.load(fontid), aspect)
|
||||
|
||||
@staticmethod
|
||||
def blur(radius, fontid=None):
|
||||
return blf.blur(FontManager.load(fontid), radius)
|
||||
|
||||
@staticmethod
|
||||
def clipping(xymin, xymax, fontid=None):
|
||||
return blf.clipping(FontManager.load(fontid), *xymin, *xymax)
|
||||
|
||||
@staticmethod
|
||||
def color(color, fontid=None):
|
||||
blf.color(FontManager.load(fontid), *color)
|
||||
|
||||
@staticmethod
|
||||
def dimensions(text, fontid=None):
|
||||
return blf.dimensions(FontManager.load(fontid), text)
|
||||
|
||||
@staticmethod
|
||||
def disable(option, fontid=None):
|
||||
return blf.disable(FontManager.load(fontid), option)
|
||||
|
||||
@staticmethod
|
||||
def disable_rotation(fontid=None):
|
||||
return blf.disable(FontManager.load(fontid), blf.ROTATION)
|
||||
|
||||
@staticmethod
|
||||
def disable_clipping(fontid=None):
|
||||
return blf.disable(FontManager.load(fontid), blf.CLIPPING)
|
||||
|
||||
@staticmethod
|
||||
def disable_shadow(fontid=None):
|
||||
return blf.disable(FontManager.load(fontid), blf.SHADOW)
|
||||
|
||||
@staticmethod
|
||||
def disable_word_wrap(fontid=None):
|
||||
return blf.disable(FontManager.load(fontid), blf.WORD_WRAP)
|
||||
|
||||
@staticmethod
|
||||
def draw(text, xyz=None, fontsize=None, fontid=None):
|
||||
fontid = FontManager.load(fontid)
|
||||
if xyz: blf.position(fontid, *xyz)
|
||||
if fontsize: FontManager.size(fontsize, fontid=fontid)
|
||||
return blf.draw(fontid, text)
|
||||
|
||||
@staticmethod
|
||||
def draw_simple(text, xyz):
|
||||
fontid = FontManager._last_fontid
|
||||
blf.position(fontid, *xyz)
|
||||
blend_eqn = gpustate.get_blend() # storing blend settings, because blf.draw used to overwrite them (not sure if still applies)
|
||||
ret = blf.draw(fontid, text)
|
||||
gpustate.blend(blend_eqn) # restore blend settings
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def enable(option, fontid=None):
|
||||
return blf.enable(FontManager.load(fontid), option)
|
||||
|
||||
@staticmethod
|
||||
def enable_rotation(fontid=None):
|
||||
return blf.enable(FontManager.load(fontid), blf.ROTATION)
|
||||
|
||||
@staticmethod
|
||||
def enable_clipping(fontid=None):
|
||||
return blf.enable(FontManager.load(fontid), blf.CLIPPING)
|
||||
|
||||
@staticmethod
|
||||
def enable_shadow(fontid=None):
|
||||
return blf.enable(FontManager.load(fontid), blf.SHADOW)
|
||||
|
||||
@staticmethod
|
||||
def enable_word_wrap(fontid=None):
|
||||
# note: not a listed option in docs for `blf.enable`, but see `blf.word_wrap`
|
||||
return blf.enable(FontManager.load(fontid), blf.WORD_WRAP)
|
||||
|
||||
@staticmethod
|
||||
def position(xyz, fontid=None):
|
||||
return blf.position(FontManager.load(fontid), *xyz)
|
||||
|
||||
@staticmethod
|
||||
def rotation(angle, fontid=None):
|
||||
return blf.rotation(FontManager.load(fontid), angle)
|
||||
|
||||
@staticmethod
|
||||
def shadow(level, rgba, fontid=None):
|
||||
return blf.shadow(FontManager.load(fontid), level, *rgba)
|
||||
|
||||
@staticmethod
|
||||
def shadow_offset(xy, fontid=None):
|
||||
return blf.shadow_offset(FontManager.load(fontid), *xy)
|
||||
|
||||
@staticmethod
|
||||
def size(size, fontid=None):
|
||||
return blf.size(FontManager.load(fontid), size)
|
||||
|
||||
@staticmethod
|
||||
def word_wrap(wrap_width, fontid=None):
|
||||
return blf.word_wrap(FontManager.load(fontid), wrap_width)
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
|
||||
|
||||
|
||||
Bitstream Vera Fonts Copyright
|
||||
------------------------------
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
||||
a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of the fonts accompanying this license ("Fonts") and associated
|
||||
documentation files (the "Font Software"), to reproduce and distribute the
|
||||
Font Software, including without limitation the rights to use, copy, merge,
|
||||
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice shall
|
||||
be included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular
|
||||
the designs of glyphs or characters in the Fonts may be modified and
|
||||
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||
are renamed to names not containing either the words "Bitstream" or the word
|
||||
"Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or Font
|
||||
Software that has been modified and is distributed under the "Bitstream
|
||||
Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but no
|
||||
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||
FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome
|
||||
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this Font Software
|
||||
without prior written authorization from the Gnome Foundation or Bitstream
|
||||
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||
org.
|
||||
|
||||
Arev Fonts Copyright
|
||||
------------------------------
|
||||
|
||||
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the fonts accompanying this license ("Fonts") and
|
||||
associated documentation files (the "Font Software"), to reproduce
|
||||
and distribute the modifications to the Bitstream Vera Font Software,
|
||||
including without limitation the rights to use, copy, merge, publish,
|
||||
distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice
|
||||
shall be included in all copies of one or more of the Font Software
|
||||
typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in
|
||||
particular the designs of glyphs or characters in the Fonts may be
|
||||
modified and additional glyphs or characters may be added to the
|
||||
Fonts, only if the fonts are renamed to names not containing either
|
||||
the words "Tavmjong Bah" or the word "Arev".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts
|
||||
or Font Software that has been modified and is distributed under the
|
||||
"Tavmjong Bah Arev" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but
|
||||
no copy of one or more of the Font Software typefaces may be sold by
|
||||
itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
||||
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of Tavmjong Bah shall not
|
||||
be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in this Font Software without prior written authorization
|
||||
from Tavmjong Bah. For further information, contact: tavmjong @ free
|
||||
. fr.
|
||||
|
||||
TeX Gyre DJV Math
|
||||
-----------------
|
||||
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||
|
||||
Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
|
||||
(on behalf of TeX users groups) are in public domain.
|
||||
|
||||
Letters imported from Euler Fraktur from AMSfonts are (c) American
|
||||
Mathematical Society (see below).
|
||||
Bitstream Vera Fonts Copyright
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
|
||||
is a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of the fonts accompanying this license (“Fonts”) and associated
|
||||
documentation
|
||||
files (the “Font Software”), to reproduce and distribute the Font Software,
|
||||
including without limitation the rights to use, copy, merge, publish,
|
||||
distribute,
|
||||
and/or sell copies of the Font Software, and to permit persons to whom
|
||||
the Font Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice
|
||||
shall be
|
||||
included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular
|
||||
the designs of glyphs or characters in the Fonts may be modified and
|
||||
additional
|
||||
glyphs or characters may be added to the Fonts, only if the fonts are
|
||||
renamed
|
||||
to names not containing either the words “Bitstream” or the word “Vera”.
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or
|
||||
Font Software
|
||||
that has been modified and is distributed under the “Bitstream Vera”
|
||||
names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but
|
||||
no copy
|
||||
of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||
FOUNDATION
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
|
||||
SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
|
||||
ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
|
||||
INABILITY TO USE
|
||||
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Except as contained in this notice, the names of GNOME, the GNOME
|
||||
Foundation,
|
||||
and Bitstream Inc., shall not be used in advertising or otherwise to promote
|
||||
the sale, use or other dealings in this Font Software without prior written
|
||||
authorization from the GNOME Foundation or Bitstream Inc., respectively.
|
||||
For further information, contact: fonts at gnome dot org.
|
||||
|
||||
AMSFonts (v. 2.2) copyright
|
||||
|
||||
The PostScript Type 1 implementation of the AMSFonts produced by and
|
||||
previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
|
||||
available for general use. This has been accomplished through the
|
||||
cooperation
|
||||
of a consortium of scientific publishers with Blue Sky Research and Y&Y.
|
||||
Members of this consortium include:
|
||||
|
||||
Elsevier Science IBM Corporation Society for Industrial and Applied
|
||||
Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
|
||||
|
||||
In order to assure the authenticity of these fonts, copyright will be
|
||||
held by
|
||||
the American Mathematical Society. This is not meant to restrict in any way
|
||||
the legitimate use of the fonts, such as (but not limited to) electronic
|
||||
distribution of documents containing these fonts, inclusion of these fonts
|
||||
into other public domain or commercial font collections or computer
|
||||
applications, use of the outline data to create derivative fonts and/or
|
||||
faces, etc. However, the AMS does require that the AMS copyright notice be
|
||||
removed from any derivative versions of the fonts which have been altered in
|
||||
any way. In addition, to ensure the fidelity of TeX documents using Computer
|
||||
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
|
||||
has requested that any alterations which yield different font metrics be
|
||||
given a different name.
|
||||
|
||||
$Id$
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,251 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
https://github.com/CGCookie/retopoflow
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import inspect
|
||||
from functools import wraps
|
||||
|
||||
from .debug import ExceptionHandler
|
||||
from .debug import debugger
|
||||
from .functools import find_fns
|
||||
|
||||
|
||||
def get_state(state, substate):
|
||||
return '%s__%s' % (str(state), str(substate))
|
||||
|
||||
|
||||
class FSM:
|
||||
def __init__(self, obj, *, start='main', reset_state=None):
|
||||
if False: print(f'FSM.__init__: {self}, {obj}, {start}, {reset_state}')
|
||||
|
||||
if False:
|
||||
# debugging print
|
||||
for i, entry in enumerate(inspect.stack()):
|
||||
if i == 0: continue
|
||||
if 'frozen importlib.' in entry.filename: continue
|
||||
s = f'{entry.filename}:{entry.lineno}'
|
||||
s = s + ' '*max(0, 150-len(s))
|
||||
c = entry.code_context[0].replace('\n','')
|
||||
print(f' {s} {c}')
|
||||
|
||||
if reset_state is None: reset_state = start
|
||||
|
||||
self._obj = obj
|
||||
self._state_next = start
|
||||
self._state = None
|
||||
self._reset_state = reset_state
|
||||
|
||||
# collect and update state fns
|
||||
self._fsm_states_handled = { data['state'] for (data, _) in find_fns(obj, '_fsm_state') if data['substate'] == 'main' }
|
||||
self._fsm_states = {}
|
||||
for (data,fn) in find_fns(obj, '_fsm_state'):
|
||||
state_substate = data['full']
|
||||
assert state_substate not in self._fsm_states, f'FSM: Duplicate states ({data}, {fn}) registered!'
|
||||
self._fsm_states[state_substate] = fn
|
||||
data['fsm'] = self
|
||||
if False: print(f'FSM: state {data["full"]} {fn}')
|
||||
# print('%s: found fn %s as %s' % (str(self), str(fn), m))
|
||||
assert start in self._fsm_states_handled, f'FSM: start state "{start}" not in handled states ({self._fsm_states_handled})'
|
||||
assert reset_state in self._fsm_states_handled, f'FSM: reset state "{reset_state}" not in handled states ({self._fsm_states_handled})'
|
||||
|
||||
# update only-in-state fns
|
||||
for (data, fn) in find_fns(obj, '_fsm_onlyinstate'):
|
||||
if False: print(f'FSM: only-in-state {data["states"]} {fn}')
|
||||
data['fsm'] = self
|
||||
|
||||
# collect and update exception handler fns
|
||||
self._exceptionhandler = ExceptionHandler()
|
||||
for (data, fn) in find_fns(obj, '_fsm_exception'):
|
||||
if False: print(f'FSM: exception {fn}')
|
||||
self._exceptionhandler.add_callback(fn, universal=data['universal'])
|
||||
data['fsm'] = self
|
||||
|
||||
|
||||
def handle_exception(self, e):
|
||||
self._exceptionhandler.handle_exception(e)
|
||||
def add_exception_callback(self, fn, universal=True):
|
||||
self._exceptionhandler.add_callback(fn, universal=universal)
|
||||
|
||||
|
||||
#################################################################################################################################
|
||||
# these function decorators will mark the fn with special data that will be collected upon instantiation of subclass of FSM
|
||||
|
||||
@staticmethod
|
||||
def on_exception(universal=False):
|
||||
def wrapper(fn):
|
||||
fr = inspect.getframeinfo(inspect.currentframe().f_back)
|
||||
location = f'{fr.filename}:{fr.lineno}'
|
||||
data = {
|
||||
'fsm': None, # FSM object, to be set when FSM object is created + initialized
|
||||
'fn': fn,
|
||||
'location': location,
|
||||
'universal': universal,
|
||||
}
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal data, fn, location
|
||||
try:
|
||||
fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f'FSM: Caught exception while handling exception in {fn.__name__} (loc:{location}")')
|
||||
print(f' Exception: {e}')
|
||||
debugger.print_exception()
|
||||
return None
|
||||
wrapped._fsm_exception = data
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def onlyinstate(states, *, default=None):
|
||||
def wrapper(fn):
|
||||
nonlocal states, default
|
||||
if type(states) is str: states = { states }
|
||||
fr = inspect.getframeinfo(inspect.currentframe().f_back)
|
||||
location = f'{fr.filename}:{fr.lineno}'
|
||||
data = {
|
||||
'fsm': None, # FSM object, to be set when FSM object is created + initialized
|
||||
'fn': fn,
|
||||
'location': location,
|
||||
'states': states,
|
||||
'default': default,
|
||||
}
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal data, fn, location, states, default
|
||||
fsm = data['fsm']
|
||||
if not fsm:
|
||||
print(f'FSM: attempting to run {fn.__name__} ({location}) without an FSM instanced')
|
||||
print(f' returning default value')
|
||||
return default
|
||||
if fsm.state not in data['states']:
|
||||
# not in correct state to run this function
|
||||
return default
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f'FSM: Caught exception in {fn.__name__} (loc:{location}, states:"{states}")')
|
||||
print(f' Exception: {e}')
|
||||
debugger.print_exception()
|
||||
fsm.handle_exception(e)
|
||||
fsm.force_reset()
|
||||
return default
|
||||
wrapped._fsm_onlyinstate = data
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def on_state(state, substate='main'):
|
||||
def wrapper(fn):
|
||||
fr = inspect.getframeinfo(inspect.currentframe().f_back)
|
||||
location = f'{fr.filename}:{fr.lineno}'
|
||||
assert substate in {'main', 'can enter', 'enter', 'can exit', 'exit'}, f'FSM: unexpected substate "{substate}" in {fn.__name__} ({location})'
|
||||
data = {
|
||||
'fsm': None, # FSM object, to be set when FSM object is created + initialized
|
||||
'fn': fn,
|
||||
'location': location,
|
||||
'state': state,
|
||||
'substate': substate,
|
||||
'full': get_state(state, substate),
|
||||
}
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal data, fn, location, state, substate
|
||||
fsm = data['fsm']
|
||||
if not fsm:
|
||||
print(f'FSM: attempting to run {fn.__name__} ({location}) without an FSM instanced. returning')
|
||||
return
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f'FSM: Caught exception in {fn.__name__} (loc:{location}, state:"{state}", substate:"{substate}")')
|
||||
print(f' Exception: {e}')
|
||||
debugger.print_exception()
|
||||
fsm.handle_exception(e)
|
||||
fsm.force_reset()
|
||||
return
|
||||
wrapped._fsm_state = data
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
def _call(self, state, substate='main', fail_if_not_exist=False):
|
||||
s = get_state(state, substate)
|
||||
if s not in self._fsm_states:
|
||||
assert not fail_if_not_exist, f'FSM: Could not find state "{state}" with substate "{substate}" ({s})'
|
||||
return
|
||||
try:
|
||||
return self._fsm_states[s](self._obj)
|
||||
except Exception as e:
|
||||
print('Caught exception in state ("%s")' % (s))
|
||||
debugger.print_exception()
|
||||
self._exceptionhandler.handle_exception(e)
|
||||
return
|
||||
|
||||
def update(self):
|
||||
if self._state_next is not None and self._state_next != self._state:
|
||||
if self._call(self._state, substate='can exit') == False:
|
||||
# print('Cannot exit %s' % str(self._state))
|
||||
self._state_next = None
|
||||
return
|
||||
if self._call(self._state_next, substate='can enter') == False:
|
||||
# print('Cannot enter %s' % str(self._state_next))
|
||||
self._state_next = None
|
||||
return
|
||||
# print('%s -> %s' % (str(self._state), str(self._state_next)))
|
||||
self._call(self._state, substate='exit')
|
||||
self._state = self._state_next
|
||||
self._call(self._state, substate='enter')
|
||||
|
||||
ret = self._call(self._state, fail_if_not_exist=True)
|
||||
|
||||
if ret is None:
|
||||
self._state_next = ret
|
||||
ret = None
|
||||
elif type(ret) is str:
|
||||
if self.is_state(ret):
|
||||
self._state_next = ret
|
||||
ret = None
|
||||
else:
|
||||
self._state_next = None
|
||||
ret = ret
|
||||
elif type(ret) is tuple:
|
||||
st = {s for s in ret if self.is_state(s)}
|
||||
if len(st) == 0:
|
||||
self._state_next = None
|
||||
ret = ret
|
||||
elif len(st) == 1:
|
||||
self._state_next = next(st)
|
||||
ret = ret - st
|
||||
else:
|
||||
assert False, 'unhandled FSM return value "%s"' % str(ret)
|
||||
else:
|
||||
assert False, 'unhandled FSM return value "%s"' % str(ret)
|
||||
|
||||
return ret
|
||||
|
||||
def is_state(self, state):
|
||||
return state in self._fsm_states_handled
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def force_set_state(self, state, *, call_exit=False, call_enter=True):
|
||||
if call_exit: self._call(self._state, substate='exit')
|
||||
self._state = state
|
||||
self._state_next = state
|
||||
if call_enter: self._call(self._state, substate='enter')
|
||||
|
||||
def force_reset(self, **kwargs):
|
||||
self.force_set_state(self._reset_state, **kwargs)
|
||||
@@ -0,0 +1,49 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from inspect import isfunction, signature
|
||||
|
||||
|
||||
##################################################
|
||||
|
||||
|
||||
# find functions of object that has key attribute
|
||||
# returns list of (attribute value, fn)
|
||||
def find_fns(obj, key, *, full_search=False):
|
||||
classes = type(obj).__mro__ if full_search else [type(obj)]
|
||||
members = [getattr(cls, k) for cls in classes for k in dir(cls) if hasattr(cls, k)]
|
||||
# test if type is fn_type rather than isfunction() because bpy has problems!
|
||||
# methods = [member for member in members if isfunction(member)]
|
||||
fn_type = type(find_fns)
|
||||
methods = [member for member in members if type(member) == fn_type]
|
||||
return [
|
||||
(getattr(method, key), method)
|
||||
for method in methods
|
||||
if hasattr(method, key)
|
||||
]
|
||||
|
||||
def self_wrapper(self, fn):
|
||||
sig = signature(fn)
|
||||
params = list(sig.parameters.values())
|
||||
if params[0].name != 'self': return fn
|
||||
def wrapped(*args, **kwargs):
|
||||
return fn(self, *args, **kwargs)
|
||||
return wrapped
|
||||
@@ -0,0 +1,52 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
'''
|
||||
This code helps prevent circular importing.
|
||||
Each of the main common objects are referenced here.
|
||||
'''
|
||||
|
||||
class GlobalsMeta(type):
|
||||
# allows for `Globals.drawing` instead of `Globals.get('drawing')`
|
||||
def __setattr__(self, name, value):
|
||||
self.set(value, objtype=name)
|
||||
def __getattr__(self, objtype):
|
||||
return self.get(objtype)
|
||||
|
||||
class Globals(metaclass=GlobalsMeta):
|
||||
__vars = {}
|
||||
|
||||
@staticmethod
|
||||
def set(obj, objtype=None):
|
||||
Globals.__vars[objtype or type(obj).__name__.lower()] = obj
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def is_set(objtype):
|
||||
return Globals.__vars.get(objtype, None) is not None
|
||||
|
||||
@staticmethod
|
||||
def get(objtype):
|
||||
return Globals.__vars.get(objtype, None)
|
||||
|
||||
@staticmethod
|
||||
def __getattr__(objtype):
|
||||
return Globals.get(objtype)
|
||||
@@ -0,0 +1,941 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
#######################################################################
|
||||
# THE FOLLOWING FUNCTIONS ARE ONLY FOR THE TRANSITION FROM BGL TO GPU #
|
||||
# THIS FILE **SHOULD** GO AWAY ONCE WE DROP SUPPORT FOR BLENDER 2.83 #
|
||||
# AROUND JUNE 2023 AS BLENDER 2.93 HAS GPU MODULE #
|
||||
#######################################################################
|
||||
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
from inspect import isroutine
|
||||
from itertools import chain
|
||||
from contextlib import contextmanager
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from .blender import get_path_from_addon_common
|
||||
from .globals import Globals
|
||||
from .decorators import only_in_blender_version, warn_once, add_cache
|
||||
from .maths import mid
|
||||
from .utils import Dict
|
||||
from ..terminal import term_printer
|
||||
|
||||
|
||||
# note: not all supported by user system, but we don't need full functionality
|
||||
# https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions
|
||||
# OpenGL GLSL OpenGL GLSL
|
||||
# 2.0 110 4.0 400
|
||||
# 2.1 120 4.1 410
|
||||
# 3.0 130 4.2 420
|
||||
# 3.1 140 4.3 430
|
||||
# 3.2 150 4.4 440
|
||||
# 3.3 330 4.5 450
|
||||
# 4.6 460
|
||||
|
||||
|
||||
if bpy.app.version < (3,4,0):
|
||||
use_bgl_default = True
|
||||
use_gpu_default = False
|
||||
use_gpu_scissor = False
|
||||
elif bpy.app.version < (3,5,1):
|
||||
use_bgl_default = False # gpu.platform.backend_type_get() in {'OPENGL',}
|
||||
use_gpu_default = True # not use_bgl_default
|
||||
use_gpu_scissor = False
|
||||
else:
|
||||
use_bgl_default = False # gpu.platform.backend_type_get() in {'OPENGL',}
|
||||
use_gpu_default = True # not use_bgl_default
|
||||
use_gpu_scissor = True
|
||||
|
||||
print(f'Addon Common: {use_bgl_default=} {use_gpu_default=} {use_gpu_scissor=}')
|
||||
|
||||
def get_blend(): return gpu.state.blend_get()
|
||||
def blend(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default, only=None):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
import bgl
|
||||
if only != 'function':
|
||||
if mode == 'NONE':
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
else:
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
if only != 'enable':
|
||||
map_mode_bgl = {
|
||||
'ALPHA': (bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA),
|
||||
'ALPHA_PREMULT': (bgl.GL_ONE, bgl.GL_ONE_MINUS_SRC_ALPHA),
|
||||
'ADDITIVE': (bgl.GL_SRC_ALPHA, bgl.GL_ONE),
|
||||
'ADDITIVE_PREMULT': (bgl.GL_ONE, bgl.GL_ONE),
|
||||
'MULTIPLY': (bgl.GL_DST_COLOR, bgl.GL_ZERO),
|
||||
'SUBTRACT': (bgl.GL_ONE, bgl.GL_ONE),
|
||||
'INVERT': (bgl.GL_ONE_MINUS_DST_COLOR, bgl.GL_ZERO),
|
||||
}
|
||||
bgl.glBlendFunc(*map_mode_bgl[mode])
|
||||
if use_gpu:
|
||||
if not only:
|
||||
gpu.state.blend_set(mode)
|
||||
elif only == 'enable':
|
||||
if (mode == 'NONE') != (gpu.state.blend_get() == 'NONE'):
|
||||
# enabled-ness is different (one is enabled and other disabled)
|
||||
gpu.state.blend_set(mode)
|
||||
elif only == 'function':
|
||||
if gpu.state.blend_get() != 'NONE':
|
||||
# only set when blending is already enabled
|
||||
gpu.state.blend_set(mode)
|
||||
|
||||
|
||||
def depth_test(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
import bgl
|
||||
if mode == 'NONE':
|
||||
bgl.glDisable(bgl.GL_DEPTH_TEST)
|
||||
else:
|
||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
map_mode_bgl = {
|
||||
'NEVER': bgl.GL_NEVER,
|
||||
'LESS': bgl.GL_LESS,
|
||||
'EQUAL': bgl.GL_EQUAL,
|
||||
'LESS_EQUAL': bgl.GL_LEQUAL,
|
||||
'GREATER': bgl.GL_GREATER,
|
||||
'GREATER_EQUAL': bgl.GL_GEQUAL,
|
||||
'ALWAYS': bgl.GL_ALWAYS,
|
||||
# NOTE: no equivalent for `bgl.GL_NOTEQUAL` in `gpu` module as of Blender 3.5.1
|
||||
}
|
||||
bgl.glDepthFunc(map_mode_bgl[mode])
|
||||
if use_gpu:
|
||||
gpu.state.depth_test_set(mode)
|
||||
def get_depth_test(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
return bgl_get_integerv('GL_DEPTH_FUNC')
|
||||
if use_gpu:
|
||||
return gpu.state.depth_test_get()
|
||||
|
||||
def depth_mask(enable, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
import bgl
|
||||
bgl.glDepthMask(bgl.GL_TRUE if enable else bgl.GL_FALSE)
|
||||
if use_gpu:
|
||||
gpu.state.depth_mask_set(enable)
|
||||
def get_depth_mask(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
return bgl_get_integerv('GL_DEPTH_WRITEMASK')
|
||||
if use_gpu:
|
||||
return gpu.state.depth_mask_get()
|
||||
|
||||
def line_width(width): gpu.state.line_width_set(width)
|
||||
def get_line_width(): return gpu.state.line_width_get()
|
||||
|
||||
def point_size(size): gpu.state.point_size_set(size)
|
||||
def get_point_size(): return gpu.state.point_size_get()
|
||||
|
||||
def scissor(left, bottom, width, height, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl or (not use_gpu_scissor):
|
||||
import bgl
|
||||
bgl.glScissor(left, bottom, width, height)
|
||||
if use_gpu and use_gpu_scissor:
|
||||
gpu.state.scissor_set(left, bottom, width, height)
|
||||
def get_scissor(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl or (not use_gpu_scissor):
|
||||
return bgl_get_integerv_tuple('GL_SCISSOR_BOX', 4)
|
||||
if use_gpu and use_gpu_scissor:
|
||||
return gpu.state.scissor_get()
|
||||
|
||||
def scissor_test(enable, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl or (not use_gpu_scissor):
|
||||
bgl_enable('GL_SCISSOR_TEST', enable)
|
||||
if use_gpu and use_gpu_scissor:
|
||||
gpu.state.scissor_test_set(enable)
|
||||
def get_scissor_test(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl or (not use_gpu_scissor):
|
||||
return bgl_is_enabled('GL_SCISSOR_TEST')
|
||||
if use_gpu and use_gpu_scissor:
|
||||
# NOTE: no equivalent in `gpu` module as of Blender 3.5.1
|
||||
# return gpu.state.scissor_test_get()
|
||||
return False
|
||||
|
||||
def culling(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
||||
assert use_gpu or use_bgl
|
||||
if use_bgl:
|
||||
import bgl
|
||||
if mode == 'NONE':
|
||||
bgl.glDisable(bgl.GL_CULL_FACE)
|
||||
else:
|
||||
bgl.glEnable(bgl.GL_CULL_FACE)
|
||||
map_mode_bgl = {
|
||||
'FRONT': bgl.GL_FRONT,
|
||||
'BACK': bgl.GL_BACK,
|
||||
}
|
||||
bgl.glCullFace(map_mode_bgl[mode])
|
||||
if use_gpu:
|
||||
gpu.state.face_culling_set(mode)
|
||||
|
||||
|
||||
#########################
|
||||
# opengl errors
|
||||
|
||||
@add_cache('_error_check', True)
|
||||
@add_cache('_error_count', 0)
|
||||
@add_cache('_error_limit', 10)
|
||||
def get_glerror(title, *, use_bgl=use_bgl_default):
|
||||
if not use_bgl:
|
||||
# NOTE: no equivalent in `gpu` module as of Blender 3.5.1
|
||||
return False
|
||||
if not get_glerror._error_check: return
|
||||
import bgl
|
||||
err = bgl.glGetError()
|
||||
if err == bgl.GL_NO_ERROR:
|
||||
return False
|
||||
get_glerror._error_count += 1
|
||||
if get_glerror._error_count >= get_glerror._error_limit:
|
||||
return True
|
||||
error_map = {
|
||||
getattr(bgl, k): s
|
||||
for (k,s) in [
|
||||
# https://www.khronos.org/opengl/wiki/OpenGL_Error#Meaning_of_errors
|
||||
('GL_INVALID_ENUM', 'invalid enum'),
|
||||
('GL_INVALID_VALUE', 'invalid value'),
|
||||
('GL_INVALID_OPERATION', 'invalid operation'),
|
||||
('GL_STACK_OVERFLOW', 'stack overflow'), # does not exist in b3d 2.8x for OSX??
|
||||
('GL_STACK_UNDERFLOW', 'stack underflow'), # does not exist in b3d 2.8x for OSX??
|
||||
('GL_OUT_OF_MEMORY', 'out of memory'),
|
||||
('GL_INVALID_FRAMEBUFFER_OPERATION', 'invalid framebuffer operation'),
|
||||
('GL_CONTEXT_LOST', 'context lost'),
|
||||
('GL_TABLE_TOO_LARGE', 'table too large'), # deprecated in OpenGL 3.0, removed in 3.1 core and above
|
||||
]
|
||||
if hasattr(bgl, k)
|
||||
}
|
||||
print(f'ERROR {get_glerror._error_count}/{get_glerror._error_limit} ({title}): {error_map.get(err, f"code {err}")}')
|
||||
traceback.print_stack()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
#######################################
|
||||
# shader
|
||||
|
||||
# https://developer.blender.org/rB21c658b718b9
|
||||
# https://developer.blender.org/T74139
|
||||
def get_srgb_shim(force=False):
|
||||
if not force: return ''
|
||||
return 'vec4 blender_srgb_to_framebuffer_space(vec4 c) { return pow(c, vec4(1.0/2.2, 1.0/2.2, 1.0/2.2, 1.0)); }'
|
||||
|
||||
def shader_parse_string(string, *, includeVersion=True, constant_overrides=None, define_overrides=None, force_shim=False):
|
||||
# NOTE: GEOMETRY SHADER NOT FULLY SUPPORTED, YET
|
||||
# need to find a way to handle in/out
|
||||
constant_overrides = constant_overrides or {}
|
||||
define_overrides = define_overrides or {}
|
||||
uniforms, varyings, attributes, consts = [],[],[],[]
|
||||
vertSource, geoSource, fragSource, commonSource = [],[],[],[]
|
||||
vertVersion, geoVersion, fragVersion = '','',''
|
||||
mode = 'common'
|
||||
lines = string.splitlines()
|
||||
for i_line,line in enumerate(lines):
|
||||
sline = line.lstrip()
|
||||
if re.match(r'uniform ', sline):
|
||||
uniforms.append(line)
|
||||
elif re.match(r'attribute ', sline):
|
||||
attributes.append(line)
|
||||
elif re.match(r'varying ', sline):
|
||||
varyings.append(line)
|
||||
elif re.match(r'const ', sline):
|
||||
m = re.match(r'const +(?P<type>bool|int|float|vec\d) +(?P<var>[a-zA-Z0-9_]+) *= *(?P<val>[^;]+);', sline)
|
||||
if m is None:
|
||||
print(f'Shader could not match const line ({i_line}): {line}')
|
||||
elif m.group('var') in constant_overrides:
|
||||
line = 'const %s %s = %s' % (m.group('type'), m.group('var'), constant_overrides[m.group('var')])
|
||||
consts.append(line)
|
||||
elif re.match(r'#define ', sline):
|
||||
m0 = re.match(r'#define +(?P<var>[a-zA-Z0-9_]+)$', sline)
|
||||
m1 = re.match(r'#define +(?P<var>[a-zA-Z0-9_]+) +(?P<val>.+)$', sline)
|
||||
if m0 and m0.group('var') in define_overrides:
|
||||
if not define_overrides[m0.group('var')]:
|
||||
line = ''
|
||||
if m1 and m1.group('var') in define_overrides:
|
||||
line = '#define %s %s' % (m1.group('var'), define_overrides[m1.group('var')])
|
||||
if not m0 and not m1:
|
||||
print(f'Shader could not match #define line ({i_line}): {line}')
|
||||
consts.append(line)
|
||||
elif re.match(r'#version ', sline):
|
||||
match mode:
|
||||
case 'common': vertVersion = geoVersion = fragVersion = line, line, line
|
||||
case 'vert': vertVersion = line
|
||||
case 'geo': geoVersion = line
|
||||
case 'frag': fragVersion = line
|
||||
case _: assert False, f'Addon Common: Unhandled mode {mode}'
|
||||
elif mode == 'common' and re.match(r'precision ', sline):
|
||||
commonSource.append(line)
|
||||
elif m := re.match(r'//+ +(?P<mode>common|vert(ex)?|geo(m(etry)?)?|frag(ment)?) shader', sline.lower()):
|
||||
match m['mode'][0]:
|
||||
case 'c': mode = 'common'
|
||||
case 'v': mode = 'vert'
|
||||
case 'g': mode = 'geo'
|
||||
case 'f': mode = 'frag'
|
||||
else:
|
||||
if not line.strip(): continue
|
||||
match mode:
|
||||
case 'common': commonSource.append(line)
|
||||
case 'vert': vertSource.append(line)
|
||||
case 'geo': geoSource.append(line)
|
||||
case 'frag': fragSource.append(line)
|
||||
case _: assert False, f'Addon Common: Unhandled mode {mode}'
|
||||
assert vertSource, f'could not detect vertex shader'
|
||||
assert fragSource, f'could not detect fragment shader'
|
||||
v_attributes = [a.replace('attribute ', 'in ') for a in attributes]
|
||||
v_varyings = [v.replace('varying ', 'out ') for v in varyings]
|
||||
f_varyings = [v.replace('varying ', 'in ') for v in varyings]
|
||||
srcVertex = '\n'.join(chain(
|
||||
([vertVersion] if includeVersion else []),
|
||||
uniforms,
|
||||
v_attributes,
|
||||
v_varyings,
|
||||
consts,
|
||||
commonSource,
|
||||
vertSource,
|
||||
))
|
||||
srcFragment = '\n'.join(chain(
|
||||
([fragVersion] if includeVersion else []),
|
||||
uniforms,
|
||||
f_varyings,
|
||||
consts,
|
||||
[get_srgb_shim(force=force_shim)],
|
||||
['/////////////////////'],
|
||||
commonSource,
|
||||
fragSource,
|
||||
))
|
||||
return (srcVertex, srcFragment)
|
||||
|
||||
def shader_read_file(filename):
|
||||
filename_guess = get_path_from_addon_common('common', 'shaders', filename)
|
||||
if os.path.exists(filename): pass
|
||||
elif os.path.exists(filename_guess): filename = filename_guess
|
||||
else: assert False, f"Shader file could not be found: {filename} ({filename_guess})"
|
||||
|
||||
contents = open(filename, 'rt').read()
|
||||
while m_include := re.search(r'\n *#include +"(?P<filename>[^"]+)" *\n', contents):
|
||||
include_contents = shader_read_file(m_include['filename'])
|
||||
contents = contents[:m_include.start()] + f'\n{include_contents}\n' + contents[m_include.end():]
|
||||
return contents
|
||||
|
||||
def shader_parse_file(filename, **kwargs):
|
||||
return shader_parse_string(shader_read_file(filename), **kwargs)
|
||||
|
||||
|
||||
def clean_shader_source(source):
|
||||
source = source + '\n' # add newline at end
|
||||
source = re.sub(r'/[*](\n|.)*?[*]/', '', source) # remove multi-line comments
|
||||
source = re.sub(r'//.*?\n', '\n', source) # remove single line comments
|
||||
source = re.sub(r'\n+', '\n', source) # remove multiple newlines
|
||||
source = re.sub(r'[ \t]+\n', '\n', source) # trim end of lines
|
||||
return source
|
||||
|
||||
re_shader_var = re.compile(
|
||||
r'((layout\((?P<layout>[^)]*)\))\s+)?'
|
||||
r'((?P<qualifier>noperspective|flat|smooth)\s+)?'
|
||||
r'(?P<uio>uniform|in|out)\s+'
|
||||
r'(?P<type>[a-zA-Z0-9_]+)\s+'
|
||||
r'(?P<var>[a-zA-Z0-9_]+)'
|
||||
r'(\s*=\s*(?P<defval>[^;]+))?\s*;'
|
||||
)
|
||||
re_shader_var_parts = ['qualifier', 'uio', 'type', 'var', 'defval', 'layout']
|
||||
def split_shader_vars(source):
|
||||
shader_vars = {
|
||||
m['var']: { part: m[part] for part in re_shader_var_parts }
|
||||
for m in re_shader_var.finditer(source)
|
||||
}
|
||||
source = re_shader_var.sub('', source)
|
||||
source = '\n'.join(l for l in source.splitlines() if l.strip())
|
||||
return (shader_vars, source)
|
||||
|
||||
re_shader_struct = re.compile(r'struct\s+(?P<name>[a-zA-Z0-9_]+)\s+[{](?P<attribs>[^}]+)[}]\s*;')
|
||||
re_shader_struct_attrib = re.compile(r'(?P<type>[a-zA-Z0-9_]+)\s+(?P<name>[a-zA-Z0-9_]+)\n*;')
|
||||
def split_shader_structs(source):
|
||||
structs = {
|
||||
m['name']: {
|
||||
'name': m['name'],
|
||||
'full': m.group(0),
|
||||
'attribs': [ (ma['type'], ma['name']) for ma in re_shader_struct_attrib.finditer(m['attribs']) ],
|
||||
'type': { ma['name']: ma['type'] for ma in re_shader_struct_attrib.finditer(m['attribs']) },
|
||||
}
|
||||
for m in re_shader_struct.finditer(source)
|
||||
}
|
||||
source = re_shader_struct.sub('', source)
|
||||
source = '\n'.join(l for l in source.splitlines() if l.strip())
|
||||
return (structs, source)
|
||||
|
||||
def shader_var_to_ctype(shader_type, shader_varname):
|
||||
return (shader_varname, shader_type_to_ctype(shader_type))
|
||||
|
||||
def shader_type_to_ctype(shader_type):
|
||||
import ctypes
|
||||
match shader_type:
|
||||
case 'mat4': return (ctypes.c_float * 4) * 4
|
||||
case 'vec4': return ctypes.c_float * 4
|
||||
case 'ivec4': return ctypes.c_int * 4
|
||||
case _: assert False, f'Unhandled shader type {shader_type}'
|
||||
|
||||
def shader_struct_to_UBO(shadername, struct, varname):
|
||||
import ctypes
|
||||
# copied+modified from scripts/addons/mesh_snap_utitilies_line/drawing_utilities.py
|
||||
class GPU_UBO(ctypes.Structure):
|
||||
_pack_ = 16
|
||||
_fields_ = [ shader_var_to_ctype(t, n) for (t, n) in struct['attribs'] ]
|
||||
ubo_data = GPU_UBO()
|
||||
ubo_data_size = ctypes.sizeof(ubo_data)
|
||||
ubo_data_slots = ubo_data_size // ctypes.sizeof(ctypes.c_float)
|
||||
if False:
|
||||
term_printer.boxed(
|
||||
f'Struct: "{struct["name"]} {varname}" ({ubo_data_size}bytes, {ubo_data_slots}slots)',
|
||||
f'Attribs: ' + '; '.join(f'{k} {v}' for (k,v) in struct['attribs']),
|
||||
title=f'GPU Shader Struct: {shadername}',
|
||||
)
|
||||
ubo_buffer = gpu.types.Buffer('UBYTE', ubo_data_size, ubo_data)
|
||||
ubo = gpu.types.GPUUniformBuf(ubo_buffer)
|
||||
def setter(name, value):
|
||||
# print(f'UBO_Wrapper.set {name} = {value} ({type(value)})')
|
||||
shader_type = struct['type'][name]
|
||||
match shader_type:
|
||||
case 'mat4':
|
||||
a = getattr(ubo_data, name)
|
||||
CType = shader_type_to_ctype('vec4')
|
||||
if len(value) == 3: value = value.to_4x4()
|
||||
assert len(value) == 4 and len(value[0]) == 4
|
||||
a[0] = CType(value[0][0], value[1][0], value[2][0], value[3][0])
|
||||
a[1] = CType(value[0][1], value[1][1], value[2][1], value[3][1])
|
||||
a[2] = CType(value[0][2], value[1][2], value[2][2], value[3][2])
|
||||
a[3] = CType(value[0][3], value[1][3], value[2][3], value[3][3])
|
||||
case 'vec4'|'ivec4':
|
||||
CType = shader_type_to_ctype(shader_type)
|
||||
if len(value) == 2: value = (*value, 0.0, 0.0)
|
||||
elif len(value) == 3: value = (*value, 0.0)
|
||||
assert len(value) == 4
|
||||
setattr(ubo_data, name, CType(*value))
|
||||
class UBO_Wrapper:
|
||||
def __init__(self):
|
||||
pass
|
||||
def set_shader(self, shader):
|
||||
self.__dict__['_shader'] = shader
|
||||
def __setattr__(self, name, value):
|
||||
self.assign(name, value)
|
||||
def slots_used(self):
|
||||
return ubo_data_slots
|
||||
def assign(self, name, value):
|
||||
try:
|
||||
setter(name, value)
|
||||
except Exception as e:
|
||||
print(f'Caught Exception while trying to set {name} = {value}')
|
||||
print(f' Shader: {shadername}')
|
||||
print(f' Exception: {e}')
|
||||
def update_shader(self, *, debug_print=False):
|
||||
try:
|
||||
if debug_print:
|
||||
print(f'UPDATING SHADER: {shadername} {varname}')
|
||||
shader = self.__dict__['_shader']
|
||||
buf = gpu.types.Buffer('UBYTE', ubo_data_size, ubo_data)
|
||||
if debug_print:
|
||||
print(buf)
|
||||
ubo.update(buf)
|
||||
shader.uniform_block(varname, ubo)
|
||||
del buf
|
||||
except Exception as e:
|
||||
print(f'Caught Exception while trying to update shader')
|
||||
print(f' Shader: {shadername}')
|
||||
print(f' Struct: {struct["name"]}')
|
||||
print(f' Variable: {varname}')
|
||||
print(f' Exception: {e}')
|
||||
return UBO_Wrapper()
|
||||
|
||||
gpu_type_size = {
|
||||
'bool',
|
||||
'uint', 'uvec2', 'uvec3', 'uvec4',
|
||||
'int', 'ivec2', 'ivec3', 'ivec4',
|
||||
'float', 'vec2', 'vec3', 'vec4',
|
||||
'mat3', 'mat4',
|
||||
}
|
||||
def glsl_to_gpu_type(t):
|
||||
if t in gpu_type_size:
|
||||
return t.upper()
|
||||
return t
|
||||
|
||||
re_shader_location = re.compile(r'location *= *(?P<location>\d+)')
|
||||
def gpu_shader(name, vert_source, frag_source, *, defines=None):
|
||||
vert_source, frag_source = map(clean_shader_source, (vert_source, frag_source))
|
||||
vert_shader_structs, vert_source = split_shader_structs(vert_source)
|
||||
frag_shader_structs, frag_source = split_shader_structs(frag_source)
|
||||
shader_structs = vert_shader_structs | frag_shader_structs
|
||||
vert_shader_vars, vert_source = split_shader_vars(vert_source)
|
||||
frag_shader_vars, frag_source = split_shader_vars(frag_source)
|
||||
shader_vars = vert_shader_vars | frag_shader_vars
|
||||
uniform_vars = { k:v for (k,v) in shader_vars.items() if v['uio'] == 'uniform' }
|
||||
in_vars = { k:v for (k,v) in vert_shader_vars.items() if v['uio'] == 'in' }
|
||||
inout_vars = { k:v for (k,v) in vert_shader_vars.items() if v['uio'] == 'out' }
|
||||
out_vars = { k:v for (k,v) in frag_shader_vars.items() if v['uio'] == 'out'}
|
||||
|
||||
if False:
|
||||
def nonetoempty(s): return s if s else ''
|
||||
def divider(s): return f'\n{"═"*5}╡ {s} ╞{"═"*(120-(len(s) + 4 + 5))}\n\n'
|
||||
term_printer.boxed(
|
||||
*(ss['full'] for ss in vert_shader_structs.values()),
|
||||
divider('Uniforms, Inputs, InOuts, Outputs'),
|
||||
f'{"Layout":12s} {"Qualifier":13s} {"UIO":7s} {"Type":10s} {"Var Name":20s} {"Def Val"}',
|
||||
f'{"-"*12 } {"-"*13 } {"-"*7 } {"-"*10 } {"-"*20 } {"-"*(120-(12+1+13+1+7+1+10+1+20+1))}',
|
||||
*(
|
||||
f'{nonetoempty(sv["layout"]):12s} '
|
||||
f'{nonetoempty(sv["qualifier"]):13s} ' # noperspective
|
||||
f'{nonetoempty(sv["uio"]):7s} ' # uniform
|
||||
f'{nonetoempty(sv["type"]):10s} '
|
||||
f'{nonetoempty(sv["var"]):20s} '
|
||||
f'{nonetoempty(sv["defval"])}'
|
||||
for sv in chain(uniform_vars.values(), in_vars.values(), inout_vars.values(), out_vars.values())
|
||||
),
|
||||
divider('Vertex Shader'),
|
||||
vert_source,
|
||||
divider('Fragment Shader'),
|
||||
frag_source,
|
||||
title=f'GPUSader {name}'
|
||||
)
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
|
||||
# STRUCTS
|
||||
# Note: as of 2023.06.04, multiple structs caused compiler errors that were difficult to debug.
|
||||
# I believe it is due to how Blender constructs the platform-specific shader from the GPU shader.
|
||||
assert len(shader_structs) <= 1, f'Cannot support shaders with more than one struct, found {len(shader_structs)} in {name}'
|
||||
for struct in shader_structs.values():
|
||||
# print(f'typedef_source("{struct["full"]}")')
|
||||
shader_info.typedef_source(struct['full'])
|
||||
UBOs = Dict()
|
||||
def update_shader(*, debug_print=False):
|
||||
for n in UBOs:
|
||||
if n in ['update_shader', 'set_shader']: continue
|
||||
UBOs[n].update_shader(debug_print=debug_print)
|
||||
UBOs.update_shader = update_shader
|
||||
def set_shader(shader):
|
||||
for n in UBOs:
|
||||
if n in ['update_shader', 'set_shader']: continue
|
||||
UBOs[n].set_shader(shader)
|
||||
UBOs.set_shader = set_shader
|
||||
|
||||
slot_samplers = 0
|
||||
slot_structs = 0
|
||||
slot_input = 0
|
||||
slot_output = 0
|
||||
|
||||
# UNIFORMS
|
||||
for uniform_var in uniform_vars.values():
|
||||
slot = None
|
||||
if uniform_var['layout'] and (m_location := re_shader_location.search(uniform_var['layout'])):
|
||||
slot = int(m_location['location'])
|
||||
|
||||
match uniform_var['type']:
|
||||
case 'sampler2D':
|
||||
if slot is None: slot = slot_samplers
|
||||
shader_info.sampler(slot, 'FLOAT_2D', uniform_var['var'])
|
||||
slot_samplers = max(slot + 1, slot_samplers)
|
||||
case t if t in gpu_type_size:
|
||||
shader_info.push_constant(glsl_to_gpu_type(uniform_var['type']), uniform_var['var'])
|
||||
case _:
|
||||
if slot is None: slot = slot_structs
|
||||
shader_info.uniform_buf(slot, uniform_var['type'], uniform_var['var'])
|
||||
ubo_wrapper = shader_struct_to_UBO(name, shader_structs[uniform_var['type']], uniform_var['var'])
|
||||
UBOs[uniform_var['var']] = ubo_wrapper
|
||||
# print(f'uniform struct {uniform_var["type"]} {uniform_var["var"]} {slot=}')
|
||||
slot_structs = max(slot + ubo_wrapper.slots_used(), slot_structs)
|
||||
if False:
|
||||
term_printer.boxed(
|
||||
str(UBOs),
|
||||
title=f'Uniforms'
|
||||
)
|
||||
|
||||
# PREPROCESSING DEFINE DIRECTIVES
|
||||
if defines:
|
||||
for k,v in defines.items():
|
||||
shader_info.define(str(k), str(v))
|
||||
|
||||
# INPUTS
|
||||
for in_var in in_vars.values():
|
||||
shader_info.vertex_in(slot_input, glsl_to_gpu_type(in_var['type']), in_var['var'])
|
||||
slot_input += 1
|
||||
|
||||
# INTERFACE
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
||||
safe_name = re.sub(r'__+', '_', safe_name)
|
||||
shader_interface = gpu.types.GPUStageInterfaceInfo(f'interface_{safe_name}') # NOTE: DO NOT CALL IT `interface`
|
||||
qualified_fns = {
|
||||
'noperspective': shader_interface.no_perspective,
|
||||
'flat': shader_interface.flat,
|
||||
'smooth': shader_interface.smooth,
|
||||
None: shader_interface.smooth,
|
||||
}
|
||||
needs_interface = False
|
||||
for inout_var in inout_vars.values():
|
||||
needs_interface = True
|
||||
qualified_fn = qualified_fns[inout_var['qualifier']]
|
||||
qualified_fn(glsl_to_gpu_type(inout_var['type']), inout_var['var'])
|
||||
if needs_interface:
|
||||
shader_info.vertex_out(shader_interface)
|
||||
|
||||
# OUTPUTS
|
||||
for out_var in out_vars.values():
|
||||
# https://wiki.blender.org/wiki/Style_Guide/GLSL#Shared_Shader_Files:~:text=If%20fragment%20shader%20is%20writing%20to%20gl_FragDepth%2C%20usage%20must%20be%20correctly%20defined%20in%20the%20shader%27s%20create%20info%20using%20.depth_write(DepthWrite).
|
||||
if out_var['var'] == 'gl_FragDepth':
|
||||
if hasattr(shader_info, 'depth_write'):
|
||||
# SHOULD BE INCLUDED IN 4.0, AND HOPEFULLY IN 3.6
|
||||
shader_info.depth_write('ANY')
|
||||
if bpy.app.version < (3, 4, 0) or gpu.platform.backend_type_get() == 'OPENGL':
|
||||
continue
|
||||
shader_info.fragment_out(slot_output, glsl_to_gpu_type(out_var['type']), out_var['var'])
|
||||
slot_output += 1
|
||||
|
||||
if False:
|
||||
print(shader_vars)
|
||||
print(vert_source)
|
||||
print(frag_source)
|
||||
|
||||
shader_info.vertex_source(vert_source)
|
||||
shader_info.fragment_source(frag_source)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
UBOs.set_shader(shader)
|
||||
del shader_interface
|
||||
del shader_info
|
||||
return shader, UBOs
|
||||
|
||||
# return gpu.types.GPUShader(vert_source, frag_source)
|
||||
|
||||
|
||||
######################################################################################################
|
||||
|
||||
|
||||
class FrameBuffer:
|
||||
def __init__(self, width, height):
|
||||
self._width, self._height = None, None
|
||||
self._is_bound = False
|
||||
self.resize(width, height)
|
||||
|
||||
def resize(self, width, height, clear_color=True, clear_depth=True):
|
||||
assert not self._is_bound, 'Cannot resize a bounded FrameBuffer'
|
||||
|
||||
width, height = max(1, int(width)), max(1, int(height))
|
||||
if self._width == width and self._height == height: return
|
||||
self._width, self._height = width, height
|
||||
|
||||
vx, vy, vw, vh = -1, -1, 2 / self._width, 2 / self._height
|
||||
self._matrix = Matrix([
|
||||
[vw, 0, 0, vx],
|
||||
[ 0, vh, 0, vy],
|
||||
[ 0, 0, 1, 0],
|
||||
[ 0, 0, 0, 1],
|
||||
])
|
||||
|
||||
self._tex_color = gpu.types.GPUTexture((self._width, self._height), format='RGBA8')
|
||||
self._tex_depth = gpu.types.GPUTexture((self._width, self._height), format='DEPTH_COMPONENT32F')
|
||||
|
||||
self._framebuffer = gpu.types.GPUFrameBuffer(
|
||||
color_slots={ 'texture': self._tex_color },
|
||||
depth_slot=self._tex_depth,
|
||||
)
|
||||
|
||||
@property
|
||||
def color_texture(self): return self._tex_color
|
||||
@property
|
||||
def width(self): return self._width
|
||||
@property
|
||||
def height(self): return self._height
|
||||
|
||||
def _set_viewport(self):
|
||||
o = self._framebuffer if False else gpu.state
|
||||
o.viewport_set(0, 0, self._width, self._height)
|
||||
def _reset_viewport(self):
|
||||
o = self._cur_fbo if False else gpu.state
|
||||
o.viewport_set(*self._cur_viewport)
|
||||
|
||||
def _set_projection(self):
|
||||
gpu.matrix.load_projection_matrix(self._matrix)
|
||||
def _reset_projection(self):
|
||||
gpu.matrix.load_projection_matrix(self._cur_projection)
|
||||
|
||||
def _set_scissor(self):
|
||||
ScissorStack.push(0, self._height - 1, self._width, self._height, clamp=False)
|
||||
def _reset_scissor(self):
|
||||
ScissorStack.pop()
|
||||
|
||||
def _clear(self):
|
||||
self._framebuffer.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0)
|
||||
|
||||
@contextmanager
|
||||
def bind(self):
|
||||
assert not self._is_bound, 'Cannot bind a bounded FrameBuffer'
|
||||
try:
|
||||
self._is_bound = True
|
||||
self._cur_fbo = gpu.state.active_framebuffer_get()
|
||||
self._cur_viewport = gpu.state.viewport_get()
|
||||
self._cur_projection = gpu.matrix.get_projection_matrix()
|
||||
with self._framebuffer.bind():
|
||||
self._set_viewport()
|
||||
self._set_projection()
|
||||
self._set_scissor()
|
||||
self._clear()
|
||||
yield None
|
||||
except Exception as e:
|
||||
print(f'Caught exception while FrameBuffer was bound:')
|
||||
print(f' {e}')
|
||||
Globals.debugger.print_exception()
|
||||
raise e
|
||||
finally:
|
||||
self._reset_scissor()
|
||||
self._reset_projection()
|
||||
self._reset_viewport()
|
||||
self._cur_fbo = None
|
||||
self._cur_viewport = None
|
||||
self._cur_projection = None
|
||||
self._is_bound = False
|
||||
|
||||
|
||||
|
||||
|
||||
######################################################################################################
|
||||
|
||||
|
||||
class ScissorStack:
|
||||
is_started = False
|
||||
scissor_test_was_enabled = False
|
||||
stack = None # stack of (l,t,w,h) in region-coordinates, because viewport is set to region
|
||||
msg_stack = None
|
||||
|
||||
@staticmethod
|
||||
def start(context):
|
||||
assert not ScissorStack.is_started, 'Attempting to start a started ScissorStack'
|
||||
|
||||
# region pos and size are window-coordinates
|
||||
rgn = context.region
|
||||
rl,rb,rw,rh = rgn.x, rgn.y, rgn.width, rgn.height
|
||||
rt = rb + rh - 1
|
||||
|
||||
# remember the current scissor box settings so we can return to them when done
|
||||
ScissorStack.scissor_test_was_enabled = get_scissor_test()
|
||||
get_glerror('get_scissor_test')
|
||||
if ScissorStack.scissor_test_was_enabled:
|
||||
pl, pb, pw, ph = get_scissor() #ScissorStack.buf
|
||||
get_glerror('get_scissor')
|
||||
pt = pb + ph - 1
|
||||
ScissorStack.stack = [(pl, pt, pw, ph)]
|
||||
ScissorStack.msg_stack = ['init']
|
||||
# don't need to enable, because we are already scissoring!
|
||||
# TODO: this is not tested!
|
||||
else:
|
||||
ScissorStack.stack = [(0, rh - 1, rw, rh)]
|
||||
ScissorStack.msg_stack = ['init']
|
||||
scissor_test(True)
|
||||
|
||||
# we're ready to go!
|
||||
ScissorStack.is_started = True
|
||||
ScissorStack._set_scissor()
|
||||
|
||||
@staticmethod
|
||||
def end(force=False):
|
||||
if not force:
|
||||
assert ScissorStack.is_started, 'Attempting to end a non-started ScissorStack'
|
||||
assert len(ScissorStack.stack) == 1, 'Attempting to end a non-empty ScissorStack (size: %d)' % (len(ScissorStack.stack)-1)
|
||||
scissor_test(ScissorStack.scissor_test_was_enabled)
|
||||
ScissorStack.is_started = False
|
||||
ScissorStack.stack = None
|
||||
|
||||
@staticmethod
|
||||
def _set_scissor():
|
||||
assert ScissorStack.is_started, 'Attempting to set scissor settings with non-started ScissorStack'
|
||||
# print(f'ScissorStack: {ScissorStack.stack}')
|
||||
l,t,w,h = ScissorStack.stack[-1]
|
||||
b = t - (h - 1)
|
||||
scissor(l, b, w, h)
|
||||
get_glerror('scissor')
|
||||
|
||||
@staticmethod
|
||||
def push(nl, nt, nw, nh, msg='', clamp=True):
|
||||
# note: pos and size are already in region-coordinates, but it is specified from top-left corner
|
||||
|
||||
assert ScissorStack.is_started, 'Attempting to push to a non-started ScissorStack!'
|
||||
|
||||
if clamp:
|
||||
# get previous scissor box
|
||||
pl, pt, pw, ph = ScissorStack.stack[-1]
|
||||
pr = pl + (pw - 1)
|
||||
pb = pt - (ph - 1)
|
||||
# compute right and bottom of new scissor box
|
||||
nr = nl + (nw - 1)
|
||||
nb = nt - (nh - 1) - 1 # sub 1 (not certain why this needs to be)
|
||||
# compute clamped l,r,t,b,w,h
|
||||
cl, cr, ct, cb = mid(nl,pl,pr), mid(nr,pl,pr), mid(nt,pt,pb), mid(nb,pt,pb)
|
||||
cw, ch = max(0, cr - cl + 1), max(0, ct - cb + 1)
|
||||
ScissorStack.stack.append((int(cl), int(ct), int(cw), int(ch)))
|
||||
else:
|
||||
ScissorStack.stack.append((int(nl), int(nt), int(nw), int(nh)))
|
||||
ScissorStack.msg_stack.append(msg)
|
||||
|
||||
ScissorStack._set_scissor()
|
||||
|
||||
@staticmethod
|
||||
def pop():
|
||||
assert len(ScissorStack.stack) > 1, 'Attempting to pop from empty ScissorStack!'
|
||||
ScissorStack.stack.pop()
|
||||
ScissorStack.msg_stack.pop()
|
||||
ScissorStack._set_scissor()
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def wrap(*args, disabled=False, **kwargs):
|
||||
if disabled:
|
||||
yield None
|
||||
return
|
||||
try:
|
||||
ScissorStack.push(*args, **kwargs)
|
||||
yield None
|
||||
ScissorStack.pop()
|
||||
except Exception as e:
|
||||
ScissorStack.pop()
|
||||
print(f'Caught exception while scissoring')
|
||||
print(f'{args=} {kwargs=}')
|
||||
print(f'Exception: {e}')
|
||||
Globals.debugger.print_exception()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_current_view():
|
||||
assert ScissorStack.is_started
|
||||
assert ScissorStack.stack
|
||||
l, t, w, h = ScissorStack.stack[-1]
|
||||
#r, b = l + (w - 1), t - (h - 1)
|
||||
return (l, t, w, h)
|
||||
|
||||
@staticmethod
|
||||
def print_view_stack():
|
||||
for i,st in enumerate(ScissorStack.stack):
|
||||
l, t, w, h = st
|
||||
#r, b = l + (w - 1), t - (h - 1)
|
||||
print((' '*i) + str((l,t,w,h)) + ' ' + ScissorStack.msg_stack[i])
|
||||
|
||||
@staticmethod
|
||||
def is_visible():
|
||||
vl,vt,vw,vh = ScissorStack.get_current_view()
|
||||
return vw > 0 and vh > 0
|
||||
|
||||
@staticmethod
|
||||
def is_box_visible(l, t, w, h):
|
||||
if w <= 0 or h <= 0: return False
|
||||
vl, vt, vw, vh = ScissorStack.get_current_view()
|
||||
if vw <= 0 or vh <= 0: return False
|
||||
vr, vb = vl + (vw - 1), vt - (vh - 1)
|
||||
r, b = l + (w - 1), t - (h - 1)
|
||||
return not (l > vr or r < vl or t < vb or b > vt)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#######################################
|
||||
# gather gpu information
|
||||
|
||||
# https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glGetString.xml
|
||||
@only_in_blender_version('< 3.0')
|
||||
def gpu_info():
|
||||
import bgl
|
||||
return {
|
||||
'vendor': bgl.glGetString(bgl.GL_VENDOR),
|
||||
'renderer': bgl.glGetString(bgl.GL_RENDERER),
|
||||
'version': bgl.glGetString(bgl.GL_VERSION),
|
||||
'shading': bgl.glGetString(bgl.GL_SHADING_LANGUAGE_VERSION),
|
||||
}
|
||||
|
||||
@only_in_blender_version('>= 3.0', '< 3.4')
|
||||
def gpu_info():
|
||||
return {
|
||||
'vendor': gpu.platform.vendor_get(),
|
||||
'renderer': gpu.platform.renderer_get(),
|
||||
'version': gpu.platform.version_get(),
|
||||
}
|
||||
|
||||
@only_in_blender_version('>= 3.4')
|
||||
def gpu_info():
|
||||
platform = {
|
||||
'backend': gpu.platform.backend_type_get(),
|
||||
'device': gpu.platform.device_type_get(),
|
||||
'vendor': gpu.platform.vendor_get(),
|
||||
'renderer': gpu.platform.renderer_get(),
|
||||
'version': gpu.platform.version_get(),
|
||||
}
|
||||
cap = [(a, getattr(gpu.capabilities, a)) for a in dir(gpu.capabilities) if 'extensions' not in a]
|
||||
cap = [(a, fn) for (a, fn) in cap if isroutine(fn)]
|
||||
capabilities = {}
|
||||
for (a, fn) in cap:
|
||||
try: capabilities[a] = fn()
|
||||
except: pass
|
||||
return platform | capabilities
|
||||
|
||||
if not bpy.app.background:
|
||||
print(f'Addon Common: {gpu_info()}')
|
||||
|
||||
|
||||
####################################
|
||||
# helper functions
|
||||
|
||||
@contextmanager
|
||||
@add_cache('_buffers', dict())
|
||||
def bgl_get_temp_buffer(type_str, size):
|
||||
import bgl
|
||||
bufs, key = bgl_get_temp_buffer._buffers, (type_str, size)
|
||||
if key not in bufs:
|
||||
bufs[key] = bgl.Buffer(getattr(bgl, type_str), size)
|
||||
yield bufs[key]
|
||||
|
||||
def bgl_get_integerv(pname_str, *, type_str='GL_INT'):
|
||||
import bgl
|
||||
with bgl_get_temp_buffer(type_str, 1) as buf:
|
||||
bgl.glGetIntegerv(getattr(bgl, pname_str), buf)
|
||||
return buf[0]
|
||||
|
||||
def bgl_get_integerv_tuple(pname_str, size, *, type_str='GL_INT'):
|
||||
import bgl
|
||||
with bgl_get_temp_buffer(type_str, size) as buf:
|
||||
bgl.glGetIntegerv(getattr(bgl, pname_str), buf)
|
||||
return tuple(buf)
|
||||
|
||||
def bgl_is_enabled(pname_str):
|
||||
import bgl
|
||||
return (bgl.glIsEnabled(getattr(bgl, pname_str)) == bgl.GL_TRUE)
|
||||
|
||||
def bgl_enable(pname_str, enabled):
|
||||
import bgl
|
||||
pname = getattr(bgl, pname_str)
|
||||
if enabled: bgl.glEnable(pname)
|
||||
else: bgl.glDisable(pname)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import time
|
||||
from struct import pack
|
||||
from hashlib import md5
|
||||
|
||||
import bpy
|
||||
from bmesh.types import BMesh
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .maths import (
|
||||
Point, Direction, Normal, Frame,
|
||||
Point2D, Vec2D, Direction2D,
|
||||
Ray, XForm, BBox, Plane,
|
||||
Color
|
||||
)
|
||||
|
||||
|
||||
known_hash_types = {
|
||||
str, type(None), dict
|
||||
}
|
||||
|
||||
class Hasher:
|
||||
def __init__(self, *args):
|
||||
self._hasher = md5()
|
||||
self._digest = None
|
||||
self.add(*args)
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.add(other)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return '<Hasher %s>' % str(self.get_hash())
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.get_hash())
|
||||
|
||||
list_like_types = {
|
||||
list: 'list',
|
||||
tuple: 'tuple',
|
||||
set: 'set',
|
||||
}
|
||||
def add(self, *args):
|
||||
self._digest = None
|
||||
llt = Hasher.list_like_types
|
||||
for arg in args:
|
||||
t = type(arg)
|
||||
if t is Vector:
|
||||
self._hasher.update(bytes(f'Vector {len(arg)}', 'utf8'))
|
||||
self.add(*arg)
|
||||
elif t is Matrix:
|
||||
l0 = len(arg)
|
||||
l1 = len(arg[0])
|
||||
self._hasher.update(bytes(f'Matrix {l0} {l1}', 'utf8'))
|
||||
self.add_list([v for r in arg for v in r])
|
||||
elif t is Color:
|
||||
self._hasher.update(bytes(f'Color', 'utf8'))
|
||||
self.add_list([arg.r, arg.g, arg.b, arg.a])
|
||||
elif t in llt:
|
||||
self._hasher.update(bytes(f'{llt[t]} {len(arg)}', 'utf8'))
|
||||
self.add_list(arg)
|
||||
elif t is int:
|
||||
self._hasher.update(pack('i', arg))
|
||||
elif t is float:
|
||||
self._hasher.update(pack('f', arg))
|
||||
elif t is bool:
|
||||
self._hasher.update(pack('b', arg))
|
||||
elif t in known_hash_types:
|
||||
self._hasher.update(bytes(str(arg), 'utf8'))
|
||||
else:
|
||||
# unknown type. still works, but might want to know about it
|
||||
# to handle special cases
|
||||
# print(f'Hasher.add: {arg} {t}')
|
||||
self._hasher.update(bytes(str(arg), 'utf8'))
|
||||
|
||||
def add_list(self, args):
|
||||
for arg in args: self.add(arg)
|
||||
|
||||
def get_hash(self):
|
||||
if self._digest is None:
|
||||
self._digest = self._hasher.hexdigest()
|
||||
return self._digest
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is not Hasher: return False
|
||||
return self.get_hash() == other.get_hash()
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def hash_cycle(cycle):
|
||||
l = len(cycle)
|
||||
h = [hash(v) for v in cycle]
|
||||
m = min(h)
|
||||
mi = h.index(m)
|
||||
h = rotate_cycle(h, -mi)
|
||||
if h[1] > h[-1]:
|
||||
h.reverse()
|
||||
h = rotate_cycle(h, 1)
|
||||
return ' '.join(str(c) for c in h)
|
||||
|
||||
|
||||
def hash_object(obj:bpy.types.Object):
|
||||
if obj is None: return None
|
||||
assert type(obj) is bpy.types.Object, "Only call hash_object on mesh objects!"
|
||||
assert type(obj.data) is bpy.types.Mesh, "Only call hash_object on mesh objects!"
|
||||
# print(f'RetopoFlow: Hashing object {obj.name}...')
|
||||
t = time.time()
|
||||
# get object data to act as a hash
|
||||
me = obj.data
|
||||
counts = (len(me.vertices), len(me.edges), len(me.polygons), len(obj.modifiers))
|
||||
bbox = obj.bound_box
|
||||
bbox = (
|
||||
(min(c[0] for c in bbox), min(c[1] for c in bbox), min(c[2] for c in bbox)),
|
||||
(max(c[0] for c in bbox), max(c[1] for c in bbox), max(c[2] for c in bbox)),
|
||||
)
|
||||
vsum = tuple(sum((v.co for v in me.vertices), Vector((0,0,0))))
|
||||
xform = tuple(e for l in obj.matrix_world for e in l)
|
||||
mods = []
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'SUBSURF':
|
||||
mods += [('SUBSURF', mod.levels)]
|
||||
elif mod.type == 'DECIMATE':
|
||||
mods += [('DECIMATE', mod.ratio)]
|
||||
else:
|
||||
mods += [(mod.type)]
|
||||
hashed = (counts, bbox, vsum, xform, hash(obj), str(mods)) # ob.name???
|
||||
# print(f' hash: {hashed}')
|
||||
# print(f' time: {time.time() - t}')
|
||||
return hashed
|
||||
|
||||
def hash_bmesh(bme:BMesh):
|
||||
if bme is None: return None
|
||||
assert type(bme) is BMesh, 'Only call hash_bmesh on BMesh objects!'
|
||||
|
||||
# bme.verts.ensure_lookup_table()
|
||||
# bme.edges.ensure_lookup_table()
|
||||
# bme.faces.ensure_lookup_table()
|
||||
# return Hasher(
|
||||
# [list(v.co) + list(v.normal) + [v.select] for v in bme.verts],
|
||||
# [[v.index for v in e.verts] + [e.select] for e in bme.edges],
|
||||
# [[v.index for v in f.verts] + [f.select] for f in bme.faces],
|
||||
# )
|
||||
|
||||
counts = (len(bme.verts), len(bme.edges), len(bme.faces))
|
||||
bbox = BBox(from_bmverts=bme.verts)
|
||||
vsum = tuple(sum((v.co for v in bme.verts), Vector((0,0,0))))
|
||||
hashed = (counts, tuple(bbox.min) if bbox.min else None, tuple(bbox.max) if bbox.max else None, vsum)
|
||||
return hashed
|
||||
@@ -0,0 +1,46 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
no_arrows = {
|
||||
' ': ' ',
|
||||
'`': '`',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
# '→': '→',
|
||||
}
|
||||
|
||||
arrows = { # https://www.toptal.com/designers/htmlarrows/arrows/
|
||||
'↑': '↑',
|
||||
'↓': '↓',
|
||||
'←': '←',
|
||||
'→': '→',
|
||||
'↔': '↔',
|
||||
'↕': '↕',
|
||||
'⇑': '⇑',
|
||||
'⇓': '⇓',
|
||||
'⇐': '⇐',
|
||||
'⇒': '⇒',
|
||||
'⇔': '⇔',
|
||||
'⇕': '⇕',
|
||||
}
|
||||
|
||||
all_chars = no_arrows | arrows
|
||||
@@ -0,0 +1,197 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import platform
|
||||
|
||||
# these are separated into a list so that "SHIFT+ZERO" (for example) is handled
|
||||
# before the "SHIFT" gets turned into "Shift"
|
||||
kmi_to_humanreadable = [
|
||||
{
|
||||
# shifted top-row numbers
|
||||
'SHIFT+ZERO': ')',
|
||||
'SHIFT+ONE': '!',
|
||||
'SHIFT+TWO': '@',
|
||||
'SHIFT+THREE': '#',
|
||||
'SHIFT+FOUR': '$',
|
||||
'SHIFT+FIVE': '%',
|
||||
'SHIFT+SIX': '^',
|
||||
'SHIFT+SEVEN': '&',
|
||||
'SHIFT+EIGHT': '*',
|
||||
'SHIFT+NINE': '(',
|
||||
|
||||
# shifted punctuation
|
||||
'SHIFT+PERIOD': '>',
|
||||
'SHIFT+PLUS': '+',
|
||||
'SHIFT+MINUS': '_',
|
||||
'SHIFT+SLASH': '?',
|
||||
'SHIFT+BACK_SLASH': '|',
|
||||
'SHIFT+EQUAL': '+',
|
||||
'SHIFT+SEMI_COLON': ':',
|
||||
'SHIFT+COMMA': '<',
|
||||
'SHIFT+LEFT_BRACKET': '{',
|
||||
'SHIFT+RIGHT_BRACKET': '}',
|
||||
'SHIFT+QUOTE': '"',
|
||||
'SHIFT+ACCENT_GRAVE': '~',
|
||||
},{
|
||||
# numpad numbers
|
||||
'NUMPAD_PERIOD': 'Num.',
|
||||
'NUMPAD_PLUS': 'Num+',
|
||||
'NUMPAD_MINUS': 'Num-',
|
||||
'NUMPAD_SLASH': 'Num/',
|
||||
'NUMPAD_ASTERIX': 'Num*',
|
||||
|
||||
# numpad operators
|
||||
'NUMPAD_PERIOD': 'Num.',
|
||||
'NUMPAD_PLUS': 'Num+',
|
||||
'NUMPAD_MINUS': 'Num-',
|
||||
'NUMPAD_SLASH': 'Num/',
|
||||
'NUMPAD_ASTERIX': 'Num*',
|
||||
|
||||
# numpad enter
|
||||
'NUMPAD_ENTER': 'NumEnter',
|
||||
},{
|
||||
'BACK_SLASH': '\\',
|
||||
},{
|
||||
# top-row numbers
|
||||
'ZERO': '0',
|
||||
'ONE': '1',
|
||||
'TWO': '2',
|
||||
'THREE': '3',
|
||||
'FOUR': '4',
|
||||
'FIVE': '5',
|
||||
'SIX': '6',
|
||||
'SEVEN': '7',
|
||||
'EIGHT': '8',
|
||||
'NINE': '9',
|
||||
|
||||
# operators
|
||||
'PERIOD': '.',
|
||||
'PLUS': '+',
|
||||
'MINUS': '-',
|
||||
'SLASH': '/',
|
||||
|
||||
# characters that are easier to read as symbols than as their name
|
||||
'EQUAL': '=',
|
||||
'SEMI_COLON': ';',
|
||||
'COMMA': ',',
|
||||
'LEFT_BRACKET': '[',
|
||||
'RIGHT_BRACKET': ']',
|
||||
'QUOTE': "'",
|
||||
'ACCENT_GRAVE': '`', #'`',
|
||||
|
||||
# non-printable characters
|
||||
'ESC': 'Escape',
|
||||
'BACK_SPACE': 'Backspace',
|
||||
'RET': 'Enter',
|
||||
'HOME': 'Home',
|
||||
'END': 'End',
|
||||
'LEFT_ARROW': 'ArrowLeft',
|
||||
'RIGHT_ARROW': 'ArrowRight',
|
||||
'UP_ARROW': 'ArrowUp',
|
||||
'DOWN_ARROW': 'ArrowDown',
|
||||
'PAGE_UP': 'PageUp',
|
||||
'PAGE_DOWN': 'PageDown',
|
||||
'INSERT': 'Insert',
|
||||
'DEL': 'Delete',
|
||||
'TAB': 'Tab',
|
||||
|
||||
# mouse actions
|
||||
'LEFTMOUSE': 'LMB',
|
||||
'MIDDLEMOUSE': 'MMB',
|
||||
'RIGHTMOUSE': 'RMB',
|
||||
'WHEELUPMOUSE': 'WheelUp',
|
||||
'WHEELDOWNMOUSE': 'WheelDown',
|
||||
|
||||
# postfix modifiers
|
||||
'DRAG': 'Drag',
|
||||
'DOUBLE': 'Double',
|
||||
'CLICK': 'Click',
|
||||
},{
|
||||
'SPACE': 'Space',
|
||||
}
|
||||
]
|
||||
|
||||
# platform-specific prefix modifiers
|
||||
if platform.system() == 'Darwin':
|
||||
kmi_to_humanreadable += [{
|
||||
'SHIFT': '⇧ Shift',
|
||||
'CTRL': '^ Ctrl',
|
||||
'ALT': '⌥ Opt',
|
||||
'OSKEY': '⌘ Cmd',
|
||||
}]
|
||||
else:
|
||||
kmi_to_humanreadable += [{
|
||||
'SHIFT': 'Shift',
|
||||
'CTRL': 'Ctrl',
|
||||
'ALT': 'Alt',
|
||||
'OSKEY': 'OSKey',
|
||||
}]
|
||||
|
||||
|
||||
# reversed human readable dict
|
||||
humanreadable_to_kmi = [ { v:k for (k,v) in s.items() } for s in reversed(kmi_to_humanreadable) ]
|
||||
# | {'Space': 'SPACE'} # does not work in Blender 2.92
|
||||
humanreadable_to_kmi += [{'Space': 'SPACE'}]
|
||||
|
||||
|
||||
html_char = {
|
||||
'`': '`',
|
||||
}
|
||||
|
||||
visible_char = {
|
||||
' ': 'Space',
|
||||
}
|
||||
|
||||
def convert_actions_to_human_readable(actions, *, sep=',', onlyfirst=None, translate_html_char=False, visible=False):
|
||||
ret = set()
|
||||
if type(actions) is str: actions = {actions}
|
||||
for action in actions:
|
||||
for kmi2hr in kmi_to_humanreadable:
|
||||
for k,v in kmi2hr.items():
|
||||
action = action.replace(k, v)
|
||||
ret.add(action)
|
||||
if visible:
|
||||
ret = { visible_char.get(r, r) for r in ret }
|
||||
if translate_html_char:
|
||||
for k,v in html_char.items():
|
||||
ret = {r.replace(k,v) for r in ret}
|
||||
ret = sorted(ret)
|
||||
if onlyfirst is not None: ret = ret[:onlyfirst]
|
||||
return sep.join(ret)
|
||||
|
||||
def convert_human_readable_to_actions(actions):
|
||||
ret = []
|
||||
if type(actions) is str: actions = [actions]
|
||||
for action in actions:
|
||||
if platform.system() == 'Darwin':
|
||||
action = action.replace('^ Ctrl+', 'CTRL+')
|
||||
action = action.replace('⇧ Shift+', 'SHIFT+')
|
||||
action = action.replace('⌥ Opt+', 'ALT+')
|
||||
action = action.replace('⌘ Cmd+', 'OSKEY+')
|
||||
else:
|
||||
action = action.replace('Ctrl+', 'CTRL+')
|
||||
action = action.replace('Shift+', 'SHIFT+')
|
||||
action = action.replace('Alt+', 'ALT+')
|
||||
action = action.replace('Cmd+', 'OSKEY+')
|
||||
for hr2kmi in humanreadable_to_kmi:
|
||||
kmi = hr2kmi.get(action, action)
|
||||
ret.append(kmi)
|
||||
return ret
|
||||
@@ -0,0 +1,102 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import glob
|
||||
import atexit
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
|
||||
|
||||
from .blender import get_path_from_addon_root
|
||||
from .ui_core_images import preload_image, set_image_cache
|
||||
|
||||
|
||||
# preload images to view faster
|
||||
class ImagePreloader:
|
||||
_paused = False
|
||||
_quitted = False
|
||||
|
||||
@classmethod
|
||||
def pause(cls): cls._paused = True
|
||||
@classmethod
|
||||
def resume(cls): cls._paused = False
|
||||
@classmethod
|
||||
def paused(cls): return cls._paused
|
||||
|
||||
@classmethod
|
||||
def quit(cls): cls._quitted = True
|
||||
@classmethod
|
||||
def quitted(cls): return cls._quitted
|
||||
|
||||
@classmethod
|
||||
def start(cls, paths, *, version='thread'):
|
||||
path_images = []
|
||||
|
||||
path_cur = os.getcwd()
|
||||
for path in paths:
|
||||
os.chdir(get_path_from_addon_root(*path))
|
||||
path_images.extend(glob.glob('*.png'))
|
||||
os.chdir(path_cur)
|
||||
|
||||
match version:
|
||||
case 'process':
|
||||
# this version spins up new Processes, so Python's GIL isn't an issue
|
||||
# :) loading is much FASTER! (truly parallel loading)
|
||||
# :( DIFFICULT to pause or abort (no shared resources)
|
||||
def setter(p):
|
||||
if cls.quitted(): return
|
||||
for path_image, img in p.result():
|
||||
if img is None: continue
|
||||
print(f'CookieCutter: {path_image} is preloaded')
|
||||
set_image_cache(path_image, img)
|
||||
executor = ProcessPoolExecutor() # ThreadPoolExecutor()
|
||||
for path_image in path_images:
|
||||
p = executor.submit(preload_image, path_image)
|
||||
p.add_done_callback(setter)
|
||||
def abort():
|
||||
nonlocal executor
|
||||
cls.quit()
|
||||
# the following line causes a crash :(
|
||||
# executor.shutdown(wait=False)
|
||||
atexit.register(abort)
|
||||
|
||||
case 'thread':
|
||||
# this version spins up new Threads, so Python's GIL is used
|
||||
# :( loading is much SLOWER! (serial loading)
|
||||
# :) EASY to pause and abort (shared resources)
|
||||
def abort():
|
||||
cls.quit()
|
||||
atexit.register(abort)
|
||||
def start():
|
||||
for png in path_images:
|
||||
print(f'CookieCutter: preloading image "{png}"')
|
||||
preload_image(png)
|
||||
time.sleep(0.5)
|
||||
for loop in range(10):
|
||||
if not cls.paused(): break
|
||||
if cls.quitted(): break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
# if looped too many times, just quit
|
||||
return
|
||||
if cls.quitted(): return
|
||||
print(f'CookieCutter: all images preloaded')
|
||||
ThreadPoolExecutor().submit(start)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import inspect
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
class ScopeBuilder(MutableMapping):
|
||||
"""
|
||||
A dictionary-like object that mimics frame.f_locals
|
||||
- builds up a custom locals mapping based on current f_locals
|
||||
- names can be transformed (ex: a local x can be referred using y in nonlocal)
|
||||
- value getting is lazy (no need to capture after variable has been assigned to)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_frame(frame_depth):
|
||||
frame = inspect.currentframe()
|
||||
for i in range(frame_depth):
|
||||
frame = frame.f_back
|
||||
return frame
|
||||
|
||||
def __init__(self, *args, frame_depth=1, **kwargs):
|
||||
self.__store = dict()
|
||||
|
||||
frame = self.get_frame(frame_depth + 1)
|
||||
for nonlocalname in args:
|
||||
localname = nonlocalname
|
||||
self.__store[nonlocalname] = (frame, localname)
|
||||
for nonlocalname, localname in kwargs.items():
|
||||
self.__store[nonlocalname] = (frame, localname)
|
||||
|
||||
def items(self):
|
||||
return { nonlocalname: self[nonlocalname] for nonlocalname in self }
|
||||
|
||||
def __getitem__(self, nonlocalname):
|
||||
(frame, localname) = self.__store[nonlocalname]
|
||||
if localname in frame.f_locals: return frame.f_locals[localname]
|
||||
if localname in frame.f_globals: return frame.f_globals[localname]
|
||||
assert False, f'Could not find {localname} in locals or globals of {frame}'
|
||||
|
||||
def __setitem__(self, nonlocalname, localname):
|
||||
frame = self.get_frame(2)
|
||||
self.__store[nonlocalname] = (frame, localname)
|
||||
|
||||
def __delitem__(self, nonlocalname):
|
||||
del self.__store[nonlocalname]
|
||||
|
||||
def keys(self):
|
||||
return self.__store.keys()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__store)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__store)
|
||||
|
||||
def _keytransform(self, key):
|
||||
return key
|
||||
|
||||
def capture_fn(self, arg, *, frame_depth=1):
|
||||
frame = self.get_frame(frame_depth + 1)
|
||||
if inspect.isfunction(arg):
|
||||
fn = arg
|
||||
nonlocalname = fn.__name__
|
||||
localname = fn.__name__
|
||||
self.__store[nonlocalname] = (frame, localname)
|
||||
return fn
|
||||
|
||||
nonlocalname = arg
|
||||
def cb(fn):
|
||||
localname = fn.__name__
|
||||
self.__store[nonlocalname] = (frame, localname)
|
||||
return fn
|
||||
return cb
|
||||
|
||||
def capture_var(self, nonlocalname, /, localname=None, *, frame_depth=1):
|
||||
frame = self.get_frame(frame_depth + 1)
|
||||
self.__store[nonlocalname] = (frame, localname or nonlocalname)
|
||||
|
||||
|
||||
# class CaptureLocals(dict):
|
||||
# def __init__(self, *args, frame_depth=1, **kwargs):
|
||||
# self.__frame = inspect.currentframe()
|
||||
# for i in range(frame_depth):
|
||||
# self.__frame = self.__frame.f_back
|
||||
# for arg in args: self.capture(arg)
|
||||
# for k, v in kwargs.items(): self.capture(v, k)
|
||||
|
||||
# def capture(self, var, as_var=None):
|
||||
# self[as_var or var] = self.__frame.f_locals[var]
|
||||
|
||||
# def capture_fn(self, fn):
|
||||
# self[fn.__name__] = fn
|
||||
# return fn
|
||||
|
||||
def capture_locals(*args, frame_depth=1, **kwargs):
|
||||
frame = inspect.currentframe()
|
||||
for i in range(frame_depth): frame = frame.f_back
|
||||
f_locals = {}
|
||||
for arg in args:
|
||||
f_locals[arg] = frame.f_locals[arg]
|
||||
for k, v in kwargs.items():
|
||||
f_locals[k] = frame.f_locals[v]
|
||||
return f_locals
|
||||
@@ -0,0 +1,80 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import socket
|
||||
import sys
|
||||
|
||||
'''
|
||||
Note: this is a work in progress only!
|
||||
'''
|
||||
|
||||
# https://pythonspot.com/building-an-irc-bot/
|
||||
class IRC:
|
||||
def __init__(self):
|
||||
self.done = False
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def send_text(self, text):
|
||||
if not text.endswith('\n'): text += '\n'
|
||||
self.socket.send(bytes(text, encoding='utf-8'))
|
||||
|
||||
def send(self, chan, msg):
|
||||
self.socket.send(bytes("PRIVMSG " + chan + " :" + msg + "\n", encoding='utf-8'))
|
||||
|
||||
def connect(self, server, channel, nickname):
|
||||
#defines the socket
|
||||
print("connecting to:", server)
|
||||
self.socket.connect((server, 6667)) #connects to the server
|
||||
self.socket.send(bytes("USER " + nickname + " " + nickname +" " + nickname + " :This is a fun bot!\n", encoding='utf-8')) #user authentication
|
||||
self.socket.send(bytes("NICK " + nickname + "\n", encoding='utf-8'))
|
||||
self.socket.send(bytes("JOIN " + channel + "\n", encoding='utf-8')) #join the chan
|
||||
|
||||
def get_text(self, blocking=True):
|
||||
self.socket.setblocking(blocking)
|
||||
try:
|
||||
text = str(self.socket.recv(4096), encoding='utf-8') #receive the text
|
||||
except socket.error:
|
||||
text = None
|
||||
return text
|
||||
|
||||
def close(self):
|
||||
if self.done: return
|
||||
self.socket.close()
|
||||
self.done = True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
channel = "#retopoflow"
|
||||
server = "irc.freenode.net"
|
||||
nickname = "rftester"
|
||||
|
||||
irc = IRC()
|
||||
irc.connect(server, channel, nickname)
|
||||
|
||||
while 1:
|
||||
text = irc.get_text()
|
||||
if text: print(text)
|
||||
|
||||
if "PRIVMSG" in text and channel in text and "hello" in text:
|
||||
irc.send(channel, "Hello!")
|
||||
@@ -0,0 +1,78 @@
|
||||
'''
|
||||
Copyright (C) 2014 Plasmasolutions
|
||||
software@plasmasolutions.de
|
||||
|
||||
Created by Thomas Beck
|
||||
Donated to CGCookie and the world
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
|
||||
from .blender import show_blender_popup, show_blender_text
|
||||
|
||||
from .globals import Globals
|
||||
|
||||
class Logger:
|
||||
_log_filename = 'Logger'
|
||||
_divider = '\n\n%s\n' % ('='*80)
|
||||
|
||||
@staticmethod
|
||||
def set_log_filename(path):
|
||||
Logger._log_filename = path
|
||||
|
||||
@staticmethod
|
||||
def get_log_filename():
|
||||
return Logger._log_filename
|
||||
|
||||
@staticmethod
|
||||
def get_log(create=True):
|
||||
if Logger._log_filename not in bpy.data.texts:
|
||||
if not create: return None
|
||||
old = { t.name for t in bpy.data.texts }
|
||||
# create a log file for recording
|
||||
bpy.ops.text.new()
|
||||
for t in bpy.data.texts:
|
||||
if t.name in old: continue
|
||||
t.name = Logger._log_filename
|
||||
break
|
||||
else:
|
||||
assert False
|
||||
return bpy.data.texts[Logger._log_filename]
|
||||
|
||||
@staticmethod
|
||||
def has_log():
|
||||
return Logger.get_log(create=False) is not None
|
||||
|
||||
@staticmethod
|
||||
def add(line):
|
||||
try:
|
||||
log = Logger.get_log()
|
||||
log.write('%s%s' % (Logger._divider, str(line)))
|
||||
except Exception as e:
|
||||
print(f'Logger: Caught exception while trying to write to log')
|
||||
print(f' {line=}"')
|
||||
print(f' {e}')
|
||||
|
||||
@staticmethod
|
||||
def open_log():
|
||||
if Logger.has_log():
|
||||
show_blender_text(Logger._log_filename)
|
||||
else:
|
||||
show_blender_popup(f'Log file ({Logger._log_filename}) not found')
|
||||
|
||||
logger = Logger()
|
||||
Globals.set(logger)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
# markdown line (first line only, ex: table)
|
||||
line_tests = {
|
||||
'h1': re.compile(r'(?<!#)# +(?P<text>.+)'),
|
||||
'h2': re.compile(r'(?<!#)## +(?P<text>.+)'),
|
||||
'h3': re.compile(r'(?<!#)### +(?P<text>.+)'),
|
||||
'ul': re.compile(r'(?P<indent> *)- +(?P<text>.+)'),
|
||||
'ol': re.compile(r'(?P<indent> *)\d+\. +(?P<text>.+)'),
|
||||
'img': re.compile(r'!\[(?P<caption>[^\]]*)\]\((?P<filename>[^) ]+)(?P<style>[^)]*)\)'),
|
||||
'table': re.compile(r'\| +(([^|]*?) +\|)+'),
|
||||
}
|
||||
|
||||
# markdown inline
|
||||
inline_tests = {
|
||||
'br': re.compile(r'<br */?> *'),
|
||||
'img': re.compile(r'!\[(?P<caption>[^\]]*)\]\((?P<filename>[^) ]+)(?P<style>[^)]*)\)'),
|
||||
'bold': re.compile(r'\*(?P<text>.+?)\*'),
|
||||
'code': re.compile(r'`(?P<text>[^`]+)`'),
|
||||
'link': re.compile(r'\[(?P<text>.+?)\]\((?P<link>.+?)\)'),
|
||||
'italic': re.compile(r'_(?P<text>.+?)_'),
|
||||
'html': re.compile(r'''<((?P<tagname>[a-zA-Z]+)(?P<params>( +(?P<key>[a-zA-Z_]+(=(?P<val>"[^"]*"|'[^']*'|[^"' >]+))?)))*)(>(?P<contents>.*?)(?P<closetag></\2>)|(?P<selfclose> +/>))'''),
|
||||
# 'checkbox': re.compile(r'<input (?P<params>.*?type="checkbox".*?)>(?P<innertext>.*?)<\/input>'),
|
||||
# 'number': re.compile(r'<input (?P<params>.*?type="number".*?)>'),
|
||||
# 'button': re.compile(r'<button(?P<params>[^>]*)>(?P<innertext>.*?)<\/button>'),
|
||||
# 'progress': re.compile(r'<progress(?P<params>.*?)(>(?P<innertext>.*?)<\/progress>| \/>)'),
|
||||
|
||||
# https://www.toptal.com/designers/htmlarrows/arrows/
|
||||
'arrow': re.compile(r'&(?P<dir>uarr|darr|larr|rarr|harr|varr|uArr|dArr|lArr|rArr|hArr|vArr); *'),
|
||||
}
|
||||
|
||||
# process markdown text similarly to Markdown
|
||||
preprocessing = [
|
||||
(r'<!--.*?-->', r''), # remove comments
|
||||
(r'^\n*', r''), # remove leading \n
|
||||
(r'\n*$', r''), # remove trailing \n
|
||||
(r'\n\n\n*', r'\n\n'), # 2+ \n => \n\n
|
||||
(r'---', r'—'), # em dash
|
||||
(r'(?<!-)--', r'–'), # en dash
|
||||
]
|
||||
|
||||
# https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||
re_url = re.compile(r'^((https?)|mailto)://([-a-zA-Z0-9@:%._\+~#=]+\.)*?[-a-zA-Z0-9@:%._+~#=]+\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$')
|
||||
re_html_char = re.compile(r'(?P<pre>[^ ]*?)(?P<code>&([a-zA-Z]+|#x?[0-9A-Fa-f]+);)(?P<post>.*)')
|
||||
re_embedded_code = re.compile(r'(?P<pre>[^ `]+)(?P<code>`[^`]*`)(?P<post>.*)')
|
||||
|
||||
class Markdown:
|
||||
@staticmethod
|
||||
def preprocess(txt):
|
||||
for m,r in preprocessing:
|
||||
txt = re.sub(m, r, txt)
|
||||
return txt
|
||||
|
||||
@staticmethod
|
||||
def is_url(txt): return re_url.match(txt) is not None
|
||||
|
||||
@staticmethod
|
||||
def match_inline(line):
|
||||
#line = line.lstrip() # ignore leading spaces
|
||||
for (t,r) in inline_tests.items():
|
||||
m = r.match(line)
|
||||
if m: return (t, m)
|
||||
return (None, None)
|
||||
|
||||
@staticmethod
|
||||
def match_line(line):
|
||||
line = line.rstrip() # ignore trailing spaces
|
||||
for (t,r) in line_tests.items():
|
||||
m = r.match(line)
|
||||
if m: return (t, m)
|
||||
return (None, None)
|
||||
|
||||
@staticmethod
|
||||
def split_word(line, allow_empty_pre=False):
|
||||
# search for html characters, like
|
||||
m = re_html_char.match(line)
|
||||
if m:
|
||||
pr = m.group('pre')
|
||||
co = m.group('code')
|
||||
po = m.group('post')
|
||||
if co == ' ':
|
||||
# must get handled specially later!
|
||||
# for now, consider part of the pre
|
||||
npr,npo = Markdown.split_word(po, allow_empty_pre=True)
|
||||
return (f'{pr}{co}{npr}', npo)
|
||||
if pr or allow_empty_pre:
|
||||
return (pr, f'{co}{po}')
|
||||
return (co, po)
|
||||
# search for embedded code in word, like (`-`)
|
||||
m = re_embedded_code.match(line)
|
||||
if m:
|
||||
pr = m.group('pre')
|
||||
co = m.group('code')
|
||||
po = m.group('post')
|
||||
return (pr, f'{co}{po}')
|
||||
if ' ' not in line:
|
||||
return (line,'')
|
||||
i = line.index(' ') + 1
|
||||
return (line[:i],line[i:])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import random
|
||||
from math import sqrt, acos, cos, sin, floor, ceil, isinf, sqrt, pi, isnan, isfinite
|
||||
from typing import List
|
||||
from itertools import chain
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
import gpu
|
||||
from mathutils import Matrix, Vector, Quaternion
|
||||
from bmesh.types import BMVert
|
||||
from mathutils.geometry import intersect_line_plane, intersect_point_tri
|
||||
|
||||
from .maths import zero_threshold, BBox2D, Point2D, clamp, Vec2D, Vec, mid
|
||||
|
||||
from .colors import colorname_to_color
|
||||
from .decorators import stats_wrapper, blender_version_wrapper
|
||||
from .profiler import profiler, time_it
|
||||
|
||||
from ..terminal import term_printer
|
||||
|
||||
|
||||
class SimpleVert:
|
||||
def __init__(self, co):
|
||||
self.co = co
|
||||
self.normal = Vec((0, 0, 0))
|
||||
self.is_valid = True
|
||||
|
||||
class SimpleEdge:
|
||||
def __init__(self, verts):
|
||||
self.verts = verts
|
||||
self.p0 = verts[0].co
|
||||
self.p1 = verts[1].co
|
||||
self.v01 = self.p1 - self.p0
|
||||
self.l = self.v01.length
|
||||
self.d01 = self.v01 / max(self.l, zero_threshold)
|
||||
self.is_valid = True
|
||||
def closest(self, p):
|
||||
v0p = p - self.p0
|
||||
d = self.d01.dot(v0p)
|
||||
return self.p0 + self.d01 * mid(d, 0, self.l)
|
||||
|
||||
class Accel2D:
|
||||
margin = 0.001
|
||||
DEBUG = False
|
||||
|
||||
# @staticmethod
|
||||
# def simple_verts(label, lco, Point_to_Point2Ds):
|
||||
# verts = [ SimpleVert(co) for co in lco ]
|
||||
# return Accel2D(label, verts, [], [], Point_to_Point2Ds)
|
||||
|
||||
@staticmethod
|
||||
def simple_edges(label, edges, Point_to_Point2Ds):
|
||||
edges = [ SimpleEdge(( SimpleVert(co0), SimpleVert(co1) )) for (co0, co1) in edges ]
|
||||
verts = [ co for e in edges for co in e.verts ]
|
||||
return Accel2D(label, verts, edges, [], Point_to_Point2Ds)
|
||||
|
||||
def _insert_edge(self, edge):
|
||||
pts_list = zip(*[ self.Point_to_Point2Ds(v.co, v.normal) for v in edge.verts ])
|
||||
for co0, co1 in pts_list:
|
||||
(i0, j0), (i1, j1) = self.compute_ij(co0), self.compute_ij(co1)
|
||||
mini, minj, maxi, maxj = min(i0, i1), min(j0, j1), max(i0, i1), max(j0, j1)
|
||||
for i in range(mini, maxi + 1):
|
||||
for j in range(minj, maxj + 1):
|
||||
self._put((i, j), edge)
|
||||
|
||||
# @profiler.function
|
||||
def __init__(self, label, verts, edges, faces, Point_to_Point2Ds):
|
||||
self.verts = list(verts) if verts else []
|
||||
self.edges = list(edges) if edges else []
|
||||
self.faces = list(faces) if faces else []
|
||||
self.Point_to_Point2Ds = Point_to_Point2Ds
|
||||
|
||||
vert_type, edge_type, face_type = ( type(elems[0] if elems else None) for elems in [self.verts, self.edges, self.faces] )
|
||||
self._is_vert = lambda elem: isinstance(elem, vert_type)
|
||||
self._is_edge = lambda elem: isinstance(elem, edge_type)
|
||||
self._is_face = lambda elem: isinstance(elem, face_type)
|
||||
self.bins = {}
|
||||
|
||||
# collect all involved pts so we can find bbox
|
||||
with time_it('collect', enabled=Accel2D.DEBUG):
|
||||
bbox = BBox2D()
|
||||
with time_it('collect verts', enabled=Accel2D.DEBUG):
|
||||
bbox.insert_points(pt for v in verts for pt in Point_to_Point2Ds(v.co, v.normal))
|
||||
with time_it('collect edges and faces', enabled=Accel2D.DEBUG):
|
||||
bbox.insert_points(
|
||||
pt
|
||||
for ef in chain(edges, faces)
|
||||
for ef_pts in zip(*[Point_to_Point2Ds(v.co, v.normal) for v in ef.verts])
|
||||
for pt in ef_pts
|
||||
)
|
||||
if bbox.count == 0:
|
||||
bbox.insert(Point2D((0,0)))
|
||||
|
||||
tot_points = len(self.verts) + 2 * len(self.edges) + sum(len(f.verts) for f in self.faces)
|
||||
|
||||
self.min = Point2D((bbox.mx - self.margin, bbox.my - self.margin))
|
||||
self.max = Point2D((bbox.Mx + self.margin, bbox.My + self.margin))
|
||||
self.size = self.max - self.min # includes margin
|
||||
self.sizex, self.sizey = self.size
|
||||
self.minx, self.miny = self.min
|
||||
self.bin_len = ceil(sqrt(tot_points) + 0.1)
|
||||
|
||||
# Accel2D.debug variables
|
||||
tot_inserted = 0
|
||||
max_spread = (1, 1, 1)
|
||||
|
||||
# inserting verts
|
||||
with time_it('insert verts', enabled=Accel2D.DEBUG):
|
||||
for v in verts:
|
||||
for pt in Point_to_Point2Ds(v.co, v.normal):
|
||||
tot_inserted += 1
|
||||
i, j = self.compute_ij(pt)
|
||||
self._put((i, j), v)
|
||||
|
||||
# inserting edges and faces
|
||||
with time_it('insert edges and faces', enabled=Accel2D.DEBUG):
|
||||
for e in edges:
|
||||
self._insert_edge(e)
|
||||
for ef in faces:
|
||||
ef_pts_list = zip(*[Point_to_Point2Ds(v.co, v.normal) for v in ef.verts])
|
||||
for ef_pts in ef_pts_list:
|
||||
tot_inserted += 1
|
||||
bbox2 = BBox2D((self.compute_ij(pt) for pt in ef_pts))
|
||||
mini, minj, maxi, maxj = int(bbox2.mx), int(bbox2.my), int(bbox2.Mx), int(bbox2.My)
|
||||
sizei, sizej = maxi - mini + 1, maxj - minj + 1
|
||||
if (spread := sizei*sizej) > max_spread[0]: max_spread = (spread, sizei, sizej)
|
||||
for i in range(mini, maxi + 1):
|
||||
for j in range(minj, maxj + 1):
|
||||
self._put((i, j), ef)
|
||||
|
||||
if Accel2D.DEBUG:
|
||||
# debug reporting
|
||||
def get_index(s, v, m, M): return clamp(int(len(s) * (v - m) / max(1, M - m)), 0, len(s) - 1)
|
||||
fill_max = max((len(b) for b in self.bins.values()), default=0)
|
||||
fill_min = min((len(b) for b in self.bins.values()), default=0)
|
||||
distribution = [0] * min(100, self.bin_len * self.bin_len)
|
||||
for b in self.bins.values():
|
||||
distribution[get_index(distribution, len(b), fill_min, fill_max)] += 1
|
||||
filling_max = max(distribution)
|
||||
chars = '_▁▂▃▄▅▆▇█' # https://en.wikipedia.org/wiki/Block_Elements
|
||||
def get_char(v): return chars[get_index(chars, v, 0, filling_max)] if v else ' '
|
||||
distribution = ''.join(get_char(v) for v in distribution)
|
||||
term_printer.boxed(
|
||||
f'Counts: v={len(self.verts)} e={len(self.edges)} f={len(self.faces)}',
|
||||
f' total pts={tot_points}, bbox ins={bbox.count}, accel ins={tot_inserted}',
|
||||
f'Size: min={self.min}, max={self.max} size={self.size}',
|
||||
f'Bins: {self.bin_len}x{self.bin_len} non-zero={len(self.bins)}/{self.bin_len*self.bin_len} ({100*len(self.bins)/(self.bin_len*self.bin_len):0.0f}%)',
|
||||
f'Inserts: total={tot_inserted}, max spread={max_spread}',
|
||||
f'Fill: {fill_min} [{distribution}] {fill_max}',
|
||||
title=f'Accel2D: {label}', color='black', highlight='green',
|
||||
)
|
||||
|
||||
# @profiler.function
|
||||
def compute_ij(self, v2d):
|
||||
bl = self.bin_len
|
||||
return (
|
||||
clamp(int(bl * (v2d.x - self.minx) / self.sizex), 0, bl - 1),
|
||||
clamp(int(bl * (v2d.y - self.miny) / self.sizey), 0, bl - 1)
|
||||
)
|
||||
|
||||
def _put(self, ij, o):
|
||||
# assert 0 <= ij[0] < self.bin_len and 0 <= ij[1] < self.bin_len, f'{ij} is outside {self.bin_len}x{self.bin_len}'
|
||||
if ij in self.bins: self.bins[ij].add(o)
|
||||
else: self.bins[ij] = { o }
|
||||
|
||||
def _get(self, ij):
|
||||
return self.bins[ij] if ij in self.bins else set()
|
||||
|
||||
# @profiler.function
|
||||
def clean_invalid(self):
|
||||
self.bins = {
|
||||
t: {o for o in objs if o.is_valid}
|
||||
for (t, objs) in self.bins.items()
|
||||
}
|
||||
|
||||
# @profiler.function
|
||||
def get(self, v2d, within, *, fn_filter=None):
|
||||
if v2d is None or not (isfinite(v2d.x) and isfinite(v2d.y)): return set()
|
||||
delta = Vec2D((within, within))
|
||||
p0, p1 = v2d - delta, v2d + delta
|
||||
i0, j0 = self.compute_ij(p0)
|
||||
i1, j1 = self.compute_ij(p1)
|
||||
ret = {
|
||||
elem
|
||||
for i in range(i0, i1+1)
|
||||
for j in range(j0, j1+1)
|
||||
for elem in self._get((i, j))
|
||||
if elem.is_valid and (fn_filter is None or fn_filter(elem))
|
||||
}
|
||||
return ret
|
||||
|
||||
# @profiler.function
|
||||
def get_verts(self, v2d, within):
|
||||
return self.get(v2d, within, fn_filter=self._is_vert)
|
||||
|
||||
# @profiler.function
|
||||
def get_edges(self, v2d, within):
|
||||
return self.get(v2d, within, fn_filter=self._is_edge)
|
||||
|
||||
# @profiler.function
|
||||
def get_faces(self, v2d, within):
|
||||
return self.get(v2d, within, fn_filter=self._is_face)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'''
|
||||
Copyright (C) 2023 Taylor University, CG Cookie
|
||||
|
||||
Created by Dr. Jon Denning and Spring 2015 COS 424 class
|
||||
|
||||
Some code copied from CG Cookie Retopoflow project
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
'''
|
||||
RegisterRFClasses handles self registering classes to simplify creating new tools, cursors, etc.
|
||||
With self registration, the new entities only need to by imported in, and they automatically
|
||||
show up as an available entity.
|
||||
'''
|
||||
|
||||
|
||||
class SingletonClass(type):
|
||||
'''
|
||||
from https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
|
||||
''' # noqa
|
||||
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
supercls = super(SingletonClass, cls)
|
||||
cls._instances[cls] = supercls.__call__(*args, *kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
# def __getattr__(cls, name):
|
||||
# return cls._instances[cls].__getattr__(name)
|
||||
|
||||
|
||||
class RegisterClass(type):
|
||||
'''
|
||||
# from http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Metaprogramming.html#example-self-registration-of-subclasses
|
||||
''' # noqa
|
||||
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(RegisterClass, cls).__init__(name, bases, nmspc)
|
||||
if not hasattr(cls, 'registry'):
|
||||
cls.registry = set()
|
||||
cls.registry.add(cls)
|
||||
cls.registry -= set(bases) # Remove base classes
|
||||
|
||||
# Metamethods, called on class objects:
|
||||
def __iter__(cls):
|
||||
return iter(cls.registry)
|
||||
|
||||
def __str__(cls):
|
||||
if cls in cls.registry:
|
||||
return cls.__name__
|
||||
return cls.__name__ + ": " + ", ".join([sc.__name__ for sc in cls])
|
||||
|
||||
def __len__(cls):
|
||||
return len(cls.registry)
|
||||
|
||||
|
||||
class SingletonRegisterClass(SingletonClass, RegisterClass):
|
||||
pass
|
||||
@@ -0,0 +1,155 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
|
||||
#####################################################################################
|
||||
# below are helper classes for converting input to character stream,
|
||||
# and for converting character stream to token stream
|
||||
|
||||
class Parse_CharStream:
|
||||
def __init__(self, charstream):
|
||||
self.i_char = 0
|
||||
self.i_line = 0
|
||||
self.charstream = charstream
|
||||
|
||||
def numberoflines(self):
|
||||
return self.charstream.count('\n')
|
||||
|
||||
def endofstream(self):
|
||||
return self.i_char >= len(self.charstream)
|
||||
|
||||
def peek(self, l=1):
|
||||
if self.endofstream(): return ''
|
||||
return self.charstream[self.i_char:self.i_char+l]
|
||||
|
||||
def peek_restofline(self):
|
||||
if self.endofstream(): return ''
|
||||
i = self.charstream.find('\n', self.i_char)
|
||||
if i == -1: return self.charstream[self.i_char:]
|
||||
return self.charstream[self.i_char:i]
|
||||
|
||||
def peek_remaining(self):
|
||||
if self.endofstream(): return ''
|
||||
return self.charstream[self.i_char:]
|
||||
|
||||
def consume(self, m=None, l=None):
|
||||
if l is None: l = 1 if m is None else len(m)
|
||||
o = self.peek(l=l)
|
||||
if m is not None: assert o == m
|
||||
self.i_char += l
|
||||
self.i_line += o.count('\n')
|
||||
return o
|
||||
|
||||
def consume_while_in(self, s):
|
||||
w = ''
|
||||
while self.peek() in s: w += self.consume()
|
||||
return w
|
||||
|
||||
|
||||
class Parse_Lexer:
|
||||
'''
|
||||
Converts character stream input into a stream of tokens
|
||||
'''
|
||||
def __init__(self, charstream:Parse_CharStream, token_rules):
|
||||
token_rules = [(tname, conv, list(map(re.compile, retokens))) for (tname,conv,retokens) in token_rules]
|
||||
|
||||
self.tokens = []
|
||||
self.i = 0
|
||||
self.max_lines = charstream.numberoflines()
|
||||
|
||||
while not charstream.endofstream():
|
||||
rest = charstream.peek_remaining()
|
||||
i_line = charstream.i_line+1
|
||||
|
||||
# match against all possible tokens
|
||||
matches = [(tname, conv, retoken.match(rest)) for (tname,conv,retokens) in token_rules for retoken in retokens]
|
||||
# filter out non-matches
|
||||
matches = list(filter(lambda nm: nm[2] is not None, matches))
|
||||
assert matches, f'Parse_Lexer: Syntax error on line {i_line}: "{charstream.peek_restofline()}"'
|
||||
# find longest match
|
||||
longest = max(len(m.group(0)) for (tname,conv,m) in matches)
|
||||
# filter out non-longest matches
|
||||
matches = list(filter(lambda nm: len(nm[2].group(0))==longest, matches))
|
||||
# consume characters from stream
|
||||
charstream.consume(l=longest)
|
||||
|
||||
# convert token to python/blender types
|
||||
matches = {k:(c,v) for (k,c,v) in matches}
|
||||
for k,(conv,v) in list(matches.items()):
|
||||
v = conv(v)
|
||||
if v is None: del matches[k]
|
||||
else: matches[k] = v
|
||||
if not matches: continue
|
||||
|
||||
ks = set(matches.keys())
|
||||
v = list(matches.values())[0]
|
||||
self.tokens.append((ks, v, i_line))
|
||||
|
||||
def current_line(self):
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
return ti_line
|
||||
|
||||
def match_t_v(self, t):
|
||||
assert self.i < len(self.tokens), 'hit end on token stream'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
t = {t} if type(t) is str else set(t)
|
||||
assert tts & t, 'expected type(s) "%s" but saw "%s" (text: "%s", line: %d)' % ('","'.join(t), '","'.join(tts), tv, ti_line)
|
||||
self.i += 1
|
||||
return tv
|
||||
|
||||
def match_v_v(self, v):
|
||||
assert self.i < len(self.tokens), 'hit end on token stream'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
v = {v} if type(v) is str else set(v)
|
||||
assert tv in v, 'expected value(s) "%s" but saw "%s" (type: "%s", line: %d)' % ('","'.join(v), tv, '","'.join(tts), ti_line)
|
||||
self.i += 1
|
||||
return tv
|
||||
|
||||
def next_t(self):
|
||||
assert self.i < len(self.tokens), 'hit end of token stream'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
self.i += 1
|
||||
return tts
|
||||
|
||||
def next_v(self):
|
||||
assert self.i < len(self.tokens), 'hit end of token stream'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
self.i += 1
|
||||
return tv
|
||||
|
||||
def peek(self):
|
||||
if self.i == len(self.tokens): return ('eof','eof',self.max_lines)
|
||||
return self.tokens[self.i]
|
||||
|
||||
def peek_t(self):
|
||||
if self.i == len(self.tokens): return 'eof'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
return tts
|
||||
|
||||
def peek_v(self):
|
||||
if self.i == len(self.tokens): return 'eof'
|
||||
tts,tv,ti_line = self.tokens[self.i]
|
||||
return tv
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import time
|
||||
import inspect
|
||||
import contextlib
|
||||
|
||||
from .blender import get_path_from_addon_root
|
||||
from .globals import Globals
|
||||
|
||||
def clamp(v, m, M):
|
||||
return max(m, min(M, v))
|
||||
|
||||
class ProfilerHelper:
|
||||
def __init__(self, pr, text):
|
||||
full_text = (pr.stack[-1].full_text+'^' if pr.stack else '') + text
|
||||
parent_text = (pr.stack[-1].full_text) if pr.stack else None
|
||||
if full_text in pr.d_start:
|
||||
Profiler._broken = True
|
||||
assert False, '"%s" found in profiler already?' % text
|
||||
self.pr = pr
|
||||
self.text = text
|
||||
self.full_text = full_text
|
||||
self.parent_text = parent_text
|
||||
self.all_call = '~~ All Calls ~~^%s' % text
|
||||
self.parent_all_call = pr.stack[-1].all_call if pr.stack else None
|
||||
self.direct_call = '~~ Direct Calls ~~^%s --> %s' % (pr.stack[-1].text if pr.stack else 'None', text)
|
||||
self.parent_direct_call = pr.stack[-1].direct_call if pr.stack else None
|
||||
self._is_done = False
|
||||
self.pr.d_start[self.full_text] = time.time()
|
||||
self.pr.stack.append(self)
|
||||
|
||||
def __del__(self):
|
||||
if Profiler._broken:
|
||||
return
|
||||
if self._is_done:
|
||||
return
|
||||
Profiler._broken = True
|
||||
print('Deleting Profiler (%s) before finished' % self.full_text)
|
||||
#assert False, 'Deleting Profiler before finished'
|
||||
|
||||
def update(self, key, delta, key_parent=None):
|
||||
self.pr.d_count[key] = self.pr.d_count.get(key, 0) + 1
|
||||
self.pr.d_times[key] = self.pr.d_times.get(key, 0) + delta
|
||||
if self.pr._keep_all_times:
|
||||
if key not in self.pr.d_times_all:
|
||||
self.pr.d_times_all[key] = []
|
||||
self.pr.d_times_all[key].append(delta)
|
||||
if key_parent:
|
||||
self.pr.d_times_sub[key_parent] = self.pr.d_times_sub.get(key_parent, 0) + delta
|
||||
self.pr.d_mins[key] = min(
|
||||
self.pr.d_mins.get(key, float('inf')), delta)
|
||||
self.pr.d_maxs[key] = max(
|
||||
self.pr.d_maxs.get(key, float('-inf')), delta)
|
||||
self.pr.d_last[key] = delta
|
||||
|
||||
def done(self):
|
||||
while self.pr.stack and self.pr.stack[-1] != self:
|
||||
self.pr.stack.pop()
|
||||
if not self.pr.stack:
|
||||
if self.full_text in self.pr.d_start:
|
||||
del self.pr.d_start[self.full_text]
|
||||
return
|
||||
#assert self.pr.stack[-1] == self
|
||||
assert not self._is_done
|
||||
self.pr.stack.pop()
|
||||
self._is_done = True
|
||||
st = self.pr.d_start[self.full_text]
|
||||
en = time.time()
|
||||
delta = en-st
|
||||
self.update(self.full_text, delta, key_parent=self.parent_text)
|
||||
self.update('~~ All Calls ~~', delta)
|
||||
self.update(self.all_call, delta, key_parent=self.parent_all_call)
|
||||
self.update('~~ Direct Calls ~~', delta)
|
||||
self.update(self.direct_call, delta, key_parent=self.parent_direct_call)
|
||||
del self.pr.d_start[self.full_text]
|
||||
self.pr.clear_handler()
|
||||
|
||||
class ProfilerHelper_Ignore:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
def done(self): pass
|
||||
profilerhelper_ignore = ProfilerHelper_Ignore()
|
||||
|
||||
|
||||
|
||||
class Profiler:
|
||||
_enabled = False
|
||||
_keep_all_times = False
|
||||
_filename = 'Profiler'
|
||||
_broken = False
|
||||
_clear = False
|
||||
|
||||
@staticmethod
|
||||
def set_profiler_enabled(v):
|
||||
Profiler._enabled = v
|
||||
|
||||
@staticmethod
|
||||
def get_profiler_enabled():
|
||||
return Profiler._enabled
|
||||
|
||||
@staticmethod
|
||||
def set_profiler_filename(path):
|
||||
Profiler._filename = path
|
||||
|
||||
@staticmethod
|
||||
def get_profiler_filename():
|
||||
return Profiler._filename
|
||||
|
||||
def __init__(self):
|
||||
self.clear_handler(force=True)
|
||||
|
||||
def reset(self):
|
||||
self._broken = False
|
||||
self.clear()
|
||||
|
||||
@staticmethod
|
||||
def is_broken():
|
||||
return Profiler._broken
|
||||
|
||||
def clear_handler(self, force=False):
|
||||
if not force:
|
||||
if not self._clear: return
|
||||
if self.stack: return
|
||||
self.d_start = {}
|
||||
self.d_times = {}
|
||||
self.d_times_sub = {}
|
||||
self.d_times_all = {}
|
||||
self.d_mins = {}
|
||||
self.d_maxs = {}
|
||||
self.d_last = {}
|
||||
self.d_count = {}
|
||||
self.stack = []
|
||||
self.last_profile_out = 0
|
||||
self.clear_time = time.time()
|
||||
self._clear = False
|
||||
|
||||
def clear(self):
|
||||
self._clear = True
|
||||
self.clear_handler()
|
||||
|
||||
def _start(self, text=None, addFile=True, enabled=True, n_backs=1):
|
||||
# assert not Profiler._broken
|
||||
if Profiler._broken:
|
||||
print('Profiler broken. Ignoring')
|
||||
return profilerhelper_ignore
|
||||
if not Profiler._enabled:
|
||||
return profilerhelper_ignore
|
||||
if not enabled:
|
||||
return profilerhelper_ignore
|
||||
|
||||
frame = inspect.currentframe()
|
||||
for _ in range(n_backs): frame = frame.f_back
|
||||
filename = os.path.basename(frame.f_code.co_filename)
|
||||
linenum = frame.f_lineno
|
||||
fnname = frame.f_code.co_name
|
||||
if addFile:
|
||||
text = text or fnname
|
||||
space = ' '*(30-len(text))
|
||||
text = '%s%s (%s:%d)' % (text, space, filename, linenum)
|
||||
else:
|
||||
text = text or fnname
|
||||
return ProfilerHelper(self, text)
|
||||
|
||||
def __del__(self):
|
||||
# self.printout()
|
||||
pass
|
||||
|
||||
def add_note(self, *args, **kwargs):
|
||||
self._start(*args, n_backs=2, **kwargs).done()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def code(self, *args, enabled=True, **kwargs):
|
||||
if not Profiler._enabled or not enabled:
|
||||
yield None
|
||||
return
|
||||
try:
|
||||
pr = self._start(*args, n_backs=3, **kwargs) # n_backs=3 for contextlib wrapper
|
||||
yield pr
|
||||
pr.done()
|
||||
except Exception as e:
|
||||
pr.done()
|
||||
print('Caught exception while profiling:', args, kwargs)
|
||||
Globals.debugger.print_exception()
|
||||
raise e
|
||||
|
||||
# def function_params(self, *args):
|
||||
# if not Profiler._enabled:
|
||||
# def nowrapper(fn):
|
||||
# return fn
|
||||
# return nowrapper
|
||||
|
||||
|
||||
def function(self, fn):
|
||||
if not Profiler._enabled:
|
||||
return fn
|
||||
|
||||
frame = inspect.currentframe().f_back
|
||||
f_locals = frame.f_locals
|
||||
filename = os.path.basename(frame.f_code.co_filename)
|
||||
clsname = f_locals['__qualname__'] if '__qualname__' in f_locals else ''
|
||||
linenum = frame.f_lineno
|
||||
fnname = fn.__name__ # frame.f_code.co_name
|
||||
if clsname:
|
||||
fnname = clsname + '.' + fnname
|
||||
space = ' '*(30-len(fnname))
|
||||
text = '%s%s (%s:%d)' % (fnname, space, filename, linenum)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
# assert not Profiler._broken
|
||||
if Profiler._broken:
|
||||
return fn(*args, **kwargs)
|
||||
if not Profiler._enabled:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
pr = self._start(text=text, addFile=False)
|
||||
ret = None
|
||||
try:
|
||||
ret = fn(*args, **kwargs)
|
||||
pr.done()
|
||||
return ret
|
||||
except Exception as e:
|
||||
pr.done()
|
||||
print('CAUGHT EXCEPTION ' + str(e))
|
||||
print(text)
|
||||
Globals.debugger.print_exception()
|
||||
raise e
|
||||
wrapper.__name__ = fn.__name__
|
||||
wrapper.__doc__ = fn.__doc__
|
||||
return wrapper
|
||||
|
||||
def strout(self):
|
||||
all_width = 50
|
||||
all_chars = '.:-=+#%%@'
|
||||
# all_chars = ' .:;+=xX$'
|
||||
if not Profiler._enabled:
|
||||
return ''
|
||||
s = [
|
||||
'Profiler:',
|
||||
' run: %6.2fsecs' % (time.time() - self.clear_time),
|
||||
'----------------------------------------------------------------------------------------------',
|
||||
' total call ------- seconds / call ------- delta ',
|
||||
' secs / count = last, min, avg, max ( fps) - time - call stack ',
|
||||
'----------------------------------------------------------------------------------------------',
|
||||
]
|
||||
for text in sorted(self.d_times):
|
||||
tottime = self.d_times[text]
|
||||
totcount = self.d_count[text]
|
||||
deltime = self.d_times[text] - self.d_times_sub.get(text, 0)
|
||||
avgt = tottime / totcount
|
||||
mint = self.d_mins[text]
|
||||
maxt = self.d_maxs[text]
|
||||
last = self.d_last[text]
|
||||
calls = text.split('^')
|
||||
t = text if len(calls) == 1 else (
|
||||
' | '*(len(calls)-2) + ' \\- ' + calls[-1])
|
||||
fps = totcount / tottime if tottime > 0 else 1000
|
||||
fps = ' 1k+ ' if fps >= 1000 else '%5.1f' % fps
|
||||
s += [' %8.4f / %7d = %6.4f, %6.4f, %6.4f, %6.4f, (%s) - %6.2f - %s' % (
|
||||
tottime, totcount, last, mint, avgt, maxt, fps, deltime, t)]
|
||||
if self._keep_all_times and maxt > mint:
|
||||
histo = [0 for _ in range(all_width)]
|
||||
l = len(all_chars)
|
||||
for t in self.d_times_all[text]:
|
||||
i = int(clamp((t - mint) / (maxt - mint) * all_width, 0, all_width-1))
|
||||
histo[i] += 1
|
||||
m = max(histo)
|
||||
if m:
|
||||
histo = [' ' if v==0 else all_chars[int(clamp(v/m*l, 0, l-1))] for v in histo]
|
||||
s += [' [%s]' % ''.join(histo)]
|
||||
s += ['run: %6.2fsecs' % (time.time() - self.clear_time)]
|
||||
return '\n'.join(s)
|
||||
|
||||
def printout(self):
|
||||
if not Profiler._enabled:
|
||||
return
|
||||
print('%s\n\n\n' % self.strout())
|
||||
|
||||
def printfile(self, interval=0.25):
|
||||
# $ # to watch the file from terminal (bash) use:
|
||||
# $ watch --interval 0.1 cat filename
|
||||
|
||||
if not Profiler._enabled:
|
||||
return
|
||||
|
||||
if time.time() < self.last_profile_out + interval:
|
||||
return
|
||||
self.last_profile_out = time.time()
|
||||
|
||||
# .. back to retopoflow root
|
||||
filename = get_path_from_addon_root(Profiler._filename)
|
||||
open(filename, 'wt').write(self.strout())
|
||||
|
||||
profiler = Profiler()
|
||||
Globals.set(profiler)
|
||||
|
||||
# class CodeProfiler:
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# self.args = args
|
||||
# self.kwargs = kwargs
|
||||
# def __enter__(self):
|
||||
# self.pr = profiler._start(*self.args, n_backs=2, **self.kwargs)
|
||||
# def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# self.pr.done()
|
||||
# profiler.code = CodeProfiler
|
||||
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def time_it(label=None, *, prefix='', infix=' ', enabled=True):
|
||||
if not enabled:
|
||||
yield None
|
||||
return
|
||||
|
||||
start = time.time()
|
||||
|
||||
if label is None:
|
||||
frame = inspect.currentframe().f_back.f_back
|
||||
filename = os.path.basename(frame.f_code.co_filename)
|
||||
linenum = frame.f_lineno
|
||||
fnname = frame.f_code.co_name
|
||||
label = f'{filename}.{fnname}:{linenum}'
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
delta = time.time() - start
|
||||
print(f'{prefix}{delta:0.4f} {label}{infix}')
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 uMVPMatrix;
|
||||
float uInOut;
|
||||
}
|
||||
|
||||
uniform Options options;
|
||||
|
||||
in vec4 vPos;
|
||||
in vec4 vFrom;
|
||||
in vec4 vInColor;
|
||||
in vec4 vOutColor;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
#version 330
|
||||
|
||||
out float aRot;
|
||||
out vec4 aInColor;
|
||||
out vec4 aOutColor;
|
||||
|
||||
float angle(vec2 d) { return atan(d.y, d.x); }
|
||||
|
||||
void main() {
|
||||
vec4 p0 = options.uMVPMatrix * vFrom;
|
||||
vec4 p1 = options.uMVPMatrix * vPos;
|
||||
gl_Position = p1;
|
||||
aRot = angle((p1.xy / p1.w) - (p0.xy / p0.w));
|
||||
aInColor = vInColor;
|
||||
aOutColor = vOutColor;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
#version 330
|
||||
|
||||
in float aRot;
|
||||
in vec4 aInColor;
|
||||
in vec4 aOutColor;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
float alpha(vec2 dir) {
|
||||
vec2 d0 = dir - vec2(1,1);
|
||||
vec2 d1 = dir - vec2(1,-1);
|
||||
|
||||
float d0v = -d0.x/2.0 - d0.y;
|
||||
float d1v = -d1.x/2.0 + d1.y;
|
||||
float dv0 = length(dir);
|
||||
float dv1 = distance(dir, vec2(-2,0));
|
||||
|
||||
if(d0v < 1.0 || d1v < 1.0) return -1.0;
|
||||
// if(dv0 > 1.0) return -1.0;
|
||||
if(dv1 < 1.3) return -1.0;
|
||||
|
||||
if(d0v - 1.0 < (1.0 - options.uInOut) || d1v - 1.0 < (1.0 - options.uInOut)) return 0.0;
|
||||
//if(dv0 > options.uInOut) return 0.0;
|
||||
if(dv1 - 1.3 < (1.0 - options.uInOut)) return 0.0;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 d = 2.0 * (gl_PointCoord - vec2(0.5, 0.5));
|
||||
vec2 dr = vec2(cos(aRot)*d.x - sin(aRot)*d.y, sin(aRot)*d.x + cos(aRot)*d.y);
|
||||
float a = alpha(dr);
|
||||
if(a < 0.0) { discard; return; }
|
||||
outColor = mix(aOutColor, aInColor, a);
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "bmesh_render_prefix.glsl"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec4 vert_pos0; // position wrt model
|
||||
in vec4 vert_pos1; // position wrt model
|
||||
in vec2 vert_offset;
|
||||
in vec4 vert_norm; // normal wrt model
|
||||
in float selected; // is edge selected? 0=no; 1=yes
|
||||
in float warning; // is edge warning? 0=no; 1=yes
|
||||
in float pinned; // is edge pinned? 0=no; 1=yes
|
||||
in float seam; // is edge on seam? 0=no; 1=yes
|
||||
|
||||
out vec4 vPPosition; // final position (projected)
|
||||
out vec4 vCPosition; // position wrt camera
|
||||
out vec4 vWPosition; // position wrt world
|
||||
out vec4 vMPosition; // position wrt model
|
||||
out vec4 vTPosition; // position wrt target
|
||||
out vec4 vWTPosition_x; // position wrt target world
|
||||
out vec4 vWTPosition_y; // position wrt target world
|
||||
out vec4 vWTPosition_z; // position wrt target world
|
||||
out vec4 vCTPosition_x; // position wrt target camera
|
||||
out vec4 vCTPosition_y; // position wrt target camera
|
||||
out vec4 vCTPosition_z; // position wrt target camera
|
||||
out vec4 vPTPosition_x; // position wrt target projected
|
||||
out vec4 vPTPosition_y; // position wrt target projected
|
||||
out vec4 vPTPosition_z; // position wrt target projected
|
||||
out vec3 vCNormal; // normal wrt camera
|
||||
out vec3 vWNormal; // normal wrt world
|
||||
out vec3 vMNormal; // normal wrt model
|
||||
out vec3 vTNormal; // normal wrt target
|
||||
out vec4 vColorIn; // color of geometry inside
|
||||
out vec4 vColorOut; // color of geometry outside (considers selection)
|
||||
out vec2 vPCPosition;
|
||||
|
||||
bool is_warning() { return use_warning() && warning > 0.5; }
|
||||
bool is_pinned() { return use_pinned() && pinned > 0.5; }
|
||||
bool is_seam() { return use_seam() && seam > 0.5; }
|
||||
bool is_selection() { return use_selection() && selected > 0.5; }
|
||||
|
||||
void main() {
|
||||
vec4 pos0 = get_pos(vec3(vert_pos0));
|
||||
vec4 pos1 = get_pos(vec3(vert_pos1));
|
||||
vec2 ppos0 = xyz4(options.matrix_p * options.matrix_v * options.matrix_m * pos0).xy;
|
||||
vec2 ppos1 = xyz4(options.matrix_p * options.matrix_v * options.matrix_m * pos1).xy;
|
||||
vec2 pdir0 = normalize(ppos1 - ppos0);
|
||||
vec2 pdir1 = vec2(-pdir0.y, pdir0.x);
|
||||
vec4 off = vec4((options.radius.x + options.radius.y + 2.0) * pdir1 * 2.0 * (vert_offset.y-0.5) / options.screen_size.xy, 0, 0);
|
||||
|
||||
vec4 pos = pos0 + vert_offset.x * (pos1 - pos0);
|
||||
vec3 norm = normalize(vec3(vert_norm) * vec3(options.vert_scale));
|
||||
|
||||
vec4 wpos = push_pos(options.matrix_m * pos);
|
||||
vec3 wnorm = normalize(mat3(options.matrix_mn) * norm);
|
||||
|
||||
vec4 tpos = options.matrix_ti * wpos;
|
||||
vec3 tnorm = vec3(
|
||||
dot(wnorm, vec3(options.mirror_x)),
|
||||
dot(wnorm, vec3(options.mirror_y)),
|
||||
dot(wnorm, vec3(options.mirror_z)));
|
||||
|
||||
vMPosition = pos;
|
||||
vWPosition = wpos;
|
||||
vCPosition = options.matrix_v * wpos;
|
||||
vPPosition = off + xyz4(options.matrix_p * options.matrix_v * wpos);
|
||||
vPCPosition = xyz4(options.matrix_p * options.matrix_v * wpos).xy;
|
||||
|
||||
vMNormal = norm;
|
||||
vWNormal = wnorm;
|
||||
vCNormal = normalize(mat3(options.matrix_vn) * wnorm);
|
||||
|
||||
vTPosition = tpos;
|
||||
vWTPosition_x = options.matrix_t * vec4(0.0, tpos.y, tpos.z, 1.0);
|
||||
vWTPosition_y = options.matrix_t * vec4(tpos.x, 0.0, tpos.z, 1.0);
|
||||
vWTPosition_z = options.matrix_t * vec4(tpos.x, tpos.y, 0.0, 1.0);
|
||||
vCTPosition_x = options.matrix_v * vWTPosition_x;
|
||||
vCTPosition_y = options.matrix_v * vWTPosition_y;
|
||||
vCTPosition_z = options.matrix_v * vWTPosition_z;
|
||||
vPTPosition_x = options.matrix_p * vCTPosition_x;
|
||||
vPTPosition_y = options.matrix_p * vCTPosition_y;
|
||||
vPTPosition_z = options.matrix_p * vCTPosition_z;
|
||||
vTNormal = tnorm;
|
||||
|
||||
gl_Position = vPPosition;
|
||||
|
||||
vColorIn = options.color_normal;
|
||||
vColorOut = vec4(options.color_normal.rgb, 0.0);
|
||||
|
||||
if(is_selection()) {
|
||||
vColorIn = color_over(options.color_selected, vColorIn);
|
||||
vColorOut = vec4(options.color_selected.rgb, 0.0);
|
||||
}
|
||||
if(is_warning()) vColorOut = color_over(options.color_warning, vColorOut);
|
||||
if(is_pinned()) vColorOut = color_over(options.color_pinned, vColorOut);
|
||||
if(is_seam()) vColorOut = color_over(options.color_seam, vColorOut);
|
||||
|
||||
vColorIn.a *= 1.0 - options.hidden.x;
|
||||
vColorOut.a *= 1.0 - options.hidden.x;
|
||||
|
||||
if(debug_invert_backfacing && vCNormal.z < 0.0) {
|
||||
vColorIn = vec4(vec3(1,1,1) - vColorIn.rgb, vColorIn.a);
|
||||
vColorOut = vec4(vec3(1,1,1) - vColorOut.rgb, vColorOut.a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec4 vPPosition; // final position (projected)
|
||||
in vec4 vCPosition; // position wrt camera
|
||||
in vec4 vWPosition; // position wrt world
|
||||
in vec4 vMPosition; // position wrt model
|
||||
in vec4 vTPosition; // position wrt target
|
||||
in vec4 vWTPosition_x; // position wrt target world
|
||||
in vec4 vWTPosition_y; // position wrt target world
|
||||
in vec4 vWTPosition_z; // position wrt target world
|
||||
in vec4 vCTPosition_x; // position wrt target camera
|
||||
in vec4 vCTPosition_y; // position wrt target camera
|
||||
in vec4 vCTPosition_z; // position wrt target camera
|
||||
in vec4 vPTPosition_x; // position wrt target projected
|
||||
in vec4 vPTPosition_y; // position wrt target projected
|
||||
in vec4 vPTPosition_z; // position wrt target projected
|
||||
in vec3 vCNormal; // normal wrt camera
|
||||
in vec3 vWNormal; // normal wrt world
|
||||
in vec3 vMNormal; // normal wrt model
|
||||
in vec3 vTNormal; // normal wrt target
|
||||
in vec4 vColorIn; // color of geometry inside (considers selection)
|
||||
in vec4 vColorOut; // color of geometry outside
|
||||
in vec2 vPCPosition;
|
||||
|
||||
out vec4 outColor;
|
||||
out float gl_FragDepth;
|
||||
|
||||
void main() {
|
||||
float clip = options.clip[1] - options.clip[0];
|
||||
float focus = (view_distance() - options.clip[0]) / clip + 0.04;
|
||||
|
||||
float dist_from_center = length(options.screen_size.xy * (vPCPosition - vPPosition.xy));
|
||||
float alpha_mult = 1.0 - (dist_from_center - (options.radius.x + options.radius.y));
|
||||
if(alpha_mult <= 0) {
|
||||
discard;
|
||||
return;
|
||||
}
|
||||
|
||||
float mix_in_out = clamp(dist_from_center - options.radius.x, 0.0, 1.0);
|
||||
vec4 vColor = mix(vColorIn, vColorOut, mix_in_out);
|
||||
vec3 rgb = vColor.rgb;
|
||||
float alpha = vColor.a * min(1.0, alpha_mult);
|
||||
|
||||
if(is_view_perspective()) {
|
||||
// perspective projection
|
||||
vec3 v = xyz3(vCPosition);
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = -dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// orthographic projection
|
||||
vec3 v = vec3(0, 0, clip * 0.5); // + vCPosition.xyz / vCPosition.w;
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25));
|
||||
outColor = coloring(vec4(rgb, alpha));
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define BMESH_FACE
|
||||
|
||||
#include "bmesh_render_prefix.glsl"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec4 vert_pos; // position wrt model
|
||||
in vec4 vert_norm; // normal wrt model
|
||||
in float selected; // is face selected? 0=no; 1=yes
|
||||
in float pinned; // is face pinned? 0=no; 1=yes
|
||||
|
||||
out vec4 vPPosition; // final position (projected)
|
||||
out vec4 vCPosition; // position wrt camera
|
||||
out vec4 vTPosition; // position wrt target
|
||||
out vec4 vCTPosition_x; // position wrt target camera
|
||||
out vec4 vCTPosition_y; // position wrt target camera
|
||||
out vec4 vCTPosition_z; // position wrt target camera
|
||||
out vec4 vPTPosition_x; // position wrt target projected
|
||||
out vec4 vPTPosition_y; // position wrt target projected
|
||||
out vec4 vPTPosition_z; // position wrt target projected
|
||||
out vec3 vCNormal; // normal wrt camera
|
||||
out vec4 vColor; // color of geometry (considers selection)
|
||||
|
||||
void main() {
|
||||
//vec4 off = vec4(radius * (vert_dir0 * vert_offset.x + vert_dir1 * vert_offset.y) / screen_size, 0, 0);
|
||||
|
||||
vec4 pos = get_pos(vec3(vert_pos));
|
||||
vec3 norm = normalize(vec3(vert_norm) * vec3(options.vert_scale));
|
||||
|
||||
vec4 wpos = push_pos(options.matrix_m * pos);
|
||||
vec3 wnorm = normalize(mat3(options.matrix_mn) * norm);
|
||||
|
||||
vec4 tpos = options.matrix_ti * wpos;
|
||||
vec3 tnorm = vec3(
|
||||
dot(wnorm, vec3(options.mirror_x)),
|
||||
dot(wnorm, vec3(options.mirror_y)),
|
||||
dot(wnorm, vec3(options.mirror_z)));
|
||||
|
||||
vCPosition = options.matrix_v * wpos;
|
||||
vPPosition = xyz4(options.matrix_p * options.matrix_v * wpos);
|
||||
|
||||
vCNormal = normalize(mat3(options.matrix_vn) * wnorm);
|
||||
|
||||
vTPosition = tpos;
|
||||
vCTPosition_x = options.matrix_v * options.matrix_t * vec4(0.0, tpos.y, tpos.z, 1.0);
|
||||
vCTPosition_y = options.matrix_v * options.matrix_t * vec4(tpos.x, 0.0, tpos.z, 1.0);
|
||||
vCTPosition_z = options.matrix_v * options.matrix_t * vec4(tpos.x, tpos.y, 0.0, 1.0);
|
||||
vPTPosition_x = options.matrix_p * vCTPosition_x;
|
||||
vPTPosition_y = options.matrix_p * vCTPosition_y;
|
||||
vPTPosition_z = options.matrix_p * vCTPosition_z;
|
||||
|
||||
gl_Position = vPPosition;
|
||||
|
||||
vColor = options.color_normal;
|
||||
|
||||
if(use_selection() && selected > 0.5) vColor = mix(vColor, options.color_selected, 0.75);
|
||||
if(use_pinned() && pinned > 0.5) vColor = mix(vColor, options.color_pinned, 0.75);
|
||||
|
||||
vColor.a *= 1.0 - options.hidden.x;
|
||||
|
||||
if(debug_invert_backfacing && vCNormal.z < 0.0) {
|
||||
vColor = vec4(vec3(1,1,1) - vColor.rgb, vColor.a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec4 vPPosition; // final position (projected)
|
||||
in vec4 vCPosition; // position wrt camera
|
||||
in vec4 vTPosition; // position wrt target
|
||||
in vec4 vCTPosition_x; // position wrt target camera
|
||||
in vec4 vCTPosition_y; // position wrt target camera
|
||||
in vec4 vCTPosition_z; // position wrt target camera
|
||||
in vec4 vPTPosition_x; // position wrt target projected
|
||||
in vec4 vPTPosition_y; // position wrt target projected
|
||||
in vec4 vPTPosition_z; // position wrt target projected
|
||||
in vec3 vCNormal; // normal wrt camera
|
||||
in vec4 vColor; // color of geometry (considers selection)
|
||||
|
||||
out vec4 outColor;
|
||||
out float gl_FragDepth;
|
||||
|
||||
void main() {
|
||||
float clip = options.clip[1] - options.clip[0];
|
||||
float focus = (view_distance() - options.clip[0]) / clip + 0.04;
|
||||
vec3 rgb = vColor.rgb;
|
||||
float alpha = vColor.a;
|
||||
|
||||
if(vCNormal.z < 0) { discard; return; }
|
||||
|
||||
if(is_view_perspective()) {
|
||||
// perspective projection
|
||||
vec3 v = xyz3(vCPosition);
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = -dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// orthographic projection
|
||||
vec3 v = vec3(0, 0, clip * 0.5); // + vCPosition.xyz / vCPosition.w;
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25));
|
||||
outColor = coloring(vec4(rgb, alpha));
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// common shader
|
||||
|
||||
struct Options {
|
||||
mat4 matrix_m; // model xform matrix
|
||||
mat4 matrix_mn; // model xform matrix for normal (inv transpose of matrix_m)
|
||||
mat4 matrix_t; // target xform matrix
|
||||
mat4 matrix_ti; // target xform matrix inverse
|
||||
mat4 matrix_v; // view xform matrix
|
||||
mat4 matrix_vn; // view xform matrix for normal
|
||||
mat4 matrix_p; // projection matrix
|
||||
|
||||
vec4 clip;
|
||||
vec4 screen_size;
|
||||
vec4 view_settings0; // [ view_distance, perspective, focus_mult, alpha_backface ]
|
||||
vec4 view_settings1; // [ cull_backfaces, unit_scaling_factor, normal_offset (how far to push geo along normal), constrain_offset (should constrain by focus) ]
|
||||
vec4 view_settings2; // [ view push, xxx, xxx, xxx ]
|
||||
vec4 view_position;
|
||||
|
||||
vec4 color_normal; // color of geometry if not selected
|
||||
vec4 color_selected; // color of geometry if selected
|
||||
vec4 color_warning; // color of geometry if warning
|
||||
vec4 color_pinned; // color of geometry if pinned
|
||||
vec4 color_seam; // color of geometry if seam
|
||||
|
||||
vec4 use_settings0; // [ selection, warning, pinned, seam ]
|
||||
vec4 use_settings1; // [ rounding, xxx, xxx, xxx ]
|
||||
|
||||
vec4 mirror_settings; // [ view (0=none; 1=edge at plane; 2=color faces on far side of plane), effect (0=no effect, 1=full), xxx, xxx ]
|
||||
vec4 mirroring; // mirror along axis: 0=false, 1=true
|
||||
vec4 mirror_o; // mirroring origin wrt world
|
||||
vec4 mirror_x; // mirroring x-axis wrt world
|
||||
vec4 mirror_y; // mirroring y-axis wrt world
|
||||
vec4 mirror_z; // mirroring z-axis wrt world
|
||||
|
||||
vec4 vert_scale; // used for mirroring
|
||||
|
||||
vec4 hidden; // affects alpha for geometry below surface. 0=opaque, 1=transparent
|
||||
vec4 offset;
|
||||
vec4 dotoffset;
|
||||
|
||||
vec4 radius;
|
||||
};
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
const bool debug_invert_backfacing = false;
|
||||
|
||||
int mirror_view() {
|
||||
float v = options.mirror_settings[0];
|
||||
if(v > 1.5) return 2;
|
||||
if(v > 0.5) return 1;
|
||||
return 0;
|
||||
}
|
||||
float mirror_effect() { return options.mirror_settings[1]; }
|
||||
|
||||
float view_distance() { return options.view_settings0[0]; }
|
||||
bool is_view_perspective() { return options.view_settings0[1] > 0.5; }
|
||||
float focus_mult() { return options.view_settings0[2]; }
|
||||
float alpha_backface() { return options.view_settings0[3]; }
|
||||
bool cull_backfaces() { return options.view_settings1[0] > 0.5; }
|
||||
float unit_scaling_factor() { return options.view_settings1[1]; }
|
||||
float normal_offset() { return options.view_settings1[2]; }
|
||||
bool constrain_offset() { return options.view_settings1[3] > 0.5; }
|
||||
float view_push() { return options.view_settings2[0]; }
|
||||
vec4 view_position() { return options.view_position; }
|
||||
|
||||
float clip_near() { return options.clip[0]; }
|
||||
float clip_far() { return options.clip[1]; }
|
||||
|
||||
bool use_selection() { return options.use_settings0[0] > 0.5; }
|
||||
bool use_warning() { return options.use_settings0[1] > 0.5; }
|
||||
bool use_pinned() { return options.use_settings0[2] > 0.5; }
|
||||
bool use_seam() { return options.use_settings0[3] > 0.5; }
|
||||
bool use_rounding() { return options.use_settings1[0] > 0.5; }
|
||||
|
||||
float magic_offset() { return options.offset.x; }
|
||||
float magic_dotoffset() { return options.dotoffset.x; }
|
||||
|
||||
vec4 color_over(vec4 top, vec4 bottom) {
|
||||
float a = top.a + (1.0 - top.a) * bottom.a;
|
||||
vec3 c = (top.rgb * top.a + (1.0 - top.a) * bottom.a * bottom.rgb) / a;
|
||||
return vec4(c, a);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
vec4 get_pos(vec3 p) {
|
||||
float mult = 1.0;
|
||||
if(constrain_offset()) {
|
||||
mult = 1.0;
|
||||
} else {
|
||||
float clip_dist = clip_far() - clip_near();
|
||||
float focus = (view_distance() - clip_near()) / clip_dist + 0.04;
|
||||
mult = focus;
|
||||
}
|
||||
vec3 norm_offset = vec3(vert_norm) * normal_offset() * mult;
|
||||
vec3 mirror = vec3(options.vert_scale);
|
||||
return vec4((p + norm_offset) * mirror, 1.0);
|
||||
}
|
||||
|
||||
vec4 push_pos(vec4 p) {
|
||||
float clip_dist = clip_far() - clip_near();
|
||||
float focus = (1.0 - clamp((view_distance() - clip_near()) / clip_dist, 0.0, 1.0)) * 0.1;
|
||||
return vec4( mix(view_position().xyz, p.xyz, view_push()), p.w);
|
||||
}
|
||||
|
||||
vec4 xyz4(vec4 v) { return vec4(v.xyz / abs(v.w), sign(v.w)); }
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
vec3 xyz3(vec4 v) { return v.xyz / v.w; }
|
||||
|
||||
// adjusts color based on mirroring settings and fragment position
|
||||
vec4 coloring(vec4 orig) {
|
||||
vec4 mixer = vec4(0.6, 0.6, 0.6, 0.0);
|
||||
if(mirror_view() == 0) {
|
||||
// NO SYMMETRY VIEW
|
||||
} else if(mirror_view() == 1) {
|
||||
// EDGE VIEW
|
||||
float edge_width = 5.0 / options.screen_size.y;
|
||||
vec3 viewdir;
|
||||
if(is_view_perspective()) {
|
||||
viewdir = normalize(xyz3(vCPosition));
|
||||
} else {
|
||||
viewdir = vec3(0,0,1);
|
||||
}
|
||||
vec3 diffc_x = xyz3(vCTPosition_x) - xyz3(vCPosition);
|
||||
vec3 diffc_y = xyz3(vCTPosition_y) - xyz3(vCPosition);
|
||||
vec3 diffc_z = xyz3(vCTPosition_z) - xyz3(vCPosition);
|
||||
vec3 dirc_x = normalize(diffc_x);
|
||||
vec3 dirc_y = normalize(diffc_y);
|
||||
vec3 dirc_z = normalize(diffc_z);
|
||||
vec3 diffp_x = xyz3(vPTPosition_x) - xyz3(vPPosition);
|
||||
vec3 diffp_y = xyz3(vPTPosition_y) - xyz3(vPPosition);
|
||||
vec3 diffp_z = xyz3(vPTPosition_z) - xyz3(vPPosition);
|
||||
vec3 aspect = vec3(1.0, options.screen_size.y / options.screen_size.x, 0.0);
|
||||
|
||||
float s = 0.0;
|
||||
if(options.mirroring.x > 0.5 && length(diffp_x * aspect) < edge_width * (1.0 - pow(abs(dot(viewdir,dirc_x)), 10.0))) {
|
||||
mixer.r = 1.0;
|
||||
s = max(s, (vTPosition.x < 0.0) ? 1.0 : 0.1);
|
||||
}
|
||||
if(options.mirroring.y > 0.5 && length(diffp_y * aspect) < edge_width * (1.0 - pow(abs(dot(viewdir,dirc_y)), 10.0))) {
|
||||
mixer.g = 1.0;
|
||||
s = max(s, (vTPosition.y > 0.0) ? 1.0 : 0.1);
|
||||
}
|
||||
if(options.mirroring.z > 0.5 && length(diffp_z * aspect) < edge_width * (1.0 - pow(abs(dot(viewdir,dirc_z)), 10.0))) {
|
||||
mixer.b = 1.0;
|
||||
s = max(s, (vTPosition.z < 0.0) ? 1.0 : 0.1);
|
||||
}
|
||||
mixer.a = mirror_effect() * s + mixer.a * (1.0 - s);
|
||||
} else if(mirror_view() == 2) {
|
||||
// FACE VIEW
|
||||
if(options.mirroring.x > 0.5 && vTPosition.x < 0.0) {
|
||||
mixer.r = 1.0;
|
||||
mixer.a = mirror_effect();
|
||||
}
|
||||
if(options.mirroring.y > 0.5 && vTPosition.y > 0.0) {
|
||||
mixer.g = 1.0;
|
||||
mixer.a = mirror_effect();
|
||||
}
|
||||
if(options.mirroring.z > 0.5 && vTPosition.z < 0.0) {
|
||||
mixer.b = 1.0;
|
||||
mixer.a = mirror_effect();
|
||||
}
|
||||
}
|
||||
|
||||
float m0 = mixer.a, m1 = 1.0 - mixer.a;
|
||||
|
||||
#ifdef BMESH_FACE
|
||||
return vec4(mixer.rgb * m0 + orig.rgb * orig.a * m1, m0 + orig.a * m1);
|
||||
#else
|
||||
return vec4(mixer.rgb * m0 + orig.rgb * m1, m0 + orig.a * m1);
|
||||
#endif
|
||||
}
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "bmesh_render_prefix.glsl"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec4 vert_pos; // position wrt model
|
||||
in vec2 vert_offset;
|
||||
in vec4 vert_norm; // normal wrt model
|
||||
in float selected; // is vertex selected? 0=no; 1=yes
|
||||
in float warning; // is vertex warning? 0=no; 1=yes
|
||||
in float pinned; // is vertex pinned? 0=no; 1=yes
|
||||
in float seam; // is vertex along seam? 0=no; 1=yes
|
||||
|
||||
out vec4 vPPosition; // final position (projected)
|
||||
out vec4 vCPosition; // position wrt camera
|
||||
out vec4 vTPosition; // position wrt target
|
||||
out vec4 vCTPosition_x; // position wrt target camera
|
||||
out vec4 vCTPosition_y; // position wrt target camera
|
||||
out vec4 vCTPosition_z; // position wrt target camera
|
||||
out vec4 vPTPosition_x; // position wrt target projected
|
||||
out vec4 vPTPosition_y; // position wrt target projected
|
||||
out vec4 vPTPosition_z; // position wrt target projected
|
||||
out vec3 vCNormal; // normal wrt camera
|
||||
out vec4 vColor; // color of geometry (considers selection)
|
||||
out vec2 vPCPosition;
|
||||
|
||||
void main() {
|
||||
vec2 vo = vert_offset * 2 - vec2(1, 1);
|
||||
vec4 off = vec4((options.radius.x + 2) * vo / options.screen_size.xy, 0, 0);
|
||||
|
||||
vec4 pos = get_pos(vec3(vert_pos));
|
||||
vec3 norm = normalize(vec3(vert_norm) * vec3(options.vert_scale));
|
||||
|
||||
vec4 wpos = push_pos(options.matrix_m * pos);
|
||||
vec3 wnorm = normalize(mat3(options.matrix_mn) * norm);
|
||||
|
||||
vec4 tpos = options.matrix_ti * wpos;
|
||||
vec3 tnorm = vec3(
|
||||
dot(wnorm, vec3(options.mirror_x)),
|
||||
dot(wnorm, vec3(options.mirror_y)),
|
||||
dot(wnorm, vec3(options.mirror_z)));
|
||||
|
||||
vCPosition = options.matrix_v * wpos;
|
||||
vPPosition = off + xyz4(options.matrix_p * options.matrix_v * wpos);
|
||||
vPCPosition = xyz4(options.matrix_p * options.matrix_v * wpos).xy;
|
||||
|
||||
vCNormal = normalize(mat3(options.matrix_vn) * wnorm);
|
||||
|
||||
vTPosition = tpos;
|
||||
vCTPosition_x = options.matrix_v * options.matrix_t * vec4(0.0, tpos.y, tpos.z, 1.0);
|
||||
vCTPosition_y = options.matrix_v * options.matrix_t * vec4(tpos.x, 0.0, tpos.z, 1.0);
|
||||
vCTPosition_z = options.matrix_v * options.matrix_t * vec4(tpos.x, tpos.y, 0.0, 1.0);
|
||||
vPTPosition_x = options.matrix_p * vCTPosition_x;
|
||||
vPTPosition_y = options.matrix_p * vCTPosition_y;
|
||||
vPTPosition_z = options.matrix_p * vCTPosition_z;
|
||||
|
||||
gl_Position = vPPosition;
|
||||
|
||||
vColor = options.color_normal;
|
||||
|
||||
if(use_warning() && warning > 0.5) vColor = mix(vColor, options.color_warning, 0.75);
|
||||
if(use_selection() && selected > 0.5) vColor = mix(vColor, options.color_selected, 0.75);
|
||||
if(use_pinned() && pinned > 0.5) vColor = mix(vColor, options.color_pinned, 0.75);
|
||||
if(use_seam() && seam > 0.5) vColor = mix(vColor, options.color_seam, 0.75);
|
||||
|
||||
vColor.a *= 1.0 - options.hidden.x;
|
||||
|
||||
if(debug_invert_backfacing && vCNormal.z < 0.0) {
|
||||
vColor = vec4(vec3(1,1,1) - vColor.rgb, vColor.a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec4 vPPosition; // final position (projected)
|
||||
in vec4 vCPosition; // position wrt camera
|
||||
in vec4 vTPosition; // position wrt target
|
||||
in vec4 vCTPosition_x; // position wrt target camera
|
||||
in vec4 vCTPosition_y; // position wrt target camera
|
||||
in vec4 vCTPosition_z; // position wrt target camera
|
||||
in vec4 vPTPosition_x; // position wrt target projected
|
||||
in vec4 vPTPosition_y; // position wrt target projected
|
||||
in vec4 vPTPosition_z; // position wrt target projected
|
||||
in vec3 vCNormal; // normal wrt camera
|
||||
in vec4 vColor; // color of geometry (considers selection)
|
||||
in vec2 vPCPosition;
|
||||
|
||||
out vec4 outColor;
|
||||
out float gl_FragDepth;
|
||||
|
||||
void main() {
|
||||
float clip = options.clip[1] - options.clip[0];
|
||||
float focus = (view_distance() - options.clip[0]) / clip + 0.04;
|
||||
vec3 rgb = vColor.rgb;
|
||||
float alpha = vColor.a;
|
||||
|
||||
if(use_rounding()) {
|
||||
float dist_from_center = length(options.screen_size.xy * (vPCPosition - vPPosition.xy));
|
||||
float alpha_mult = 1.0 - (dist_from_center - options.radius.x);
|
||||
if(alpha_mult <= 0) {
|
||||
discard;
|
||||
return;
|
||||
}
|
||||
alpha *= min(1.0, alpha_mult);
|
||||
}
|
||||
|
||||
if(is_view_perspective()) {
|
||||
// perspective projection
|
||||
vec3 v = xyz3(vCPosition);
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = -dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// orthographic projection
|
||||
vec3 v = vec3(0, 0, clip * 0.5); // + vCPosition.xyz / vCPosition.w;
|
||||
float l = length(v);
|
||||
float l_clip = (l - options.clip[0]) / clip;
|
||||
float d = dot(vCNormal, v) / l;
|
||||
if(d <= 0.0) {
|
||||
if(cull_backfaces()) {
|
||||
alpha = 0.0;
|
||||
discard;
|
||||
return;
|
||||
} else {
|
||||
alpha *= min(1.0, alpha_backface());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alpha *= min(1.0, pow(max(vCNormal.z, 0.01), 0.25));
|
||||
outColor = coloring(vec4(rgb, alpha));
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
draws an antialiased, stippled circle
|
||||
ex: stipple [3,2] color0 '=' color1 '-'
|
||||
produces '===--===--===--===-' (just wrapped as a circle!)
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 MVPMatrix; // pixel matrix
|
||||
vec4 screensize; // width,height of screen (for antialiasing)
|
||||
vec4 center; // center of circle
|
||||
vec4 color0; // color of on stipple
|
||||
vec4 color1; // color of off stipple
|
||||
vec4 radius_width; // radius of circle, line width (perp to line)
|
||||
vec4 stipple_data; // stipple lengths, offset
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
const float TAU = 6.28318530718;
|
||||
|
||||
float radius() { return options.radius_width.x; }
|
||||
float width() { return options.radius_width.y; }
|
||||
vec2 stipple_lengths() { return options.stipple_data.xy; }
|
||||
float stipple_offset() { return options.stipple_data.z; }
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // x: [0,1], ratio of circumference. y: [0,1], inner/outer radius (width)
|
||||
|
||||
noperspective out vec2 vpos; // position scaled by screensize
|
||||
noperspective out vec2 cpos; // center of line, scaled by screensize
|
||||
noperspective out float offset; // stipple offset of individual fragment
|
||||
|
||||
|
||||
void main() {
|
||||
float circumference = TAU * radius();
|
||||
float ang = TAU * pos.x;
|
||||
float r = radius() + (pos.y - 0.5) * (width() + 2.0);
|
||||
vec2 v = vec2(cos(ang), sin(ang));
|
||||
vec2 p = options.center.xy + vec2(0.5,0.5) + r * v;
|
||||
vec2 cp = options.center.xy + vec2(0.5,0.5) + radius() * v;
|
||||
vec4 pcp = options.MVPMatrix * vec4(cp, 0.0, 1.0);
|
||||
gl_Position = options.MVPMatrix * vec4(p, 0.0, 1.0);
|
||||
vpos = vec2(gl_Position.x * options.screensize.x, gl_Position.y * options.screensize.y);
|
||||
cpos = vec2(pcp.x * options.screensize.x, pcp.y * options.screensize.y);
|
||||
offset = circumference * pos.x + stipple_offset();
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
noperspective in vec2 vpos;
|
||||
noperspective in vec2 cpos;
|
||||
noperspective in float offset;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// stipple
|
||||
if(stipple_lengths().y <= 0) { // stipple disabled
|
||||
outColor = options.color0;
|
||||
} else {
|
||||
float t = stipple_lengths().x + stipple_lengths().y;
|
||||
float s = mod(offset, t);
|
||||
float sd = s - stipple_lengths().x;
|
||||
if(s <= 0.5 || s >= t - 0.5) {
|
||||
outColor = mix(options.color1, options.color0, mod(s + 0.5, t));
|
||||
} else if(s >= stipple_lengths().x - 0.5 && s <= stipple_lengths().x + 0.5) {
|
||||
outColor = mix(options.color0, options.color1, s - (stipple_lengths().x - 0.5));
|
||||
} else if(s < stipple_lengths().x) {
|
||||
outColor = options.color0;
|
||||
} else {
|
||||
outColor = options.color1;
|
||||
}
|
||||
}
|
||||
// antialias along edge of line
|
||||
float cdist = length(cpos - vpos);
|
||||
if(cdist > width()) {
|
||||
outColor.a *= clamp(1.0 - (cdist - width()), 0.0, 1.0);
|
||||
}
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 MVPMatrix; // pixel matrix
|
||||
vec4 screensize; // [ width, height, _, _ ] of screen (for antialiasing)
|
||||
vec4 center; // center of circle
|
||||
vec4 color; // color of circle
|
||||
vec4 plane_x; // x direction in plane the circle lies in
|
||||
vec4 plane_y; // y direction in plane the circle lies in
|
||||
vec4 settings; // [ radius, line width (perp to line in plane), depth range near for drawover, depth range far ]
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
|
||||
const float TAU = 6.28318530718;
|
||||
const bool srgbTarget = true;
|
||||
|
||||
float radius() { return options.settings[0]; }
|
||||
float width() { return options.settings[1]; }
|
||||
float depth_near() { return options.settings[2]; }
|
||||
float depth_far() { return options.settings[3]; }
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // x: [0,1], ratio of circumference. y: [0,1], inner/outer radius (width)
|
||||
|
||||
noperspective out vec2 vpos; // position scaled by screensize
|
||||
noperspective out vec2 cpos; // center of line, scaled by screensize
|
||||
|
||||
void main() {
|
||||
float ang = TAU * pos.x;
|
||||
float r = radius() + (pos.y - 0.5) * width();
|
||||
vec3 v = options.plane_x.xyz * cos(ang) + options.plane_y.xyz * sin(ang);
|
||||
vec3 p = options.center.xyz + r * v;
|
||||
vec3 cp = options.center.xyz + radius() * v;
|
||||
vec4 pcp = options.MVPMatrix * vec4(cp, 1.0);
|
||||
gl_Position = options.MVPMatrix * vec4(p, 1.0);
|
||||
vpos = vec2(gl_Position.x * options.screensize.x, gl_Position.y * options.screensize.y);
|
||||
cpos = vec2(pcp.x * options.screensize.x, pcp.y * options.screensize.y);
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
noperspective in vec2 vpos;
|
||||
noperspective in vec2 cpos;
|
||||
|
||||
out vec4 outColor;
|
||||
out float gl_FragDepth;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
outColor = options.color;
|
||||
|
||||
// antialias along edge of line.... NOT WORKING!
|
||||
float cdist = length(cpos - vpos);
|
||||
if(cdist > width()) {
|
||||
outColor.a *= clamp(1.0 - (cdist - width()), 1.0, 1.0);
|
||||
}
|
||||
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
gl_FragDepth = mix(depth_near(), depth_far(), gl_FragCoord.z);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
draws an antialiased, stippled line
|
||||
ex: stipple [3,2] color0 '=' color1 '-'
|
||||
produces '===--===--===--===-'
|
||||
| |
|
||||
\_pos0 pos1_/
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 MVPMatrix; // pixel matrix
|
||||
vec4 screensize; // width,height of screen (for antialiasing)
|
||||
vec4 pos0; // front end of line
|
||||
vec4 pos1; // back end of line
|
||||
vec4 color0; // color of on stipple
|
||||
vec4 color1; // color of off stipple
|
||||
vec4 stipple_width; // lengths for stipple (x: color0, y: color1, z: initial shift) and line width (perp to line)
|
||||
};
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // which corner of line ([0,0], [0,1], [1,1], [1,0])
|
||||
|
||||
noperspective out vec2 vpos; // position scaled by screensize
|
||||
noperspective out vec2 cpos; // center of line, scaled by screensize
|
||||
noperspective out float offset; // stipple offset of individual fragment
|
||||
|
||||
void main() {
|
||||
vec2 v01 = options.pos1.xy - options.pos0.xy;
|
||||
vec2 d01 = normalize(v01);
|
||||
vec2 perp = vec2(-d01.y, d01.x);
|
||||
vec2 cp = options.pos0.xy + vec2(0.5,0.5) + (pos.x * v01);
|
||||
vec2 p = cp + ((options.stipple_width.w + 2.0) * (pos.y - 0.5) * perp);
|
||||
vec4 pcp = options.MVPMatrix * vec4(cp, 0.0, 1.0);
|
||||
gl_Position = options.MVPMatrix * vec4(p, 0.0, 1.0);
|
||||
offset = length(v01) * pos.x + options.stipple_width.z;
|
||||
vpos = vec2(gl_Position.x * options.screensize.x, gl_Position.y * options.screensize.y);
|
||||
cpos = vec2(pcp.x * options.screensize.x, pcp.y * options.screensize.y);
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
noperspective in vec2 vpos;
|
||||
noperspective in vec2 cpos;
|
||||
noperspective in float offset;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
// stipple
|
||||
if(options.stipple_width.y <= 0) { // stipple disabled
|
||||
outColor = options.color0;
|
||||
} else {
|
||||
float t = options.stipple_width.x + options.stipple_width.y;
|
||||
float s = mod(offset, t);
|
||||
float sd = s - options.stipple_width.x;
|
||||
vec4 colors = options.color1;
|
||||
if(colors.a < (1.0/255.0)) colors.rgb = options.color0.rgb;
|
||||
if(s <= 0.5 || s >= t - 0.5) {
|
||||
outColor = mix(colors, options.color0, mod(s + 0.5, t));
|
||||
} else if(s >= options.stipple_width.x - 0.5 && s <= options.stipple_width.x + 0.5) {
|
||||
outColor = mix(options.color0, colors, s - (options.stipple_width.x - 0.5));
|
||||
} else if(s < options.stipple_width.x) {
|
||||
outColor = options.color0;
|
||||
} else {
|
||||
outColor = colors;
|
||||
}
|
||||
}
|
||||
// antialias along edge of line
|
||||
float cdist = length(cpos - vpos);
|
||||
if(cdist > options.stipple_width.w) {
|
||||
outColor.a *= clamp(1.0 - (cdist - options.stipple_width.w), 0.0, 1.0);
|
||||
}
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 mvpmatrix; // pixel matrix
|
||||
vec4 screensize; // width,height of screen (for antialiasing)
|
||||
vec4 center; // center of point
|
||||
vec4 radius_border;
|
||||
vec4 color; // color point
|
||||
vec4 colorBorder; // color of border
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // four corners of point ([0,0], [0,1], [1,1], [1,0])
|
||||
|
||||
noperspective out vec2 vpos; // position scaled by screensize
|
||||
|
||||
void main() {
|
||||
float radius_border = options.radius_border.x + options.radius_border.y;
|
||||
vec2 p = options.center.xy + (pos - vec2(0.5, 0.5)) * radius_border;
|
||||
gl_Position = options.mvpmatrix * vec4(p, 0.0, 1.0);
|
||||
vpos = gl_Position.xy * options.screensize.xy; // just p?
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
noperspective in vec2 vpos;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
float radius_border = options.radius_border.x + options.radius_border.y;
|
||||
vec4 colorb = options.colorBorder;
|
||||
if(colorb.a < (1.0/255.0)) colorb.rgb = options.color.rgb;
|
||||
vec2 ctr = (options.mvpmatrix * vec4(options.center.xy, 0.0, 1.0)).xy;
|
||||
float d = distance(vpos, ctr.xy * options.screensize.xy);
|
||||
if(d > radius_border) { discard; return; }
|
||||
if(d <= options.radius_border.x) {
|
||||
float d2 = options.radius_border.x - d;
|
||||
outColor = mix(colorb, options.color, clamp(d2 - options.radius_border.y/2.0, 0.0, 1.0));
|
||||
} else {
|
||||
float d2 = d - options.radius_border.x;
|
||||
outColor = mix(colorb, vec4(colorb.rgb,0), clamp(d2 - options.radius_border.y/2.0, 0.0, 1.0));
|
||||
}
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 MVPMatrix; // pixel matrix
|
||||
vec4 pos0;
|
||||
vec4 color0;
|
||||
vec4 pos1;
|
||||
vec4 color1;
|
||||
vec4 pos2;
|
||||
vec4 color2;
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // x: [0,1], alpha. y: [0,1], beta
|
||||
|
||||
out vec4 color;
|
||||
|
||||
void main() {
|
||||
float a = clamp(pos.x, 0.0, 1.0);
|
||||
float b = clamp(pos.y, 0.0, 1.0);
|
||||
float c = 1.0 - a - b;
|
||||
vec2 p = (options.pos0 * a + options.pos1 * b + options.pos2 * c).xy;
|
||||
gl_Position = options.MVPMatrix * vec4(p, 0.0, 1.0);
|
||||
color = options.color0 * a + options.color1 * b + options.color2 * c;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec4 color;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
outColor = color;
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
struct Options {
|
||||
mat4 MVPMatrix; // view matrix
|
||||
vec4 pos0;
|
||||
vec4 color0;
|
||||
vec4 pos1;
|
||||
vec4 color1;
|
||||
vec4 pos2;
|
||||
vec4 color2;
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
|
||||
const bool srgbTarget = true;
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos; // x: [0,1], alpha. y: [0,1], beta
|
||||
|
||||
out vec4 color;
|
||||
|
||||
void main() {
|
||||
float a = clamp(pos.x, 0.0, 1.0);
|
||||
float b = clamp(pos.y, 0.0, 1.0);
|
||||
float c = 1.0 - a - b;
|
||||
vec3 p = vec3(options.pos0) * a + vec3(options.pos1) * b + vec3(options.pos2) * c;
|
||||
gl_Position = options.MVPMatrix * vec4(p, 1.0);
|
||||
color = options.color0 * a + options.color1 * b + options.color2 * c;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec4 color;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
outColor = color;
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
outColor = blender_srgb_to_framebuffer_space(outColor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#version 330
|
||||
|
||||
// // the following two lines are an attempt to solve issues #1025, #879, #753
|
||||
// precision mediump float;
|
||||
// precision lowp int; // only used to represent enum or bool
|
||||
|
||||
struct Options {
|
||||
mat4 uMVPMatrix;
|
||||
|
||||
vec4 lrtb;
|
||||
vec4 wh;
|
||||
|
||||
vec4 depth;
|
||||
|
||||
vec4 margin_lrtb;
|
||||
vec4 padding_lrtb;
|
||||
|
||||
vec4 border_width_radius;
|
||||
vec4 border_left_color;
|
||||
vec4 border_right_color;
|
||||
vec4 border_top_color;
|
||||
vec4 border_bottom_color;
|
||||
|
||||
vec4 background_color;
|
||||
|
||||
// see IMAGE_SCALE_XXX values below
|
||||
ivec4 image_settings;
|
||||
};
|
||||
|
||||
uniform Options options;
|
||||
uniform sampler2D image;
|
||||
|
||||
|
||||
const bool srgbTarget = true;
|
||||
|
||||
bool image_use() { return options.image_settings[0] != 0; }
|
||||
int image_fit() { return options.image_settings[1]; }
|
||||
|
||||
float pos_l() { return options.lrtb[0]; }
|
||||
float pos_r() { return options.lrtb[1]; }
|
||||
float pos_t() { return options.lrtb[2]; }
|
||||
float pos_b() { return options.lrtb[3]; }
|
||||
|
||||
float size_w() { return options.wh[0]; }
|
||||
float size_h() { return options.wh[1]; }
|
||||
|
||||
float depth() { return options.depth[0]; }
|
||||
|
||||
float margin_l() { return options.margin_lrtb[0]; }
|
||||
float margin_r() { return options.margin_lrtb[1]; }
|
||||
float margin_t() { return options.margin_lrtb[2]; }
|
||||
float margin_b() { return options.margin_lrtb[3]; }
|
||||
float padding_l() { return options.padding_lrtb[0]; }
|
||||
float padding_r() { return options.padding_lrtb[1]; }
|
||||
float padding_t() { return options.padding_lrtb[2]; }
|
||||
float padding_b() { return options.padding_lrtb[3]; }
|
||||
|
||||
float border_width() { return options.border_width_radius[0]; }
|
||||
float border_radius() { return options.border_width_radius[1]; }
|
||||
vec4 border_left_color() { return options.border_left_color; }
|
||||
vec4 border_right_color() { return options.border_right_color; }
|
||||
vec4 border_top_color() { return options.border_top_color; }
|
||||
vec4 border_bottom_color() { return options.border_bottom_color; }
|
||||
|
||||
vec4 background_color() { return options.background_color; }
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// vertex shader
|
||||
|
||||
in vec2 pos;
|
||||
|
||||
out vec2 screen_pos;
|
||||
|
||||
void main() {
|
||||
// set vertex to bottom-left, top-left, top-right, or bottom-right location, depending on pos
|
||||
vec2 p = vec2(
|
||||
(pos.x < 0.5) ? (pos_l() - 1.0) : (pos_r() + 1.0),
|
||||
(pos.y < 0.5) ? (pos_b() - 1.0) : (pos_t() + 1.0)
|
||||
);
|
||||
|
||||
// convert depth to z-order
|
||||
float zorder = 1.0 - depth() / 1000.0;
|
||||
|
||||
screen_pos = p;
|
||||
gl_Position = options.uMVPMatrix * vec4(p, zorder, 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// fragment shader
|
||||
|
||||
in vec2 screen_pos;
|
||||
|
||||
out vec4 outColor;
|
||||
out float gl_FragDepth;
|
||||
|
||||
float sqr(float s) { return s * s; }
|
||||
float sumsqr(float a, float b) { return sqr(a) + sqr(b); }
|
||||
float min4(float a, float b, float c, float d) { return min(min(min(a, b), c) ,d); }
|
||||
|
||||
vec4 mix_over(vec4 above, vec4 below) {
|
||||
vec3 a_ = above.rgb * above.a;
|
||||
vec3 b_ = below.rgb * below.a;
|
||||
float alpha = above.a + (1.0 - above.a) * below.a;
|
||||
return vec4((a_ + b_ * (1.0 - above.a)) / alpha, alpha);
|
||||
}
|
||||
|
||||
int get_margin_region(float dist_left, float dist_right, float dist_top, float dist_bottom) {
|
||||
float dist_min = min4(dist_left, dist_right, dist_top, dist_bottom);
|
||||
if(dist_min == dist_left) return REGION_MARGIN_LEFT;
|
||||
if(dist_min == dist_right) return REGION_MARGIN_RIGHT;
|
||||
if(dist_min == dist_top) return REGION_MARGIN_TOP;
|
||||
if(dist_min == dist_bottom) return REGION_MARGIN_BOTTOM;
|
||||
return REGION_ERROR; // this should never happen
|
||||
}
|
||||
|
||||
int get_region() {
|
||||
/* this function determines which region the fragment is in wrt properties of UI element,
|
||||
specifically: position, size, border width, border radius, margins
|
||||
|
||||
v top-left
|
||||
+-----------------+
|
||||
| \ / | <- margin regions
|
||||
| +---------+ |
|
||||
| |\ /| | <- border regions
|
||||
| | +-----+ | |
|
||||
| | | | | | <- inside border region (content area + padding)
|
||||
| | +-----+ | |
|
||||
| |/ \| |
|
||||
| +---------+ |
|
||||
| / \ |
|
||||
+-----------------+
|
||||
^ bottom-right
|
||||
|
||||
- margin regions
|
||||
- broken into top, right, bottom, left
|
||||
- each TRBL margin size can be different size
|
||||
- border regions
|
||||
- broken into top, right, bottom, left
|
||||
- each can have different colors, but all same size (TODO!)
|
||||
- inside border region
|
||||
- where content is drawn (image)
|
||||
- NOTE: padding takes up this space
|
||||
- ERROR region _should_ never happen, but can be returned from this fn if something goes wrong
|
||||
*/
|
||||
|
||||
float dist_left = screen_pos.x - (pos_l() + margin_l());
|
||||
float dist_right = (pos_r() - margin_r() + 1.0) - screen_pos.x;
|
||||
float dist_bottom = screen_pos.y - (pos_b() + margin_b() - 1.0);
|
||||
float dist_top = (pos_t() - margin_t()) - screen_pos.y;
|
||||
float radwid = max(border_radius(), border_width());
|
||||
float rad = max(0.0, border_radius() - border_width());
|
||||
float radwid2 = sqr(radwid);
|
||||
float rad2 = sqr(rad);
|
||||
|
||||
if(dist_left < 0 || dist_right < 0 || dist_top < 0 || dist_bottom < 0) return REGION_OUTSIDE;
|
||||
|
||||
// margin
|
||||
int margin_region = get_margin_region(dist_left, dist_right, dist_top, dist_bottom);
|
||||
|
||||
// within top and bottom, might be left or right side
|
||||
if(dist_bottom > radwid && dist_top > radwid) {
|
||||
if(dist_left > border_width() && dist_right > border_width()) return REGION_BACKGROUND;
|
||||
if(dist_left < dist_right) return REGION_BORDER_LEFT;
|
||||
return REGION_BORDER_RIGHT;
|
||||
}
|
||||
|
||||
// within left and right, might be bottom or top
|
||||
if(dist_left > radwid && dist_right > radwid) {
|
||||
if(dist_bottom > border_width() && dist_top > border_width()) return REGION_BACKGROUND;
|
||||
if(dist_bottom < dist_top) return REGION_BORDER_BOTTOM;
|
||||
return REGION_BORDER_TOP;
|
||||
}
|
||||
|
||||
// top-left
|
||||
if(dist_top <= radwid && dist_left <= radwid) {
|
||||
float r2 = sumsqr(dist_left - radwid, dist_top - radwid);
|
||||
if(r2 > radwid2) return margin_region;
|
||||
if(r2 < rad2) return REGION_BACKGROUND;
|
||||
if(dist_left < dist_top) return REGION_BORDER_LEFT;
|
||||
return REGION_BORDER_TOP;
|
||||
}
|
||||
// top-right
|
||||
if(dist_top <= radwid && dist_right <= radwid) {
|
||||
float r2 = sumsqr(dist_right - radwid, dist_top - radwid);
|
||||
if(r2 > radwid2) return margin_region;
|
||||
if(r2 < rad2) return REGION_BACKGROUND;
|
||||
if(dist_right < dist_top) return REGION_BORDER_RIGHT;
|
||||
return REGION_BORDER_TOP;
|
||||
}
|
||||
// bottom-left
|
||||
if(dist_bottom <= radwid && dist_left <= radwid) {
|
||||
float r2 = sumsqr(dist_left - radwid, dist_bottom - radwid);
|
||||
if(r2 > radwid2) return margin_region;
|
||||
if(r2 < rad2) return REGION_BACKGROUND;
|
||||
if(dist_left < dist_bottom) return REGION_BORDER_LEFT;
|
||||
return REGION_BORDER_BOTTOM;
|
||||
}
|
||||
// bottom-right
|
||||
if(dist_bottom <= radwid && dist_right <= radwid) {
|
||||
float r2 = sumsqr(dist_right - radwid, dist_bottom - radwid);
|
||||
if(r2 > radwid2) return margin_region;
|
||||
if(r2 < rad2) return REGION_BACKGROUND;
|
||||
if(dist_right < dist_bottom) return REGION_BORDER_RIGHT;
|
||||
return REGION_BORDER_BOTTOM;
|
||||
}
|
||||
|
||||
// something bad happened
|
||||
return REGION_ERROR;
|
||||
}
|
||||
|
||||
vec4 mix_image(vec4 bg) {
|
||||
vec4 c = bg;
|
||||
// drawing space
|
||||
float dw = size_w() - (margin_l() + border_width() + padding_l() + padding_r() + border_width() + margin_r());
|
||||
float dh = size_h() - (margin_t() + border_width() + padding_t() + padding_b() + border_width() + margin_b());
|
||||
float dx = screen_pos.x - (pos_l() + (margin_l() + border_width() + padding_l()));
|
||||
float dy = -(screen_pos.y - (pos_t() - (margin_t() + border_width() + padding_t())));
|
||||
float dsx = (dx + 0.5) / dw;
|
||||
float dsy = (dy + 0.5) / dh;
|
||||
// texture
|
||||
vec2 tsz = vec2(textureSize(image, 0));
|
||||
float tw = tsz.x, th = tsz.y;
|
||||
float tx, ty;
|
||||
|
||||
switch(image_fit()) {
|
||||
case IMAGE_SCALE_FILL:
|
||||
// object-fit: fill = stretch / squash to fill entire drawing space (non-uniform scale)
|
||||
// do nothing here
|
||||
tx = tw * dx / dw;
|
||||
ty = th * dy / dh;
|
||||
break;
|
||||
case IMAGE_SCALE_CONTAIN: {
|
||||
// object-fit: contain = uniformly scale texture to fit entirely in drawing space (will be letterboxed)
|
||||
// find smaller scaled dimension, and use that
|
||||
float _tw, _th;
|
||||
if(dw / dh < tw / th) {
|
||||
// scaling by height is too big, so scale by width
|
||||
_tw = tw;
|
||||
_th = tw * dh / dw;
|
||||
} else {
|
||||
_tw = th * dw / dh;
|
||||
_th = th;
|
||||
}
|
||||
tx = dsx * _tw - (_tw - tw) / 2.0;
|
||||
ty = dsy * _th - (_th - th) / 2.0;
|
||||
break; }
|
||||
case IMAGE_SCALE_COVER: {
|
||||
// object-fit: cover = uniformly scale texture to fill entire drawing space (will be cropped)
|
||||
// find larger scaled dimension, and use that
|
||||
float _tw, _th;
|
||||
if(dw / dh > tw / th) {
|
||||
// scaling by height is too big, so scale by width
|
||||
_tw = tw;
|
||||
_th = tw * dh / dw;
|
||||
} else {
|
||||
_tw = th * dw / dh;
|
||||
_th = th;
|
||||
}
|
||||
tx = dsx * _tw - (_tw - tw) / 2.0;
|
||||
ty = dsy * _th - (_th - th) / 2.0;
|
||||
break; }
|
||||
case IMAGE_SCALE_DOWN:
|
||||
// object-fit: scale-down = either none or contain, whichever is smaller
|
||||
if(dw >= tw && dh >= th) {
|
||||
// none
|
||||
tx = dx + (tw - dw) / 2.0;
|
||||
ty = dy + (th - dh) / 2.0;
|
||||
} else {
|
||||
float _tw, _th;
|
||||
if(dw / dh < tw / th) {
|
||||
// scaling by height is too big, so scale by width
|
||||
_tw = tw;
|
||||
_th = tw * dh / dw;
|
||||
} else {
|
||||
_tw = th * dw / dh;
|
||||
_th = th;
|
||||
}
|
||||
tx = dsx * _tw - (_tw - tw) / 2.0;
|
||||
ty = dsy * _th - (_th - th) / 2.0;
|
||||
}
|
||||
break;
|
||||
case IMAGE_SCALE_NONE:
|
||||
// object-fit: none (no resizing)
|
||||
tx = dx + (tw - dw) / 2.0;
|
||||
ty = dy + (th - dh) / 2.0;
|
||||
break;
|
||||
default: // error!
|
||||
tx = tw / 2.0;
|
||||
ty = th / 2.0;
|
||||
break;
|
||||
}
|
||||
|
||||
vec2 texcoord = vec2(tx / tw, 1 - ty / th);
|
||||
bool inside = 0.0 <= texcoord.x && texcoord.x <= 1.0 && 0.0 <= texcoord.y && texcoord.y <= 1.0;
|
||||
if(inside) {
|
||||
vec4 t = texture(image, texcoord) + COLOR_DEBUG_IMAGE;
|
||||
c = mix_over(t, c);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_IMAGE_CHECKER
|
||||
if(inside) {
|
||||
// generate checker pattern to test scaling
|
||||
switch((int(32.0 * texcoord.x) + 4 * int(32.0 * texcoord.y)) % 16) {
|
||||
case 0: c = COLOR_CHECKER_00; break;
|
||||
case 1: c = COLOR_CHECKER_01; break;
|
||||
case 2: c = COLOR_CHECKER_02; break;
|
||||
case 3: c = COLOR_CHECKER_03; break;
|
||||
case 4: c = COLOR_CHECKER_04; break;
|
||||
case 5: c = COLOR_CHECKER_05; break;
|
||||
case 6: c = COLOR_CHECKER_06; break;
|
||||
case 7: c = COLOR_CHECKER_07; break;
|
||||
case 8: c = COLOR_CHECKER_08; break;
|
||||
case 9: c = COLOR_CHECKER_09; break;
|
||||
case 10: c = COLOR_CHECKER_10; break;
|
||||
case 11: c = COLOR_CHECKER_11; break;
|
||||
case 12: c = COLOR_CHECKER_12; break;
|
||||
case 13: c = COLOR_CHECKER_13; break;
|
||||
case 14: c = COLOR_CHECKER_14; break;
|
||||
case 15: c = COLOR_CHECKER_15; break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_IMAGE_OUTSIDE
|
||||
if(!inside) {
|
||||
c = vec4(
|
||||
1.0 - (1.0 - c.r) * 0.5,
|
||||
1.0 - (1.0 - c.g) * 0.5,
|
||||
1.0 - (1.0 - c.b) * 0.5,
|
||||
c.a
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
|
||||
{
|
||||
if (srgbTarget) {
|
||||
vec3 c = max(in_color.rgb, vec3(0.0));
|
||||
vec3 c1 = c * (1.0 / 12.92);
|
||||
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
|
||||
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
|
||||
}
|
||||
return in_color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 c = vec4(0,0,0,0);
|
||||
|
||||
int region = get_region();
|
||||
|
||||
// workaround switched-discard (issue #1042)
|
||||
#ifndef DEBUG_DONT_DISCARD
|
||||
#ifndef DEBUG_COLOR_REGIONS
|
||||
#ifndef DEBUG_COLOR_MARGINS
|
||||
if(region == REGION_MARGIN_TOP) { discard; return; }
|
||||
if(region == REGION_MARGIN_RIGHT) { discard; return; }
|
||||
if(region == REGION_MARGIN_BOTTOM) { discard; return; }
|
||||
if(region == REGION_MARGIN_LEFT) { discard; return; }
|
||||
#endif
|
||||
if(region == REGION_OUTSIDE) { discard; return; }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
switch(region) {
|
||||
case REGION_BORDER_TOP: c = border_top_color(); break;
|
||||
case REGION_BORDER_RIGHT: c = border_right_color(); break;
|
||||
case REGION_BORDER_BOTTOM: c = border_bottom_color(); break;
|
||||
case REGION_BORDER_LEFT: c = border_left_color(); break;
|
||||
case REGION_BACKGROUND: c = background_color(); break;
|
||||
|
||||
// following colors show only if DEBUG settings allow or something really unexpected happens
|
||||
case REGION_MARGIN_TOP: c = COLOR_MARGIN_TOP; break;
|
||||
case REGION_MARGIN_RIGHT: c = COLOR_MARGIN_RIGHT; break;
|
||||
case REGION_MARGIN_BOTTOM: c = COLOR_MARGIN_BOTTOM; break;
|
||||
case REGION_MARGIN_LEFT: c = COLOR_MARGIN_LEFT; break;
|
||||
case REGION_OUTSIDE: c = COLOR_OUTSIDE; break; // keep transparent
|
||||
case REGION_ERROR: c = COLOR_ERROR; break; // should never hit here
|
||||
default: c = COLOR_ERROR_NEVER; // should **really** never hit here
|
||||
}
|
||||
|
||||
// DEBUG_COLOR_REGIONS will mix over other colors
|
||||
#ifdef DEBUG_COLOR_REGIONS
|
||||
switch(region) {
|
||||
case REGION_BORDER_TOP: c = mix_over(COLOR_BORDER_TOP, c); break;
|
||||
case REGION_BORDER_RIGHT: c = mix_over(COLOR_BORDER_RIGHT, c); break;
|
||||
case REGION_BORDER_BOTTOM: c = mix_over(COLOR_BORDER_BOTTOM, c); break;
|
||||
case REGION_BORDER_LEFT: c = mix_over(COLOR_BORDER_LEFT, c); break;
|
||||
case REGION_BACKGROUND: c = mix_over(COLOR_BACKGROUND, c); break;
|
||||
}
|
||||
#endif
|
||||
|
||||
// apply image if used
|
||||
if(image_use()) c = mix_image(c);
|
||||
|
||||
c = vec4(c.rgb * c.a, c.a);
|
||||
|
||||
// https://wiki.blender.org/wiki/Reference/Release_Notes/2.83/Python_API
|
||||
c = blender_srgb_to_framebuffer_space(c);
|
||||
|
||||
#ifdef DEBUG_SNAP_ALPHA
|
||||
if(c.a < 0.25) {
|
||||
c.a = 0.0;
|
||||
#ifndef DEBUG_DONT_DISCARD
|
||||
discard; return;
|
||||
#endif
|
||||
}
|
||||
else c.a = 1.0;
|
||||
#endif
|
||||
|
||||
outColor = c;
|
||||
//gl_FragDepth = gl_FragDepth * 0.999999;
|
||||
gl_FragDepth = gl_FragCoord.z * 0.999999; // fix for issue #915?
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
def fix_string(s, *, remove_indentation=True, remove_initial_newline=True, remove_trailing_spaces=True):
|
||||
if remove_initial_newline:
|
||||
s = re.sub(r'^\n', '', s)
|
||||
|
||||
if remove_trailing_spaces:
|
||||
s = re.sub(r' +\n', '\n', s)
|
||||
s = re.sub(r' +$', '', s)
|
||||
|
||||
if remove_indentation:
|
||||
indent = min((
|
||||
len(line) - len(line.lstrip())
|
||||
for line in s.splitlines()
|
||||
if line.strip()
|
||||
), default=0)
|
||||
|
||||
s = '\n'.join(
|
||||
line if not line.strip() else line[indent:]
|
||||
for line in s.splitlines()
|
||||
)
|
||||
|
||||
return s
|
||||
@@ -0,0 +1,178 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
|
||||
from bpy.types import (
|
||||
Context,
|
||||
Window,
|
||||
WindowManager,
|
||||
)
|
||||
|
||||
import inspect
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
class TimerHandler:
|
||||
def __init__(self, hz:float, *, context:Context=None, wm:WindowManager=None, win:Window=None, enabled=True):
|
||||
context = context or bpy.context
|
||||
|
||||
self._wm = wm or context.window_manager
|
||||
self._win = win or context.window
|
||||
self._hz = max(0.1, hz)
|
||||
self._timer = None
|
||||
|
||||
self.enable(enabled)
|
||||
|
||||
def __del__(self):
|
||||
self.done()
|
||||
|
||||
def start(self):
|
||||
if self._timer: return
|
||||
self._timer = self._wm.event_timer_add(1.0 / self._hz, window=self._win)
|
||||
|
||||
def stop(self):
|
||||
if not self._timer: return
|
||||
self._wm.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
|
||||
def done(self):
|
||||
self.stop()
|
||||
|
||||
def enable(self, v):
|
||||
if v: self.start()
|
||||
else: self.stop()
|
||||
|
||||
|
||||
class StopwatchHandler:
|
||||
@staticmethod
|
||||
def delayed(*, time_delay=None, fn_delay=None):
|
||||
def wrap_fn(fn):
|
||||
sw = StopwatchHandler(fn, time_delay=time_delay, fn_delay=fn_delay)
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
sw.start(*args, **kwargs)
|
||||
wrapper.is_going = sw.is_going
|
||||
wrapper.cancel = sw.cancel
|
||||
wrapper.reset = sw.reset
|
||||
return wrapper
|
||||
return wrap_fn
|
||||
|
||||
def __init__(self, fn, *, time_delay=None, fn_delay=None):
|
||||
assert time_delay is not None or fn_delay is not None, f'Addon Common: Must specify either time_delay or fn_delay'
|
||||
self.fn = lambda: fn(*self._args, **self._kwargs)
|
||||
self.time_delay = time_delay
|
||||
self.fn_delay = fn_delay
|
||||
|
||||
@property
|
||||
def is_going(self):
|
||||
return bpy.app.timers.is_registered(self.fn)
|
||||
|
||||
def start(self, *args, **kwargs):
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
time_delay = self.time_delay or self.fn_delay()
|
||||
bpy.app.timers.register(self.fn, first_interval=time_delay)
|
||||
|
||||
def cancel(self):
|
||||
if self.is_going:
|
||||
bpy.app.timers.unregister(self.fn)
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
self.cancel()
|
||||
self.start(*args, **kwargs)
|
||||
|
||||
|
||||
class CallGovernor:
|
||||
# NOTE: bpy.app.timers.is_registered(self._call_now) does _NOT_ work!
|
||||
# but, setting self.fn_call_now = self._call_now and then calling
|
||||
# bpy.app.timers.is_registered(self.fn_call_now) does!
|
||||
|
||||
@staticmethod
|
||||
def limit(**kwargs):
|
||||
def wrap_fn(fn):
|
||||
cg = CallGovernor(fn, **kwargs)
|
||||
@wraps(fn)
|
||||
def wrapper(*fn_args, **fn_kwargs):
|
||||
cg(*fn_args, **fn_kwargs)
|
||||
wrapper.unpause = cg.unpause
|
||||
wrapper.stop = cg.stop
|
||||
return wrapper
|
||||
return wrap_fn
|
||||
|
||||
def __init__(self, fn, *, time_limit=None, fn_delay=None, pause_after_call=None):
|
||||
assert not all([
|
||||
time_limit is None,
|
||||
fn_delay is None,
|
||||
pause_after_call is None,
|
||||
]), 'Addon Common: Must specify at least one option'
|
||||
self.time_limit = time_limit
|
||||
self.fn_delay = fn_delay
|
||||
self.pause_after_call = pause_after_call
|
||||
self.fn = fn
|
||||
self._paused = False
|
||||
self._call_when_paused = False
|
||||
self._next_call = time.time()
|
||||
self._fn_call_now = self._call_now # THIS IS NEEDED!!! see note above
|
||||
|
||||
def unpause(self, *args):
|
||||
if not self._paused: return
|
||||
self._paused = False
|
||||
if self._call_when_paused:
|
||||
self._call_now()
|
||||
|
||||
@property
|
||||
def _calling_later(self):
|
||||
return bpy.app.timers.is_registered(self._fn_call_now)
|
||||
|
||||
def _call_now(self):
|
||||
self.stop()
|
||||
|
||||
if self.time_limit is not None:
|
||||
self._next_call = time.time() + self.time_limit
|
||||
elif self.fn_delay is not None:
|
||||
self._next_call = time.time() + self.fn_delay()
|
||||
|
||||
if self.pause_after_call:
|
||||
self._paused = True
|
||||
self._call_when_paused = False
|
||||
|
||||
self.fn(*self._args)
|
||||
|
||||
def __call__(self, *args, now=False):
|
||||
self._args = args
|
||||
|
||||
if self.time_limit is not None or self.fn_delay is not None:
|
||||
time_to_next_call = self._next_call - time.time()
|
||||
if now or time_to_next_call <= 0:
|
||||
self._call_now()
|
||||
elif not self._calling_later:
|
||||
bpy.app.timers.register(self._fn_call_now, first_interval=time_to_next_call)
|
||||
|
||||
if self.pause_after_call:
|
||||
if now or not self._paused:
|
||||
self._call_now()
|
||||
elif not self._calling_later:
|
||||
self._call_when_paused = True
|
||||
|
||||
def stop(self):
|
||||
if not self._calling_later: return
|
||||
bpy.app.timers.unregister(self._fn_call_now)
|
||||
@@ -0,0 +1,389 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from itertools import dropwhile, zip_longest
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from gpu_extras.presets import draw_texture_2d
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from . import ui_settings # needs to be first
|
||||
|
||||
from .ui_core_content import UI_Core_Content
|
||||
from .ui_core_debug import UI_Core_Debug
|
||||
from .ui_core_dirtiness import UI_Core_Dirtiness
|
||||
from .ui_core_draw import UI_Core_Draw
|
||||
from .ui_core_elements import UI_Core_Elements, tags_known
|
||||
from .ui_core_events import UI_Core_Events
|
||||
from .ui_core_fonts import get_font
|
||||
from .ui_core_images import get_loading_image, is_image_cached, load_texture, async_load_image, load_image
|
||||
from .ui_core_layout import UI_Core_Layout
|
||||
from .ui_core_markdown import UI_Core_Markdown
|
||||
from .ui_core_preventmulticalls import UI_Core_PreventMultiCalls
|
||||
from .ui_core_properties import UI_Core_Properties
|
||||
from .ui_core_style import UI_Core_Style
|
||||
from .ui_core_utilities import UI_Core_Utils, helper_wraptext, convert_token_to_cursor
|
||||
|
||||
from .ui_draw import ui_draw
|
||||
from .ui_event import UI_Event
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
|
||||
from . import gpustate
|
||||
from .blender import tag_redraw_all, get_path_from_addon_common, get_path_from_addon_root
|
||||
from .boundvar import BoundVar
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .drawing import Drawing
|
||||
from .fsm import FSM
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .maths import floor_if_finite, ceil_if_finite
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join, kwargs_splitter
|
||||
|
||||
|
||||
'''
|
||||
# NOTES
|
||||
|
||||
dirty_styling
|
||||
|
||||
- clears all style caching
|
||||
- always calls dirty_styling on children <== TODO!
|
||||
|
||||
|
||||
dirty_flow
|
||||
|
||||
- ignored if _dirtying_flow is True already
|
||||
- sets _dirtying_flow to True
|
||||
- possibly calls parent's dirty_flow
|
||||
- possibly calls children's dirty_flow
|
||||
- _layout() returns early if _dirtying_flow is False
|
||||
|
||||
|
||||
UI_Document manages UI_Body
|
||||
|
||||
example hierarchy of UI
|
||||
|
||||
- UI_Body: (singleton!)
|
||||
- UI_Dialog: tooltips
|
||||
- UI_Dialog: menu
|
||||
- help
|
||||
- about
|
||||
- exit
|
||||
- UI_Dialog: tools
|
||||
- UI_Button: toolA
|
||||
- UI_Button: toolB
|
||||
- UI_Button: toolC
|
||||
- UI_Dialog: options
|
||||
- option1
|
||||
- option2
|
||||
- option3
|
||||
|
||||
|
||||
clean call order
|
||||
|
||||
- compute_style (only if style is dirty)
|
||||
- call compute_style on all children
|
||||
- dirtied by change in style, ID, class, pseudoclass, parent, or ID/class/pseudoclass of an ancestor
|
||||
- cleaning style dirties size
|
||||
- compute_preferred_size (only if size or content are dirty)
|
||||
- determines min, max, preferred size for element (override in subclass)
|
||||
- for containers that resize based on children, whether wrapped (inline), list (block), or table, ...
|
||||
- ...
|
||||
|
||||
'''
|
||||
|
||||
|
||||
|
||||
class UI_Element(
|
||||
UI_Core_Content,
|
||||
UI_Core_Debug,
|
||||
UI_Core_Dirtiness,
|
||||
UI_Core_Draw,
|
||||
UI_Core_Elements,
|
||||
UI_Core_Events,
|
||||
UI_Core_Layout,
|
||||
UI_Core_Markdown,
|
||||
UI_Core_PreventMultiCalls,
|
||||
UI_Core_Properties,
|
||||
UI_Core_Style,
|
||||
UI_Core_Utils,
|
||||
):
|
||||
|
||||
@staticmethod
|
||||
def new_element(*args, **kwargs):
|
||||
return UI_Element(*args, **kwargs)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._init_debug()
|
||||
self._init_properties()
|
||||
self._init_events()
|
||||
self._init_dirtiness()
|
||||
self._init_content()
|
||||
|
||||
###################################################
|
||||
# start setting properties
|
||||
# NOTE: some properties require special handling
|
||||
|
||||
# handle innerText
|
||||
# if 'innerText' in kwargs and kwargs.get('pseudoelement', '') != 'text':
|
||||
# innerText = kwargs['innerText']
|
||||
# del kwargs['innerText']
|
||||
# kwargs.setdefault('children', [])
|
||||
# kwargs['children'] += [UI_Element(tagName='text', pseudoelement='text', innerText=innerText)]
|
||||
# print(f'UI_Element: {kwargs["tagName"]} creating <text::text> for "{innerText}"')
|
||||
|
||||
with self.defer_dirty('setting initial properties'):
|
||||
# NOTE: handle attribs in multiple passes, so that debug prints are more informative
|
||||
|
||||
kwargs_events = kwargs_splitter(kwargs, keys=self._events.keys())
|
||||
kwargs_special0 = kwargs_splitter(kwargs, keys={'atomic', 'max', 'min', 'value', 'checked'})
|
||||
kwargs_special1 = kwargs_splitter(kwargs, keys={'innerText', 'parent', '_parent', 'children'})
|
||||
kwargs_unhandled = kwargs_splitter(kwargs, fn=(lambda k,_: not hasattr(self, k)))
|
||||
|
||||
# handle special properties
|
||||
for k, v in kwargs_special0.items():
|
||||
match k:
|
||||
case 'atomic':
|
||||
self._atomic = v
|
||||
case 'max':
|
||||
self.valueMax = v
|
||||
case 'min':
|
||||
self.valueMin = v
|
||||
case 'value':
|
||||
if isinstance(v, BoundVar): self.value_bind(v)
|
||||
else: self.value = v
|
||||
case 'checked':
|
||||
if isinstance(v, BoundVar): self.checked_bind(v)
|
||||
else: self.checked = v
|
||||
|
||||
# handle other properties
|
||||
cls = type(self)
|
||||
for k, v in kwargs.items():
|
||||
# need to test that a setter exists for the property
|
||||
class_attr = getattr(cls, k, None)
|
||||
if type(class_attr) is property:
|
||||
# k is a property
|
||||
assert class_attr.fset is not None, f'Attempting to set a read-only property {k} to "{v}"'
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
# k is an attribute
|
||||
print(f'>> COOKIECUTTER UI WARNING: Setting non-property attribute {k} to "{v}"')
|
||||
setattr(self, k, v)
|
||||
|
||||
# handle special connections
|
||||
if kwargs_special1.get('innerText', None) is not None:
|
||||
self.innerText = kwargs_special1['innerText']
|
||||
if kwargs_special1.get('parent', None) is not None:
|
||||
# note: parent.append_child(self) will set self._parent
|
||||
kwargs_special1['parent'].append_child(self)
|
||||
if kwargs_special1.get('_parent', None) is not None:
|
||||
self._parent = kwargs_special1['_parent']
|
||||
self._document = self._parent.document
|
||||
self._do_not_dirty_parent = True
|
||||
if kwargs_special1.get('children', None):
|
||||
for child in kwargs_special1['children']:
|
||||
self.append_child(child)
|
||||
|
||||
# handle events
|
||||
for k, v in kwargs_events.items():
|
||||
# key is an event name, v is callback
|
||||
self.add_eventListener(k, v)
|
||||
|
||||
# report unhandled attribs
|
||||
if kwargs_unhandled:
|
||||
print(f'>> COOKIECUTTER UI WARNING: When creating new UI_Element, found unhandled attribute value pairs:')
|
||||
print(f' {kwargs_unhandled}')
|
||||
|
||||
self._setup_element() # NOTE: this must be done _after_ tag and type are set
|
||||
|
||||
self.dirty(cause='initially dirty')
|
||||
|
||||
def __del__(self):
|
||||
if self._cacheRenderBuf:
|
||||
self._cacheRenderBuf = None
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
if self._innerTextAsIs is not None:
|
||||
innerTextAsIs = self._innerTextAsIs.replace('\n', '\\n') if self._innerTextAsIs else ''
|
||||
return f"'{innerTextAsIs}'"
|
||||
if self._pseudoelement == 'text':
|
||||
innerText = self.innerText.replace('\n', '\\n') if self.innerText else ''
|
||||
return f'"{innerText}"'
|
||||
tagName = f'{self.tagName}::{self._pseudoelement}' if self._pseudoelement else self.tagName
|
||||
info = ['id', 'classes', 'type', 'value', 'title'] #, 'innerText', 'innerTextAsIs'
|
||||
info = [(k, getattr(self, k)) for k in info if hasattr(self, k)]
|
||||
info = [f'{k}="{v}"' for k,v in info if v]
|
||||
# if self._pseudoelement == 'text':
|
||||
# nl,bnl = '\n','\\n'
|
||||
# info += [f"{k}=\"{getattr(self, k).replace(nl, bnl)}\"" for k in ['innerText', 'innerTextAsIs'] if getattr(self, k) != None]
|
||||
if self.open: info += ['open']
|
||||
if self.is_dirty: info += ['dirty']
|
||||
if self._atomic: info += ['atomic']
|
||||
info = ' '.join(['']+info) if info else ''
|
||||
return f'<{tagName}{info}>'
|
||||
|
||||
@UI_Core_Utils.add_cleaning_callback('renderbuf')
|
||||
def _renderbuf(self):
|
||||
self._dirty_renderbuf = True
|
||||
self._dirty_properties.discard('renderbuf')
|
||||
|
||||
|
||||
|
||||
|
||||
# @UI_Core_Utils.add_option_callback('position:flexbox')
|
||||
# def position_flexbox(self, left, top, width, height):
|
||||
# pass
|
||||
# @UI_Core_Utils.add_option_callback('position:block')
|
||||
# def position_flexbox(self, left, top, width, height):
|
||||
# pass
|
||||
# @UI_Core_Utils.add_option_callback('position:inline')
|
||||
# def position_flexbox(self, left, top, width, height):
|
||||
# pass
|
||||
# @UI_Core_Utils.add_option_callback('position:none')
|
||||
# def position_flexbox(self, left, top, width, height):
|
||||
# pass
|
||||
|
||||
|
||||
# def position(self, left, top, width, height):
|
||||
# # pos and size define where this element exists
|
||||
# self._l, self._t = left, top
|
||||
# self._w, self._h = width, height
|
||||
|
||||
# dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
# display = self._computed_styles.get('display', 'block')
|
||||
# margin_top, margin_right, margin_bottom, margin_left = self._get_style_trbl('margin')
|
||||
# padding_top, padding_right, padding_bottom, padding_left = self._get_style_trbl('padding')
|
||||
# border_width = self._get_style_num('border-width', 0)
|
||||
|
||||
# l = left + dpi_mult * (margin_left + border_width + padding_left)
|
||||
# t = top - dpi_mult * (margin_top + border_width + padding_top)
|
||||
# w = width - dpi_mult * (margin_left + margin_right + border_width + border_width + padding_left + padding_right)
|
||||
# h = height - dpi_mult * (margin_top + margin_bottom + border_width + border_width + padding_top + padding_bottom)
|
||||
|
||||
# self.call_option_callback(('position:%s' % display), 'position:block', left, top, width, height)
|
||||
|
||||
# # wrap text
|
||||
# wrap_opts = {
|
||||
# 'text': self._innerText,
|
||||
# 'width': w,
|
||||
# 'fontid': self._fontid,
|
||||
# 'fontsize': self._fontsize,
|
||||
# 'preserve_newlines': (self._whitespace in {'pre', 'pre-line', 'pre-wrap'}),
|
||||
# 'collapse_spaces': (self._whitespace not in {'pre', 'pre-wrap'}),
|
||||
# 'wrap_text': (self._whitespace != 'pre'),
|
||||
# }
|
||||
# self._innerTextWrapped = helper_wraptext(**wrap_opts)
|
||||
|
||||
@property
|
||||
def absolute_pos(self):
|
||||
return self._absolute_pos
|
||||
|
||||
# @profiler.function
|
||||
def _setup_ltwh(self, recurse_children=True):
|
||||
if not self.is_visible: return
|
||||
|
||||
if not self._parent_size: return # layout has not been called yet....
|
||||
|
||||
# IMPORTANT! do NOT prevent this function from being called multiple times!
|
||||
# the position of input text boxes (inside the container) is set incorrectly when
|
||||
# :focus is set (might have to do with position: relative)
|
||||
|
||||
# parent_pos = self._parent.absolute_pos if self._parent else Point2D((0, self._parent_size.max_height-1))
|
||||
if self._tablecell_table:
|
||||
table_pos = self._tablecell_table.absolute_pos
|
||||
# rel_pos = self._relative_pos or RelPoint2D.ZERO
|
||||
# rel_offset = self._relative_offset or RelPoint2D.ZERO
|
||||
abs_pos = table_pos + self._tablecell_pos
|
||||
abs_size = self._tablecell_size
|
||||
else:
|
||||
parent_pos = self._relative_element.absolute_pos if self._relative_element and self._relative_element != self else Point2D((0, self._parent_size.max_height - 1))
|
||||
if not parent_pos: parent_pos = RelPoint2D.ZERO
|
||||
rel_pos = self._relative_pos or RelPoint2D.ZERO
|
||||
rel_offset = self._relative_offset or RelPoint2D.ZERO
|
||||
align_offset = self._alignment_offset or RelPoint2D.ZERO
|
||||
abs_pos = parent_pos + rel_pos + rel_offset + align_offset
|
||||
abs_size = self._absolute_size
|
||||
|
||||
self._absolute_pos = abs_pos + self._scroll_offset
|
||||
self._l = ceil_if_finite(abs_pos.x - 0.01)
|
||||
self._t = floor_if_finite(abs_pos.y + 0.01)
|
||||
self._w = ceil_if_finite(abs_size.width)
|
||||
self._h = ceil_if_finite(abs_size.height)
|
||||
self._r = ceil_if_finite(self._l + (self._w - 0.01))
|
||||
self._b = floor_if_finite(self._t - (self._h - 0.01))
|
||||
|
||||
if recurse_children:
|
||||
for child in self._children_all:
|
||||
child._setup_ltwh()
|
||||
|
||||
# @profiler.function
|
||||
def get_under_mouse(self, p:Point2D):
|
||||
if p is None: return None
|
||||
if self._pseudoelement: return None
|
||||
if self._w < 1 or self._h < 1: return None
|
||||
if not (self._l <= p.x <= self._r and self._b <= p.y <= self._t): return None
|
||||
# p is over element
|
||||
if not self.is_visible: return None
|
||||
if not self.can_hover: return None
|
||||
# element is visible and hoverable
|
||||
if self._atomic: return self
|
||||
for child in reversed(self._children):
|
||||
under = child.get_under_mouse(p)
|
||||
if under: return under
|
||||
return self
|
||||
|
||||
def get_mouse_distance(self, p:Point2D):
|
||||
l,t,w,h = self._l, self._t, self._w, self._h
|
||||
r,b = l+(w-1),t-(h-1)
|
||||
dx = p.x - clamp(p.x, l, r)
|
||||
dy = p.y - clamp(p.y, b, t)
|
||||
return math.sqrt(dx*dx + dy*dy)
|
||||
|
||||
|
||||
|
||||
|
||||
def create_fn(tag):
|
||||
def create(*args, **kwargs):
|
||||
return UI_Element(tagName=tag, *args, **kwargs)
|
||||
return create
|
||||
for tag in tags_known:
|
||||
setattr(UI_Element, tag.upper(), create_fn(tag))
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import time
|
||||
from math import floor, ceil
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
from . import ui_settings # needs to be first
|
||||
from .ui_core_images import get_loading_image, is_image_cached, load_texture, async_load_image, load_image
|
||||
from .ui_core_utilities import UI_Core_Utils, helper_wraptext, convert_token_to_cursor
|
||||
|
||||
from .globals import Globals
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .profiler import profiler, time_it
|
||||
|
||||
from . import html_to_unicode
|
||||
|
||||
|
||||
|
||||
class UI_Core_Content:
|
||||
def _init_content(self):
|
||||
# boxes for viewing (wrt blender region) and content (wrt view)
|
||||
# NOTE: content box is larger than viewing => scrolling, which is
|
||||
# managed by offsetting the content box up (y+1) or left (x-1)
|
||||
self._static_content_size = None # size of static content (text, image, etc.) w/o margin,border,padding
|
||||
self._dynamic_content_size = None # size of dynamic content (static or wrapped children) w/o mbp
|
||||
self._dynamic_full_size = None # size of dynamic content with mbp added
|
||||
self._mbp_width = None
|
||||
self._mbp_height = None
|
||||
self._relative_element = None
|
||||
self._relative_pos = None
|
||||
self._relative_offset = None
|
||||
self._alignment_offset = None
|
||||
self._scroll_offset = Vec2D((0,0))
|
||||
self._absolute_pos = None # abs pos of element from relative info; cached in draw
|
||||
self._absolute_size = None # viewing size of element; set by parent
|
||||
self._tablecell_table = None # table that this cell belongs to
|
||||
self._tablecell_pos = None # overriding position if table-cell
|
||||
self._tablecell_size = None # overriding size if table-cell
|
||||
self._all_lines = None # all children elements broken up into lines (for horizontal alignment)
|
||||
self._blocks = None
|
||||
self._blocks_abs = None
|
||||
self._children_text_min_size = None
|
||||
|
||||
# TODO: REPLACE WITH BETTER PROPERTIES AND DELETE!!
|
||||
self._preferred_width, self._preferred_height = 0,0
|
||||
self._content_width, self._content_height = 0,0
|
||||
|
||||
self._viewing_box = Box2D(topleft=(0,0), size=(-1,-1)) # topleft+size: set by parent element
|
||||
self._inside_box = Box2D(topleft=(0,0), size=(-1,-1)) # inside area of viewing box (less margins, paddings, borders)
|
||||
self._content_box = Box2D(topleft=(0,0), size=(-1,-1)) # topleft: set by scrollLeft, scrollTop properties
|
||||
# size: determined from children and style
|
||||
|
||||
# various sizes and boxes (set in self._position), used for layout and drawing
|
||||
self._preferred_size = Size2D() # computed preferred size, set in self._layout, used as suggestion to parent
|
||||
self._pref_content_size = Size2D() # size of content
|
||||
self._pref_full_size = Size2D() # _pref_content_size + margins + border + padding
|
||||
self._box_draw = Box2D(topleft=(0,0), size=(-1,-1)) # where UI will be drawn (restricted by parent)
|
||||
self._box_full = Box2D(topleft=(0,0), size=(-1,-1)) # where UI would draw if not restricted (offset for scrolling)
|
||||
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
info = [
|
||||
'id', 'classes', 'type', 'pseudoelement',
|
||||
# 'innerText', 'innerTextAsIs',
|
||||
'href',
|
||||
'value', 'title',
|
||||
]
|
||||
info = [(k, getattr(self, k)) for k in info if hasattr(self, k)]
|
||||
info = [f'{k}="{v}"' for k,v in info if v]
|
||||
if self.open: info += ['open']
|
||||
if self.is_dirty: info += ['dirty']
|
||||
if self._atomic: info += ['atomic']
|
||||
return '<%s>' % ' '.join([self.tagName] + info)
|
||||
|
||||
@UI_Core_Utils.add_cleaning_callback('content', {'blocks', 'renderbuf', 'style'})
|
||||
# @profiler.function
|
||||
def _compute_content(self):
|
||||
if self.defer_clean:
|
||||
# print('_compute_content: cleaning deferred!')
|
||||
return
|
||||
if not self.is_visible:
|
||||
self._dirty_properties.discard('content')
|
||||
# self._innerTextWrapped = None
|
||||
# self._innerTextAsIs = None
|
||||
return
|
||||
if 'content' not in self._dirty_properties:
|
||||
for e in list(self._dirty_callbacks.get('content', [])): e._compute_content()
|
||||
self._dirty_callbacks['content'].clear()
|
||||
return
|
||||
|
||||
self._clean_debugging['content'] = time.time()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} content')
|
||||
|
||||
# self.defer_dirty_propagation = True
|
||||
self._children_gen = []
|
||||
|
||||
content_before = self._computed_styles_before.get('content', None) if self._computed_styles_before else None
|
||||
if content_before is not None:
|
||||
# TODO: cache this!!
|
||||
self._child_before = self.new_element(tagName=self._tagName, innerText=content_before, pseudoelement='before', _parent=self)
|
||||
self._child_before.clean()
|
||||
self._new_content = True
|
||||
self._children_gen += [self._child_before]
|
||||
else:
|
||||
if self._child_before:
|
||||
self._child_before = None
|
||||
self._new_content = True
|
||||
|
||||
content_after = self._computed_styles_after.get('content', None) if self._computed_styles_after else None
|
||||
if content_after is not None:
|
||||
# TODO: cache this!!
|
||||
self._child_after = self.new_element(tagName=self._tagName, innerText=content_after, pseudoelement='after', _parent=self)
|
||||
self._child_after.clean()
|
||||
self._new_content = True
|
||||
self._children_gen += [self._child_after]
|
||||
else:
|
||||
if self._child_after:
|
||||
self._child_after = None
|
||||
self._new_content = True
|
||||
|
||||
if self._src and not self.src:
|
||||
self._src = None
|
||||
self._new_content = True
|
||||
|
||||
if self._computed_styles.get('content', None) is not None:
|
||||
self.innerText = self._computed_styles['content']
|
||||
|
||||
if self._innerText is not None:
|
||||
# TODO: cache this!!
|
||||
textwrap_opts = {
|
||||
'dpi': Globals.drawing.get_dpi_mult(),
|
||||
'text': self._innerText,
|
||||
'fontid': self._fontid,
|
||||
'fontsize': self._fontsize,
|
||||
'preserve_newlines': self._whitespace in {'pre', 'pre-line', 'pre-wrap'},
|
||||
'collapse_spaces': self._whitespace in {'normal', 'nowrap', 'pre-line'},
|
||||
'wrap_text': self._whitespace in {'normal', 'pre-wrap', 'pre-line'},
|
||||
}
|
||||
|
||||
# TODO: if whitespace:pre, then make self NOT wrap
|
||||
innerTextWrapped = helper_wraptext(**textwrap_opts)
|
||||
# print('"%s"' % innerTextWrapped)
|
||||
# print(self, id(self), self._innerTextWrapped, innerTextWrapped)
|
||||
rewrap = False
|
||||
rewrap |= self._innerTextWrapped != innerTextWrapped
|
||||
rewrap |= any(textwrap_opts[k] != self._textwrap_opts.get(k,None) for k in textwrap_opts.keys())
|
||||
if rewrap:
|
||||
# print(f'compute content: "{self._innerTextWrapped}" "{innerTextWrapped}"')
|
||||
self._textwrap_opts = textwrap_opts
|
||||
self._innerTextWrapped = innerTextWrapped
|
||||
self._children_text = []
|
||||
self._text_map = []
|
||||
idx = 0
|
||||
for l in self._innerTextWrapped.splitlines():
|
||||
if self._children_text:
|
||||
ui_br = self._generate_new_ui_elem(tagName='br', text_child=True)
|
||||
self._text_map.append({
|
||||
'ui_element': ui_br,
|
||||
'idx': idx,
|
||||
'offset': 0,
|
||||
'char': '\n',
|
||||
'pre': '',
|
||||
})
|
||||
idx += 1
|
||||
if self._whitespace in {'pre', 'nowrap'}:
|
||||
words = [l]
|
||||
else:
|
||||
words = re.split(r'([^ \n]* +)', l)
|
||||
for word in words:
|
||||
if not word: continue
|
||||
for f,t in html_to_unicode.no_arrows.items(): word = word.replace(f, t)
|
||||
ui_word = self._generate_new_ui_elem(innerTextAsIs=word, text_child=True)
|
||||
#tagName=self._tagName, pseudoelement='text',
|
||||
for i in range(len(word)):
|
||||
self._text_map.append({
|
||||
'ui_element': ui_word,
|
||||
'idx': idx,
|
||||
'offset': i,
|
||||
'char': word[i],
|
||||
'pre': word[:i],
|
||||
})
|
||||
idx += len(word)
|
||||
# needed so cursor can reach end
|
||||
ui_end = self._generate_new_ui_elem(innerTextAsIs='', text_child=True)
|
||||
#tagName=self._tagName, pseudoelement='text',
|
||||
self._text_map.append({
|
||||
'ui_element': ui_end,
|
||||
'idx': idx,
|
||||
'offset': 0,
|
||||
'char': '',
|
||||
'pre': '',
|
||||
})
|
||||
self._children_text_min_size = Size2D(width=0, height=0)
|
||||
if True: # with profiler.code('cleaning text children'):
|
||||
for child in self._children_text: child.clean()
|
||||
if any(child._static_content_size is None for child in self._children_text):
|
||||
# temporarily set
|
||||
self._children_text_min_size.width = 0
|
||||
self._children_text_min_size.height = 0
|
||||
else:
|
||||
self._children_text_min_size.width = max(child._static_content_size.width for child in self._children_text)
|
||||
self._children_text_min_size.height = max(child._static_content_size.height for child in self._children_text)
|
||||
self._new_content = True
|
||||
|
||||
elif self.src: # and not self._src:
|
||||
if ui_settings.ASYNC_IMAGE_LOADING and not self._pseudoelement and not is_image_cached(self.src):
|
||||
# print(f'LOADING {self.src} ASYNC')
|
||||
if self._src == 'image':
|
||||
self._new_content = True
|
||||
elif self._src == 'image loading':
|
||||
pass
|
||||
elif self._src == 'image loaded':
|
||||
self._src = 'image'
|
||||
self._image_data = load_texture(
|
||||
self.src,
|
||||
image=self._image_data,
|
||||
)
|
||||
self._new_content = True
|
||||
self.dirty_styling()
|
||||
self.dirty_flow()
|
||||
self.dirty(parent=True, children=True)
|
||||
else:
|
||||
self._src = 'image loading'
|
||||
self._image_data = load_texture(f'image loading {self.src}', image=get_loading_image(self.src))
|
||||
self._new_content = True
|
||||
def callback(image):
|
||||
self._src = 'image loaded'
|
||||
self._image_data = image
|
||||
self._new_content = True
|
||||
self.dirty_styling()
|
||||
self.dirty_flow()
|
||||
self.dirty(parent=True, children=True)
|
||||
def load():
|
||||
async_load_image(self.src, callback)
|
||||
ThreadPoolExecutor().submit(load)
|
||||
else:
|
||||
self._image_data = load_texture(self.src)
|
||||
self._src = 'image'
|
||||
|
||||
self._children_text = []
|
||||
self._children_text_min_size = None
|
||||
self._innerTextWrapped = None
|
||||
self._new_content = True
|
||||
|
||||
else:
|
||||
if self._children_text:
|
||||
self._new_content = True
|
||||
self._children_text = []
|
||||
self._children_text_min_size = None
|
||||
self._innerTextWrapped = None
|
||||
|
||||
# collect all children into self._children_all
|
||||
# TODO: cache this!!
|
||||
# TODO: some children are "detached" from self (act as if child.parent==root or as if floating)
|
||||
self._children_all = []
|
||||
if self._child_before: self._children_all.append(self._child_before)
|
||||
self._children_all += self._process_children()
|
||||
if self._children_text: self._children_all += self._children_text
|
||||
if self._child_after: self._children_all.append(self._child_after)
|
||||
|
||||
self._children_all = [child for child in self._children_all if child.is_visible]
|
||||
|
||||
for child in self._children_all: child._compute_content()
|
||||
|
||||
# sort children by z-index
|
||||
self._children_all_sorted = sorted(self._children_all, key=lambda e:e.z_index)
|
||||
|
||||
# content changes might have changed size
|
||||
if self._new_content:
|
||||
self.dirty_blocks(cause='content changes might have affected blocks')
|
||||
self.dirty_renderbuf(cause='content changes might have affected blocks')
|
||||
self.dirty_flow()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' possible new content')
|
||||
self._new_content = False
|
||||
self._dirty_properties.discard('content')
|
||||
self._dirty_callbacks['content'].clear()
|
||||
|
||||
# self.defer_dirty_propagation = False
|
||||
|
||||
@UI_Core_Utils.add_cleaning_callback('blocks', {'size', 'renderbuf'})
|
||||
# @profiler.function
|
||||
def _compute_blocks(self):
|
||||
'''
|
||||
split up all children into layout blocks
|
||||
|
||||
IMPORTANT: as current written, this function needs to be able to be run multiple times!
|
||||
DO NOT PREVENT THIS, otherwise infinite loop bugs will occur!
|
||||
'''
|
||||
|
||||
if self.defer_clean:
|
||||
return
|
||||
if not self.is_visible:
|
||||
self._dirty_properties.discard('blocks')
|
||||
return
|
||||
if 'blocks' not in self._dirty_properties:
|
||||
for e in list(self._dirty_callbacks.get('blocks', [])): e._compute_blocks()
|
||||
self._dirty_callbacks['blocks'].clear()
|
||||
return
|
||||
|
||||
self._clean_debugging['blocks'] = time.time()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} blocks')
|
||||
|
||||
# self.defer_dirty_propagation = True
|
||||
|
||||
for child in self._children_all:
|
||||
child._compute_blocks()
|
||||
|
||||
blocks = self._blocks
|
||||
blocks_abs = self._blocks_abs
|
||||
if self._computed_styles.get('display', 'inline') == 'flexbox':
|
||||
# all children are treated as flex blocks, regardless of their display
|
||||
pass
|
||||
else:
|
||||
# collect children into blocks
|
||||
blocks = []
|
||||
blocks_abs = []
|
||||
blocked_inlines = False
|
||||
def process_child(child):
|
||||
nonlocal blocks, blocks_abs, blocked_inlines
|
||||
d = child._computed_styles.get('display', 'inline')
|
||||
p = child._computed_styles.get('position', 'static')
|
||||
if p == 'absolute':
|
||||
blocks_abs.append(child)
|
||||
# elif p == 'fixed':
|
||||
# blocks_abs.append(child) # need separate list for fixed elements?
|
||||
elif d in {'inline', 'table-cell'}:
|
||||
if not blocked_inlines:
|
||||
blocked_inlines = True
|
||||
blocks.append([child])
|
||||
else:
|
||||
blocks[-1].append(child)
|
||||
else:
|
||||
blocked_inlines = False
|
||||
blocks.append([child])
|
||||
# if any(child._tagName == 'text' for child in self._children_all):
|
||||
# n_children_all = []
|
||||
# for child in self._children_all:
|
||||
# if child._tagName != 'text':
|
||||
# n_children_all.append(child)
|
||||
# else:
|
||||
# print(f'moving children of {child} ({child._children_all} / {child._children_text}) to {self}')
|
||||
# n_children_all += child._children_all
|
||||
# self._children_all = n_children_all
|
||||
for child in self._children_all:
|
||||
process_child(child)
|
||||
|
||||
def same(ll0, ll1):
|
||||
if ll0 == None or ll1 == None: return ll0 == ll1
|
||||
if len(ll0) != len(ll1): return False
|
||||
for (l0, l1) in zip(ll0, ll1):
|
||||
if len(l0) != len(l1): return False
|
||||
if any(i0 != i1 for (i0, i1) in zip(l0, l1)): return False
|
||||
return True
|
||||
|
||||
if not same(blocks, self._blocks) or not same([blocks_abs], [self._blocks_abs]):
|
||||
# content changes might have changed size
|
||||
self._blocks = blocks
|
||||
self._blocks_abs = blocks_abs
|
||||
self.dirty_size(cause='block changes might have changed size')
|
||||
self.dirty_renderbuf(cause='block changes might have changed size')
|
||||
self.dirty_flow()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' reflowing')
|
||||
|
||||
self._dirty_properties.discard('blocks')
|
||||
self._dirty_callbacks['blocks'].clear()
|
||||
|
||||
# self.defer_dirty_propagation = False
|
||||
|
||||
################################################################################################
|
||||
# NOTE: COMPUTE STATIC CONTENT SIZE (TEXT, IMAGE, ETC.), NOT INCLUDING MARGIN, BORDER, PADDING
|
||||
# WE MIGHT NOT NEED TO COMPUTE MIN AND MAX??
|
||||
@UI_Core_Utils.add_cleaning_callback('size', {'renderbuf'})
|
||||
# @profiler.function
|
||||
def _compute_static_content_size(self):
|
||||
if self.defer_clean:
|
||||
return
|
||||
if not self.is_visible:
|
||||
self._dirty_properties.discard('size')
|
||||
return
|
||||
if 'size' not in self._dirty_properties:
|
||||
for e in set(self._dirty_callbacks.get('size', [])):
|
||||
e._compute_static_content_size()
|
||||
self._dirty_callbacks['size'].remove(e)
|
||||
#self._dirty_callbacks['size'].clear()
|
||||
return
|
||||
|
||||
# if self.record_multicall('_compute_static_content_size'): return
|
||||
|
||||
self._clean_debugging['size'] = time.time()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} static content size')
|
||||
|
||||
# self.defer_dirty_propagation = True
|
||||
|
||||
if True: # with profiler.code('recursing to children'):
|
||||
for child in self._children_all:
|
||||
child._compute_static_content_size()
|
||||
|
||||
static_content_size = self._static_content_size
|
||||
|
||||
# set size based on content (computed size)
|
||||
if self._innerTextAsIs is not None:
|
||||
if True: # with profiler.code('computing text sizes'):
|
||||
# TODO: allow word breaking?
|
||||
# size_prev = Globals.drawing.set_font_size(self._textwrap_opts['fontsize'], fontid=self._textwrap_opts['fontid'], force=True)
|
||||
size_prev = Globals.drawing.set_font_size(self._parent._fontsize, fontid=self._parent._fontid) #, force=True)
|
||||
ts = self._parent._textshadow
|
||||
if ts is None: tsx,tsy = 0,0
|
||||
else: tsx,tsy,tsc = ts
|
||||
|
||||
# subtract 1/4 width of space to make text look a little nicer
|
||||
subw = (Globals.drawing.get_text_width(' ') * 0.25) if self._innerTextAsIs and self._innerTextAsIs[-1] == ' ' else 0
|
||||
|
||||
static_content_size = Size2D()
|
||||
static_content_size.set_all_widths(ceil(Globals.drawing.get_text_width(self._innerTextAsIs) - subw) + abs(tsx))
|
||||
static_content_size.set_all_heights(ceil(Globals.drawing.get_line_height(self._innerTextAsIs)) + abs(tsy))
|
||||
Globals.drawing.set_font_size(size_prev, fontid=self._parent._fontid) #, force=True)
|
||||
#print(f'"{self._innerTextAsIs}": {static_content_size.width} x {static_content_size.height}')
|
||||
|
||||
elif self._src in {'image', 'image loading'}:
|
||||
if True: # with profiler.code('computing image sizes'):
|
||||
# TODO: set to image size?
|
||||
dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
static_content_size = Size2D()
|
||||
try:
|
||||
w, h = float(self._image_data['width']), float(self._image_data['height'])
|
||||
static_content_size.set_all_widths(w * dpi_mult)
|
||||
static_content_size.set_all_heights(h * dpi_mult)
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
static_content_size = None
|
||||
|
||||
if static_content_size != self._static_content_size:
|
||||
self._static_content_size = static_content_size
|
||||
self.dirty_renderbuf(cause='static content changes might change render')
|
||||
self.dirty_flow()
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' reflowing')
|
||||
# self.defer_dirty_propagation = False
|
||||
self._dirty_properties.discard('size')
|
||||
self._dirty_callbacks['size'].clear()
|
||||
@@ -0,0 +1,50 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class UI_Core_Debug:
|
||||
def _init_debug(self):
|
||||
self._debug_list = []
|
||||
|
||||
def debug_print(self, d, already_printed):
|
||||
sp = ' '*d
|
||||
tag = self.as_html
|
||||
tagc = f'</{self._tagName}>'
|
||||
tagsc = f'{tag[:-1]} />'
|
||||
if self in already_printed:
|
||||
print(f'{sp}{tag}...{tagc}')
|
||||
return
|
||||
already_printed.add(self)
|
||||
if self._pseudoelement == 'text':
|
||||
innerText = self._innerText.replace('\n', '\\n') if self._innerText else ''
|
||||
print(f'{sp}"{innerText}"')
|
||||
elif self._children_all:
|
||||
print(f'{sp}{tag}')
|
||||
for c in self._children_all:
|
||||
c.debug_print(d+1, already_printed)
|
||||
print(f'{sp}{tagc}')
|
||||
else:
|
||||
print(f'{sp}{tagsc}')
|
||||
|
||||
def structure(self, depth=0, all_children=False):
|
||||
l = self._children if not all_children else self._children_all
|
||||
return '\n'.join([(' '*depth) + str(self)] + [child.structure(depth+1) for child in l])
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from .maths import Color, NumberUnit
|
||||
|
||||
class UI_Core_Defaults:
|
||||
font_family = 'sans-serif'
|
||||
font_style = 'normal'
|
||||
font_weight = 'normal'
|
||||
font_size = NumberUnit(12, 'pt')
|
||||
font_color = Color((0, 0, 0, 1))
|
||||
whitespace = 'normal'
|
||||
@@ -0,0 +1,317 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from .blender import tag_redraw_all
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
from . import ui_settings # needs to be first
|
||||
from .ui_core_utilities import UI_Core_Utils
|
||||
|
||||
|
||||
class UI_Core_Dirtiness:
|
||||
def _init_dirtiness(self):
|
||||
# dirty properties
|
||||
# used to inform parent and children to recompute
|
||||
self._dirty_properties = { # set of dirty properties, add through self.dirty to force propagation of dirtiness
|
||||
'style', # force recalculations of style
|
||||
'style parent', # force recalculations of style if parent selector changes
|
||||
'content', # content of self has changed
|
||||
'blocks', # children are grouped into blocks
|
||||
'size', # force recalculations of size
|
||||
'renderbuf', # force re-rendering buffer (if applicable)
|
||||
}
|
||||
self._new_content = True
|
||||
self._dirtying_flow = True
|
||||
self._dirtying_children_flow = True
|
||||
self._dirty_causes = []
|
||||
self._dirty_callbacks = { k:set() for k in UI_Core_Utils._cleaning_graph_nodes }
|
||||
self._dirty_propagation = { # contains deferred dirty propagation for parent and children; parent will be dirtied later
|
||||
'defer': False, # set to True to defer dirty propagation (useful when many changes are occurring)
|
||||
'parent': set(), # set of properties to dirty for parent
|
||||
'parent callback': set(), # set of dirty properties to inform parent
|
||||
'children': set(), # set of properties to dirty for children
|
||||
}
|
||||
self._defer_clean = False # set to True to defer cleaning (useful when many changes are occurring)
|
||||
self._clean_debugging = {}
|
||||
self._do_not_dirty_parent = False # special situation where self._parent attrib was set specifically in __init__ (ex: UI_Elements from innerText)
|
||||
self._draw_dirty_style = 0 # keeping track of times style is dirtied since last draw
|
||||
|
||||
# @profiler.function
|
||||
def dirty(self, **kwargs):
|
||||
self._dirty(**kwargs)
|
||||
# @profiler.function
|
||||
def dirty_selector(self, **kwargs):
|
||||
self._dirty(properties={'selector'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_style_parent(self, **kwargs):
|
||||
self._dirty(properties={'style parent'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_style(self, **kwargs):
|
||||
self._dirty(properties={'style'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_content(self, **kwargs):
|
||||
self._dirty(properties={'content'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_blocks(self, **kwargs):
|
||||
self._dirty(properties={'blocks'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_size(self, **kwargs):
|
||||
self._dirty(properties={'size'}, **kwargs)
|
||||
# @profiler.function
|
||||
def dirty_renderbuf(self, **kwargs):
|
||||
self._dirty(properties={'renderbuf'}, **kwargs)
|
||||
|
||||
def _dirty(self, *, cause=None, properties=None, parent=False, children=False, propagate_up=True):
|
||||
# assert cause
|
||||
if cause is None: cause = 'Unspecified cause'
|
||||
if properties is None: properties = set(UI_Core_Utils._cleaning_graph_nodes)
|
||||
elif type(properties) is str: properties = {properties}
|
||||
elif type(properties) is list: properties = set(properties)
|
||||
properties -= self._dirty_properties # ignore dirtying properties that are already dirty
|
||||
if not properties: return # no new dirtiness
|
||||
# if getattr(self, '_cleaning', False): print(f'{self} was dirtied ({properties}) while cleaning')
|
||||
self._dirty_properties |= properties
|
||||
if ui_settings.DEBUG_DIRTY: self._dirty_causes.append(cause)
|
||||
if self._do_not_dirty_parent: parent = False
|
||||
if parent: self._dirty_propagation['parent'] |= properties # dirty parent also (ex: size of self changes, so parent needs to layout)
|
||||
else: self._dirty_propagation['parent callback'] |= properties # let parent know self is dirty (ex: background color changes, so we need to update style of self but not parent)
|
||||
if children: self._dirty_propagation['children'] |= properties # dirty all children also (ex: :hover pseudoclass added, so children might be affected)
|
||||
|
||||
# any dirtiness _ALWAYS_ dirties renderbuf of self and parent
|
||||
self._dirty_properties.add('renderbuf')
|
||||
self._dirty_propagation['parent'].add('renderbuf')
|
||||
|
||||
if propagate_up: self.propagate_dirtiness_up()
|
||||
self.dirty_flow(children=False)
|
||||
# print(f'{self} had {properties} dirtied, because {cause}')
|
||||
tag_redraw_all("UI_Element dirty")
|
||||
|
||||
def add_dirty_callback(self, child, properties):
|
||||
if type(properties) is str: properties = [properties]
|
||||
if not properties: return
|
||||
propagate_props = {
|
||||
p for p in properties
|
||||
if p not in self._dirty_properties
|
||||
and child not in self._dirty_callbacks[p]
|
||||
}
|
||||
if not propagate_props: return
|
||||
for p in propagate_props: self._dirty_callbacks[p].add(child)
|
||||
self.add_dirty_callback_to_parent(propagate_props)
|
||||
|
||||
def add_dirty_callback_to_parent(self, properties):
|
||||
if not self._parent: return
|
||||
if self._do_not_dirty_parent: return
|
||||
if not properties: return
|
||||
self._parent.add_dirty_callback(self, properties)
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def dirty_styling(self):
|
||||
'''
|
||||
NOTE: this function clears style cache for self and all descendants
|
||||
'''
|
||||
self._computed_styles = {}
|
||||
self._styling_parent = None
|
||||
# self._styling_custom = None
|
||||
self._style_content_hash = None
|
||||
self._style_size_hash = None
|
||||
for child in self._children_all: child.dirty_styling()
|
||||
self.dirty_style(cause='Dirtying style cache')
|
||||
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def dirty_flow(self, parent=True, children=True):
|
||||
if self._dirtying_flow and self._dirtying_children_flow: return
|
||||
if not self._dirtying_flow:
|
||||
if parent and self._parent and not self._do_not_dirty_parent:
|
||||
self._parent.dirty_flow(children=False)
|
||||
self._dirtying_flow = True
|
||||
self._dirtying_children_flow |= self._computed_styles.get('display', 'block') == 'table'
|
||||
tag_redraw_all("UI_Element dirty_flow")
|
||||
|
||||
@property
|
||||
def is_dirty(self):
|
||||
return any_args(
|
||||
self._dirty_properties,
|
||||
self._dirty_propagation['parent'],
|
||||
self._dirty_propagation['parent callback'],
|
||||
self._dirty_propagation['children'],
|
||||
)
|
||||
|
||||
# @profiler.function
|
||||
def propagate_dirtiness_up(self):
|
||||
if self._dirty_propagation['defer']: return
|
||||
|
||||
if self._dirty_propagation['parent']:
|
||||
if self._parent and not self._do_not_dirty_parent:
|
||||
cause = ''
|
||||
if ui_settings.DEBUG_DIRTY:
|
||||
cause = ' -> '.join(f'{cause}' for cause in (self._dirty_causes+[
|
||||
f"\"propagating dirtiness ({self._dirty_propagation['parent']} from {self} to parent {self._parent}\""
|
||||
]))
|
||||
self._parent.dirty(
|
||||
cause=cause,
|
||||
properties=self._dirty_propagation['parent'],
|
||||
parent=True,
|
||||
children=False,
|
||||
)
|
||||
self._dirty_propagation['parent'].clear()
|
||||
|
||||
if not self._do_not_dirty_parent:
|
||||
self.add_dirty_callback_to_parent(self._dirty_propagation['parent callback'])
|
||||
self._dirty_propagation['parent callback'].clear()
|
||||
|
||||
self._dirty_causes = []
|
||||
|
||||
# @profiler.function
|
||||
def propagate_dirtiness_down(self):
|
||||
if self._dirty_propagation['defer']: return
|
||||
|
||||
if not self._dirty_propagation['children']: return
|
||||
|
||||
# no need to dirty ::before, ::after, or text, because they will be reconstructed
|
||||
for child in self._children:
|
||||
child.dirty(
|
||||
cause=f'propagating {self._dirty_propagation["children"]}',
|
||||
properties=self._dirty_propagation['children'],
|
||||
parent=False,
|
||||
children=True,
|
||||
)
|
||||
for child in self._children_gen:
|
||||
child.dirty(
|
||||
cause=f'propagating {self._dirty_propagation["children"]}',
|
||||
properties=self._dirty_propagation['children'],
|
||||
parent=False,
|
||||
children=True
|
||||
)
|
||||
self._dirty_propagation['children'].clear()
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def defer_dirty_propagation(self):
|
||||
return self._dirty_propagation['defer']
|
||||
@defer_dirty_propagation.setter
|
||||
def defer_dirty_propagation(self, v):
|
||||
self._dirty_propagation['defer'] = bool(v)
|
||||
self.propagate_dirtiness_up()
|
||||
|
||||
def _call_preclean(self):
|
||||
if not self.is_dirty: return
|
||||
if not self._preclean: return
|
||||
self._preclean()
|
||||
def _call_postclean(self):
|
||||
if not self._was_dirty: return
|
||||
self._was_dirty = False
|
||||
if not self._postclean: return
|
||||
self._postclean()
|
||||
def _call_postflow(self):
|
||||
if not self._postflow: return
|
||||
if not self.is_visible: return
|
||||
self._postflow()
|
||||
|
||||
@property
|
||||
def defer_clean(self):
|
||||
if not self._document: return True
|
||||
if self._document.defer_cleaning: return True
|
||||
if self._defer_clean: return True
|
||||
# if not self.is_dirty: return True
|
||||
return False
|
||||
@defer_clean.setter
|
||||
def defer_clean(self, value):
|
||||
self._defer_clean = value
|
||||
|
||||
# @profiler.function
|
||||
def clean(self, depth=0):
|
||||
'''
|
||||
No need to clean if
|
||||
- already clean,
|
||||
- possibly more dirtiness to propagate,
|
||||
- if deferring cleaning.
|
||||
'''
|
||||
|
||||
if self._dirty_propagation['defer']: return
|
||||
if self.defer_clean: return
|
||||
if not self.is_dirty: return
|
||||
|
||||
self._was_dirty = True # used to know if postclean should get called
|
||||
|
||||
self._cleaning = True
|
||||
|
||||
# profiler.add_note(f'pre: {self._dirty_properties}, {self._dirty_causes} {self._dirty_propagation}')
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} clean started defer={self.defer_clean}')
|
||||
|
||||
# propagate dirtiness one level down
|
||||
self.propagate_dirtiness_down()
|
||||
|
||||
# self.call_cleaning_callbacks()
|
||||
self._compute_selector()
|
||||
self._compute_style()
|
||||
if self.is_visible:
|
||||
self._compute_content()
|
||||
self._compute_blocks()
|
||||
self._compute_static_content_size()
|
||||
self._renderbuf()
|
||||
|
||||
# profiler.add_note(f'mid: {self._dirty_properties}, {self._dirty_causes} {self._dirty_propagation}')
|
||||
|
||||
for child in self._children_all:
|
||||
child.clean(depth=depth+1)
|
||||
|
||||
# profiler.add_note(f'post: {self._dirty_properties}, {self._dirty_causes} {self._dirty_propagation}')
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} clean done')
|
||||
|
||||
# self._debug_list.clear()
|
||||
|
||||
self._cleaning = False
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def call_cleaning_callbacks(self):
|
||||
g = UI_Core_Utils._cleaning_graph
|
||||
working = set(UI_Core_Utils._cleaning_graph_roots)
|
||||
done = set()
|
||||
restarts = []
|
||||
while working:
|
||||
current = working.pop()
|
||||
curnode = g[current]
|
||||
assert current not in done, f'cycle detected in cleaning callbacks ({current})'
|
||||
if not all(p in done for p in curnode['parents']): continue
|
||||
do_cleaning = False
|
||||
do_cleaning |= current in self._dirty_properties
|
||||
do_cleaning |= bool(self._dirty_callbacks.get(current, False))
|
||||
if do_cleaning:
|
||||
curnode['fn'](self)
|
||||
redirtied = [d for d in self._dirty_properties if d in done]
|
||||
if redirtied:
|
||||
# print('UI_Core.call_cleaning_callbacks:', self, current, 'dirtied', redirtied)
|
||||
if len(restarts) < 50:
|
||||
# profiler.add_note('restarting')
|
||||
working = set(UI_Core_Utils._cleaning_graph_roots)
|
||||
done = set()
|
||||
restarts.append((curnode, self._dirty_properties))
|
||||
else:
|
||||
return
|
||||
else:
|
||||
working.update(curnode['children'])
|
||||
done.add(current)
|
||||
@@ -0,0 +1,236 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
import gpu
|
||||
from gpu_extras.presets import draw_texture_2d
|
||||
|
||||
from . import ui_settings # needs to be first
|
||||
from .ui_draw import ui_draw
|
||||
|
||||
from . import gpustate
|
||||
from .globals import Globals
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .profiler import profiler, time_it
|
||||
|
||||
|
||||
class UI_Core_Draw:
|
||||
|
||||
def _draw_real(self, offset, scissor_include_margin=True, scissor_include_padding=True):
|
||||
dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
ox,oy = offset
|
||||
|
||||
if ui_settings.DEBUG_COLOR_CLEAN:
|
||||
if ui_settings.DEBUG_COLOR == 0:
|
||||
t_max = 2
|
||||
t = max(0, t_max - (time.time() - self._clean_debugging.get(ui_settings.DEBUG_PROPERTY, 0))) / t_max
|
||||
background_override = Color( ( t, t/2, 0, 0.75 ) )
|
||||
elif ui_settings.DEBUG_COLOR == 1:
|
||||
t = self._clean_debugging.get(ui_settings.DEBUG_PROPERTY, 0)
|
||||
d = time.time() - t
|
||||
h = (t / 2) % 1
|
||||
s = 1.0
|
||||
l = max(0, 0.5 - d / 10)
|
||||
background_override = Color.HSL((h, s, l, 0.75))
|
||||
else:
|
||||
background_override = None
|
||||
|
||||
gpustate.blend('ALPHA_PREMULT', only='enable')
|
||||
|
||||
sc = self._style_cache
|
||||
margin_top, margin_right, margin_bottom, margin_left = sc['margin-top'], sc['margin-right'], sc['margin-bottom'], sc['margin-left']
|
||||
padding_top, padding_right, padding_bottom, padding_left = sc['padding-top'], sc['padding-right'], sc['padding-bottom'], sc['padding-left']
|
||||
border_width = sc['border-width']
|
||||
|
||||
ol, ot = int(self._l + ox), int(self._t + oy)
|
||||
|
||||
if True: # with profiler.code('drawing mbp'):
|
||||
texture_id = self._image_data['texid'] if self._src in {'image', 'image loading'} else None
|
||||
gputexture = self._image_data['gputexture'] if self._src in {'image', 'image loading'} else None
|
||||
texture_fit = self._computed_styles.get('object-fit', 'fill')
|
||||
ui_draw.draw(ol, ot, self._w, self._h, dpi_mult, self._style_cache, texture_id, gputexture, texture_fit, background_override=background_override, depth=len(self._selector))
|
||||
|
||||
if True: # with profiler.code('drawing children'):
|
||||
# compute inner scissor area
|
||||
mt,mr,mb,ml = (margin_top, margin_right, margin_bottom, margin_left) if scissor_include_margin else (0,0,0,0)
|
||||
pt,pr,pb,pl = (padding_top,padding_right,padding_bottom,padding_left) if scissor_include_padding else (0,0,0,0)
|
||||
bw = border_width
|
||||
il = round(self._l + (ml + bw + pl) + ox)
|
||||
it = round(self._t - (mt + bw + pt) + oy)
|
||||
iw = round(self._w - ((ml + bw + pl) + (pr + bw + mr)))
|
||||
ih = round(self._h - ((mt + bw + pt) + (pb + bw + mb)))
|
||||
noclip = self._computed_styles.get('overflow-x', 'visible') == 'visible' and self._computed_styles.get('overflow-y', 'visible') == 'visible'
|
||||
|
||||
with gpustate.ScissorStack.wrap(il, it, iw, ih, msg=f'{self} mbp', disabled=noclip):
|
||||
if self._innerText is not None:
|
||||
size_prev = Globals.drawing.set_font_size(self._fontsize, fontid=self._fontid)
|
||||
if self._textshadow is not None:
|
||||
tsx,tsy,tsc = self._textshadow
|
||||
offset2 = (int(ox + tsx), int(oy - tsy))
|
||||
Globals.drawing.set_font_color(self._fontid, tsc)
|
||||
for child in self._children_all_sorted:
|
||||
child._draw(offset2)
|
||||
Globals.drawing.set_font_color(self._fontid, self._fontcolor)
|
||||
for child in self._children_all_sorted:
|
||||
child._draw(offset)
|
||||
Globals.drawing.set_font_size(size_prev, fontid=self._fontid)
|
||||
elif self._innerTextAsIs is not None:
|
||||
Globals.drawing.text_draw2D_simple(self._innerTextAsIs, (ol, ot))
|
||||
else:
|
||||
for child in self._children_all_sorted:
|
||||
gpustate.blend('ALPHA_PREMULT', only='enable')
|
||||
child._draw(offset)
|
||||
|
||||
default_draw_cache_style = {
|
||||
'background-color': (0,0,0,0),
|
||||
'margin-top': 0,
|
||||
'margin-right': 0,
|
||||
'margin-bottom': 0,
|
||||
'margin-left': 0,
|
||||
'padding-top': 0,
|
||||
'padding-right': 0,
|
||||
'padding-bottom': 0,
|
||||
'padding-left': 0,
|
||||
'border-width': 0,
|
||||
}
|
||||
def _draw_cache(self, offset):
|
||||
ox,oy = offset
|
||||
with gpustate.ScissorStack.wrap(self._l+ox, self._t+oy, self._w, self._h):
|
||||
if self._cacheRenderBuf:
|
||||
gpustate.blend('ALPHA_PREMULT')
|
||||
texture_id = self._cacheRenderBuf.color_texture
|
||||
if True:
|
||||
draw_texture_2d(texture_id, (self._l+ox, self._b+oy), self._w, self._h)
|
||||
else:
|
||||
ui_draw.draw(
|
||||
self._l+ox, self._t+oy, self._w, self._h,
|
||||
Globals.drawing.get_dpi_mult(),
|
||||
self.default_draw_cache_style,
|
||||
texture_id, 0,
|
||||
background_override=None,
|
||||
)
|
||||
else:
|
||||
gpustate.blend('ALPHA_PREMULT', only='function')
|
||||
self._draw_real(offset)
|
||||
|
||||
def _cache_create(self):
|
||||
if self._w < 1 or self._h < 1: return
|
||||
# (re-)create off-screen buffer
|
||||
if self._cacheRenderBuf:
|
||||
# already have a render buffer, so just resize it
|
||||
self._cacheRenderBuf.resize(self._w, self._h)
|
||||
else:
|
||||
# do not already have a render buffer, so create one
|
||||
self._cacheRenderBuf = gpustate.FrameBuffer(self._w, self._h)
|
||||
|
||||
def _cache_hierarchical(self, depth):
|
||||
if self._innerTextAsIs is not None: return # do not cache this low level!
|
||||
if self._innerText is not None: return
|
||||
|
||||
# make sure children are all cached (if applicable)
|
||||
for child in self._children_all_sorted:
|
||||
child._cache(depth=depth+1)
|
||||
|
||||
self._cache_create()
|
||||
|
||||
sl, st, sw, sh = 0, self._h - 1, self._w, self._h
|
||||
with self._cacheRenderBuf.bind():
|
||||
self._draw_real((-self._l, -self._b))
|
||||
# with gpustate.ScissorStack.wrap(sl, st, sw, sh, clamp=False):
|
||||
# self._draw_real((-self._l, -self._b))
|
||||
|
||||
def _cache_textleaves(self, depth):
|
||||
for child in self._children_all_sorted:
|
||||
child._cache(depth=depth+1)
|
||||
if depth == 0:
|
||||
self._cache_onlyroot(depth)
|
||||
return
|
||||
if self._innerText is None:
|
||||
return
|
||||
self._cache_create()
|
||||
sl, st, sw, sh = 0, self._h - 1, self._w, self._h
|
||||
with self._cacheRenderBuf.bind():
|
||||
self._draw_real((-self._l, -self._b))
|
||||
# with gpustate.ScissorStack.wrap(sl, st, sw, sh, clamp=False):
|
||||
# self._draw_real((-self._l, -self._b))
|
||||
|
||||
def _cache_onlyroot(self, depth):
|
||||
self._cache_create()
|
||||
with self._cacheRenderBuf.bind():
|
||||
self._draw_real((0,0))
|
||||
|
||||
# @profiler.function
|
||||
def _cache(self, depth=0):
|
||||
if not self.is_visible: return
|
||||
if self._w <= 0 or self._h <= 0: return
|
||||
|
||||
if not self._dirty_renderbuf: return # no need to cache
|
||||
# print('caching %s' % str(self))
|
||||
|
||||
if ui_settings.CACHE_METHOD == 0: pass # do not cache
|
||||
elif ui_settings.CACHE_METHOD == 1: self._cache_onlyroot(depth)
|
||||
elif ui_settings.CACHE_METHOD == 2: self._cache_hierarchical(depth)
|
||||
elif ui_settings.CACHE_METHOD == 3: self._cache_textleaves(depth)
|
||||
|
||||
self._dirty_renderbuf = False
|
||||
|
||||
# @profiler.function
|
||||
def _draw(self, offset=(0,0)):
|
||||
if not self.is_visible: return
|
||||
if self._w <= 0 or self._h <= 0: return
|
||||
# if self._draw_dirty_style > 1: print(self, self._draw_dirty_style)
|
||||
ox,oy = offset
|
||||
if not gpustate.ScissorStack.is_box_visible(self._l+ox, self._t+oy, self._w, self._h): return
|
||||
# print('drawing %s' % str(self))
|
||||
self._draw_cache(offset)
|
||||
self._draw_dirty_style = 0
|
||||
|
||||
def draw(self):
|
||||
gpustate.blend('ALPHA_PREMULT', only='function')
|
||||
self._setup_ltwh()
|
||||
self._cache()
|
||||
self._draw()
|
||||
|
||||
def _draw_vscroll(self, depth=0):
|
||||
if not self.is_visible: return
|
||||
if not gpustate.ScissorStack.is_box_visible(self._l, self._t, self._w, self._h): return
|
||||
if self._w <= 0 or self._h <= 0: return
|
||||
vscroll = max(0, self._dynamic_full_size.height - self._h)
|
||||
if vscroll < 1: return
|
||||
with gpustate.ScissorStack.wrap(self._l, self._t, self._w, self._h, msg=str(self)):
|
||||
if True: # with profiler.code('drawing scrollbar'):
|
||||
gpustate.blend('ALPHA_PREMULT', only='enable')
|
||||
w = 3
|
||||
h = self._h - (mt+bw+pt) - (mb+bw+pb) - 6
|
||||
px = self._l + self._w - (mr+bw+pr) - w/2 - 5
|
||||
py0 = self._t - (mt+bw+pt) - 3
|
||||
py1 = py0 - (h-1)
|
||||
sh = h * self._h / self._dynamic_full_size.height
|
||||
sy0 = py0 - (h-sh) * (self._scroll_offset.y / vscroll)
|
||||
sy1 = sy0 - sh
|
||||
if py0>sy0: Globals.drawing.draw2D_line(Point2D((px,py0)), Point2D((px,sy0+1)), Color((0,0,0,0.2)), width=w)
|
||||
if sy1>py1: Globals.drawing.draw2D_line(Point2D((px,sy1-1)), Point2D((px,py1)), Color((0,0,0,0.2)), width=w)
|
||||
Globals.drawing.draw2D_line(Point2D((px,sy0)), Point2D((px,sy1)), Color((1,1,1,0.2)), width=w)
|
||||
if self._innerText is None:
|
||||
for child in self._children_all_sorted:
|
||||
child._draw_vscroll(depth+1)
|
||||
def draw_vscroll(self, *args, **kwargs): return self._draw_vscroll(*args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,948 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from itertools import dropwhile
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .boundvar import BoundVar, BoundInt, BoundFloat
|
||||
from .blender import tag_redraw_all
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .globals import Globals
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .profiler import profiler, time_it
|
||||
from .ui_core_utilities import helper_wraptext, convert_token_to_cursor
|
||||
from .utils import iter_head, any_args, join, delay_exec, Dict
|
||||
|
||||
|
||||
|
||||
def setup_scrub(ui_element, value):
|
||||
'''
|
||||
must be a BoundInt or BoundFloat with min_value and max_value set
|
||||
'''
|
||||
if not type(value) in {BoundInt, BoundFloat}: return
|
||||
if not value.is_bounded and not value.step_size: return
|
||||
|
||||
state = {}
|
||||
def reset_state():
|
||||
nonlocal state
|
||||
state = {
|
||||
'can scrub': True,
|
||||
'pressed': False,
|
||||
'scrubbing': False,
|
||||
'down': None,
|
||||
'initval': None,
|
||||
'cancelled': False,
|
||||
}
|
||||
reset_state()
|
||||
|
||||
def cancel():
|
||||
nonlocal state
|
||||
if not state['scrubbing']: return
|
||||
value.value = state['initval']
|
||||
state['cancelled'] = True
|
||||
|
||||
def mousedown(e):
|
||||
nonlocal state
|
||||
if not ui_element.document: return
|
||||
if ui_element.document.activeElement and ui_element.document.activeElement.is_descendant_of(ui_element):
|
||||
# do not scrub if descendant of ui_element has focus
|
||||
return
|
||||
if e.button[2] and state['scrubbing']:
|
||||
# right mouse button cancels
|
||||
value.value = state['initval']
|
||||
state['cancelled'] = True
|
||||
e.stop_propagation()
|
||||
elif e.button[0]:
|
||||
state['pressed'] = True
|
||||
state['down'] = e.mouse
|
||||
state['initval'] = value.value
|
||||
def mouseup(e):
|
||||
nonlocal state
|
||||
if e.button[0]: return
|
||||
if state['scrubbing']: e.stop_propagation()
|
||||
reset_state()
|
||||
def mousemove(e):
|
||||
nonlocal state
|
||||
if not state['pressed']: return
|
||||
if e.button[2]:
|
||||
cancel()
|
||||
e.stop_propagation()
|
||||
if state['cancelled']: return
|
||||
state['scrubbing'] |= (e.mouse - state['down']).length > Globals.drawing.scale(5)
|
||||
if not state['scrubbing']: return
|
||||
|
||||
if ui_element._document:
|
||||
ui_element._document.blur()
|
||||
|
||||
if value.is_bounded:
|
||||
m, M = value.min_value, value.max_value
|
||||
p = (e.mouse.x - state['down'].x) / ui_element.width_pixels
|
||||
v = clamp(state['initval'] + (M - m) * p, m, M)
|
||||
value.value = v
|
||||
else:
|
||||
delta = Globals.drawing.unscale(e.mouse.x - state['down'].x)
|
||||
value.value = state['initval'] + delta * value.step_size
|
||||
e.stop_propagation()
|
||||
def keypress(e):
|
||||
nonlocal state
|
||||
if not state['pressed']: return
|
||||
if state['cancelled']: return
|
||||
if e.key == 'ESC':
|
||||
cancel()
|
||||
e.stop_propagation()
|
||||
|
||||
ui_element.add_eventListener('on_mousemove', mousemove, useCapture=True)
|
||||
ui_element.add_eventListener('on_mousedown', mousedown, useCapture=True)
|
||||
ui_element.add_eventListener('on_mouseup', mouseup, useCapture=True)
|
||||
ui_element.add_eventListener('on_keypress', keypress, useCapture=True)
|
||||
|
||||
|
||||
# all html tags: https://www.w3schools.com/tags/
|
||||
|
||||
re_html_tag = re.compile(r"(?P<tag><(?P<close>/)?(?P<name>[a-zA-Z0-9\-_]+)(?P<attributes>( +(?P<key>[a-zA-Z0-9\-_]+)(?:=(?P<value>\"(?:[^\"]|\\\")*\"|[a-zA-Z0-9\-_]+|\'(?:[^']|\\\')*?\'))?)*) *(?P<selfclose>/)?>)")
|
||||
re_attributes = re.compile(r" *(?P<key>[a-zA-Z0-9\-_]+)(?:=(?P<value>\"(?:[^\"]|\\\")*?\"|[a-zA-Z0-9\-]+|\'(?:[^']|\\\')*?\'))?")
|
||||
re_html_comment = re.compile(r"<!--(.|\n|\r)*?-->")
|
||||
|
||||
re_self = re.compile(r"self\.")
|
||||
re_bound = re.compile(r"^(?P<type>Bound(String|StringToBool|Bool|Int|Float))\((?P<args>.*)\)$")
|
||||
re_int = re.compile(r"^[-+]?[0-9]+$")
|
||||
re_float = re.compile(r"^[-+]?[0-9]*\.?[0-9]+$")
|
||||
re_fstring = re.compile(r"{(?P<eval>([^}]|\\})*)}")
|
||||
|
||||
tags_selfclose = {
|
||||
'area', 'br', 'col',
|
||||
'embed', 'hr', 'iframe',
|
||||
'img', 'input', 'link',
|
||||
'meta', 'param', 'source',
|
||||
'track', 'wbr'
|
||||
}
|
||||
tags_known = {
|
||||
'article',
|
||||
'button',
|
||||
'span', 'div', 'p',
|
||||
'a',
|
||||
'b', 'i',
|
||||
'h1', 'h2', 'h3',
|
||||
'ul', 'ol', 'li',
|
||||
'pre', 'code',
|
||||
'br',
|
||||
'img',
|
||||
'progress',
|
||||
'table', 'tr', 'th', 'td',
|
||||
'dialog',
|
||||
'label', 'input',
|
||||
'details', 'summary',
|
||||
'script',
|
||||
'text',
|
||||
}
|
||||
events_known = {
|
||||
'focus': 'on_focus', 'onfocus': 'on_focus', 'on_focus': 'on_focus',
|
||||
'blur': 'on_blur', 'onblur': 'on_blur', 'on_blur': 'on_blur',
|
||||
'focusin': 'on_focusin', 'onfocusin': 'on_focusin', 'on_focusin': 'on_focusin',
|
||||
'focusout': 'on_focusout', 'onfocusout': 'on_focusout', 'on_focusout': 'on_focusout',
|
||||
'keydown': 'on_keydown', 'onkeydown': 'on_keydown', 'on_keydown': 'on_keydown',
|
||||
'keyup': 'on_keyup', 'onkeyup': 'on_keyup', 'on_keyup': 'on_keyup',
|
||||
'keypress': 'on_keypress', 'onkeypress': 'on_keypress', 'on_keypress': 'on_keypress',
|
||||
'mouseenter': 'on_mouseenter', 'onmouseenter': 'on_mouseenter', 'on_mouseenter': 'on_mouseenter',
|
||||
'mousemove': 'on_mousemove', 'onmousemove': 'on_mousemove', 'on_mousemove': 'on_mousemove',
|
||||
'mousedown': 'on_mousedown', 'onmousedown': 'on_mousedown', 'on_mousedown': 'on_mousedown',
|
||||
'mouseup': 'on_mouseup', 'onmouseup': 'on_mouseup', 'on_mouseup': 'on_mouseup',
|
||||
'mouseclick': 'on_mouseclick', 'onmouseclick': 'on_mouseclick', 'on_mouseclick': 'on_mouseclick',
|
||||
'mousedblclick':'on_mousedblclick', 'onmousedblclick': 'on_mousedblclick', 'on_mousedblclick': 'on_mousedblclick',
|
||||
'mouseleave': 'on_mouseleave', 'onmouseleave': 'on_mouseleave', 'on_mouseleave': 'on_mouseleave',
|
||||
'scroll': 'on_scroll', 'onscroll': 'on_scroll', 'on_scroll': 'on_scroll',
|
||||
'input': 'on_input', 'oninput': 'on_input', 'on_input': 'on_input',
|
||||
'change': 'on_change', 'onchange': 'on_change', 'on_change': 'on_change',
|
||||
'toggle': 'on_toggle', 'ontoggle': 'on_toggle', 'on_toggle': 'on_toggle',
|
||||
'visibilitychange': 'on_visibilitychange', 'onvisibilitychange': 'on_visibilitychange', 'on_visibilitychange': 'on_visibilitychange',
|
||||
'close': 'on_close', 'onclose': 'on_close', 'on_close': 'on_close',
|
||||
'load': 'on_load', 'onload': 'on_load', 'on_load': 'on_load',
|
||||
}
|
||||
|
||||
|
||||
class UI_Core_Elements():
|
||||
@classmethod
|
||||
def fromHTMLFile(cls, path_html, *, frame_depth=1, frames_deep=1, f_globals=None, f_locals=None, **kwargs):
|
||||
if not path_html: return []
|
||||
assert os.path.exists(path_html), f'Could not find HTML {path_html}'
|
||||
html = open(path_html, 'rt').read()
|
||||
return cls.fromHTML(
|
||||
html,
|
||||
frame_depth=frame_depth+1,
|
||||
frames_deep=frames_deep,
|
||||
f_globals=f_globals,
|
||||
f_locals=f_locals,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fromHTML(cls, html, *, frame_depth=1, frames_deep=1, f_globals=None, f_locals=None, **kwargs):
|
||||
# use passed global and local contexts or grab contexts from calling function
|
||||
# these contexts are needed for bound variables
|
||||
if f_globals and f_locals:
|
||||
f_globals = f_globals
|
||||
f_locals = dict(f_locals)
|
||||
else:
|
||||
ff_globals, ff_locals = {}, {}
|
||||
frame = inspect.currentframe()
|
||||
for i in range(frame_depth + frames_deep):
|
||||
if i >= frame_depth:
|
||||
ff_globals = frame.f_globals | ff_globals
|
||||
ff_locals = frame.f_locals | ff_locals
|
||||
frame = frame.f_back
|
||||
f_globals = f_globals or ff_globals
|
||||
f_locals = dict(f_locals or ff_locals)
|
||||
f_locals |= kwargs
|
||||
|
||||
def next_close(html, tagName):
|
||||
m_tag = re_html_tag.search(html)
|
||||
if not m_tag: return None
|
||||
if m_tag.group('name').lower() != tagName: return None
|
||||
if not m_tag.group('close'): return None
|
||||
innerText = html[:m_tag.start()].lstrip()
|
||||
post_html = html[m_tag.end():].lstrip()
|
||||
return Dict({
|
||||
'innerText': innerText,
|
||||
'post_html': post_html
|
||||
})
|
||||
|
||||
def get_next_tag(html, ui_cur, tab, hierarchy):
|
||||
m_tag = re_html_tag.search(html)
|
||||
if not m_tag: return None
|
||||
|
||||
cur_tagName = ui_cur._tagName if ui_cur else None
|
||||
|
||||
pre_html = html[:m_tag.start()].lstrip()
|
||||
post_html = html[m_tag.end():].lstrip()
|
||||
|
||||
tname = m_tag.group('name').lower()
|
||||
attributes = m_tag.group('attributes')
|
||||
is_close = m_tag.group('close') is not None
|
||||
is_selfclose = m_tag.group('selfclose') or tname in tags_selfclose
|
||||
|
||||
event_data = {
|
||||
'this': None,
|
||||
}
|
||||
def process(ui_this):
|
||||
nonlocal event_data
|
||||
event_data['this'] = ui_this
|
||||
|
||||
attribs = {}
|
||||
if attributes:
|
||||
for m_attrib in re_attributes.finditer(attributes):
|
||||
k, v = m_attrib.group('key'), m_attrib.group('value')
|
||||
|
||||
# translate HTML attribs to CC UI attribs
|
||||
if k.lower() in {'class'}: k = 'classes'
|
||||
if k.lower() in {'for'}: k = 'forId'
|
||||
|
||||
##############################################################
|
||||
# translate HTML attrib values to CC UI attrib values
|
||||
|
||||
# if no value given, default is True
|
||||
if v is None: v = 'True'
|
||||
|
||||
# remove wrapping quotes and un-escape any escaped quote
|
||||
if v.startswith('"'):
|
||||
# wrapped in double quotes
|
||||
v = v[1:-1]
|
||||
v = re.sub(r"\\\"", '"', v)
|
||||
elif v.startswith("'"):
|
||||
# wrapped in single quotes
|
||||
v = v[1:-1]
|
||||
v = re.sub(r"\\\'", "'", v)
|
||||
|
||||
if k.lower() in {'title', 'class', 'classes'}:
|
||||
# apply fstring
|
||||
while True:
|
||||
m = re_fstring.search(v)
|
||||
if not m: break
|
||||
pre, post = v[:m.start()], v[m.end():]
|
||||
nv = eval(m.group('eval'), f_globals, f_locals)
|
||||
v = f'{pre}{nv}{post}'
|
||||
|
||||
# convert value to Python value
|
||||
m_self = re_self.match(v)
|
||||
m_bound = re_bound.match(v)
|
||||
m_int = re_int.match(v)
|
||||
m_float = re_float.match(v)
|
||||
|
||||
if k.lower() in events_known:
|
||||
# attribute is an event (value is callback)
|
||||
k = events_known[k.lower()]
|
||||
def precall(f_locals):
|
||||
nonlocal event_data
|
||||
for dk,dv in event_data.items():
|
||||
f_locals[dk] = dv
|
||||
v = delay_exec(v, f_globals=f_globals, f_locals=f_locals, ordered_parameters=['event'], precall=precall)
|
||||
elif v.lower() in {'true'}: v = True
|
||||
elif v.lower() in {'false'}: v = False
|
||||
elif m_int: v = int(v)
|
||||
elif m_float: v = float(v)
|
||||
elif m_self: v = eval(v, f_globals, f_locals)
|
||||
elif m_bound:
|
||||
try:
|
||||
v = eval(v, f_globals, f_locals)
|
||||
except Exception as e:
|
||||
print(f'')
|
||||
print(f'Caught Exception {e} while trying to eval {v}')
|
||||
print(f'{f_globals=}')
|
||||
print(f'{f_locals=}')
|
||||
raise e
|
||||
|
||||
attribs[k] = v
|
||||
|
||||
assert not (is_close and attribs), 'Cannot have closing tag with attributes'
|
||||
assert not (is_close and is_selfclose), f'Cannot be closing and self-closing: {m_tag.group("tag")}'
|
||||
assert not (is_close and tname != cur_tagName), f'Found ending tag {m_tag.group("tag")} but expecting </{cur_tagName}>\n{hierarchy}'
|
||||
assert tname in tags_known, f'Unhandled tag type: {m_tag.group("tag")}'
|
||||
|
||||
return Dict({
|
||||
'pre_html': pre_html,
|
||||
'post_html': post_html,
|
||||
'tname': tname,
|
||||
'attribs': attribs,
|
||||
'is_close': is_close,
|
||||
'is_selfclose': is_selfclose,
|
||||
'process': process,
|
||||
})
|
||||
|
||||
def create(*args, **kwargs):
|
||||
if kwargs.get('tagName', '') == 'dialog':
|
||||
kwargs.setdefault('clamp_to_parent', True)
|
||||
ui = cls(*args, **kwargs)
|
||||
def cb():
|
||||
ui.dirty(cause='BoundVar changed')
|
||||
for k,v in kwargs.items():
|
||||
if isinstance(v, BoundVar):
|
||||
v.on_change(cb)
|
||||
return ui
|
||||
|
||||
def process(html, ui_cur, hierarchy=[]):
|
||||
depth = len(hierarchy)
|
||||
tab = ' '*depth
|
||||
ret = []
|
||||
while html.strip():
|
||||
tag = get_next_tag(html, ui_cur, tab, hierarchy)
|
||||
if not tag:
|
||||
return (ret + [create(tagName='text', pseudoelement='text', innerText=html)], '')
|
||||
|
||||
if tag.pre_html.strip():
|
||||
# <tag>found some text here </tag>/<anothertag>/<selfclose/>...
|
||||
# ^ ^ tag.tname
|
||||
# \_ started here: tag.pre_html
|
||||
ui_text = create(tagName='text', pseudoelement='text', innerText=tag.pre_html)
|
||||
ret += [ui_text]
|
||||
|
||||
if tag.is_close:
|
||||
# <tag>...</tag>
|
||||
# ^ ^ closing current tag
|
||||
# \_ started here, but this is already processed
|
||||
return (ret, tag.post_html)
|
||||
elif tag.is_selfclose:
|
||||
# <tag>...<selfclose/>...
|
||||
# ^ ^ ^ tag.post_html
|
||||
# | \_ self-closing tag
|
||||
# \_ started here, but this is already processed
|
||||
ui_new = create(tagName=tag.tname, **tag.attribs)
|
||||
tag.process(ui_new)
|
||||
ret.append(ui_new)
|
||||
html = tag.post_html
|
||||
else:
|
||||
# <tag>...<anothertag>...
|
||||
# ^ ^ ^ tag.post_html
|
||||
# | \_ starting another tag
|
||||
# \_ started here, but this is already processed
|
||||
# check if anothertag is immediately closed, especially looking for <script>
|
||||
nclose = next_close(tag.post_html, tag.tname)
|
||||
if nclose:
|
||||
# case: <anothertag>some innerText</anothertag>...
|
||||
if tag.tname.lower() == 'script':
|
||||
# case anothertag=script: <script>some python code</script>
|
||||
# TODO: check for src attribute!
|
||||
written = []
|
||||
f_locals['write'] = written.append
|
||||
# print(f'executing script: {nclose.innerText}')
|
||||
exec(nclose.innerText, f_globals, f_locals)
|
||||
# prepend anything written out to HTML so it can be processed
|
||||
html = '\n'.join(written) + nclose.post_html
|
||||
else:
|
||||
# just stick pre_html into innerText
|
||||
innerText = nclose.innerText if nclose.innerText.strip() else None
|
||||
ui_new = create(tagName=tag.tname, innerText=innerText, **tag.attribs)
|
||||
tag.process(ui_new)
|
||||
ret.append(ui_new)
|
||||
html = nclose.post_html
|
||||
else:
|
||||
ui_new = create(tagName=tag.tname, **tag.attribs)
|
||||
tag.process(ui_new)
|
||||
children, html = process(tag.post_html, ui_new, hierarchy+[tag.tname])
|
||||
for child in children: ui_new.append_child(child)
|
||||
ret.append(ui_new)
|
||||
return (ret, html.strip())
|
||||
|
||||
# remove HTML comments
|
||||
html = re_html_comment.sub('', html)
|
||||
# strip leading and trailing whitespace characters
|
||||
html = re.sub(r'^[ \n\r\t]+', '', html)
|
||||
html = re.sub(r'[ \n\r\t]+$', '', html)
|
||||
|
||||
lui,rest = process(html, None)
|
||||
assert not rest, f'Could not process all of HTML\nRemaining: {rest}\nHTML: {html}'
|
||||
return lui
|
||||
|
||||
def _init_input_box(self, input_type):
|
||||
allowed = None # allow any character
|
||||
match input_type:
|
||||
case 'text':
|
||||
# could set
|
||||
# allowed = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 `~!@#$%^&*()[{]}\'"\\|-_;:,<.>'''
|
||||
# but that would exclude any non-US-keyboard inputs
|
||||
pass
|
||||
case 'number':
|
||||
if type(self._value) is BoundInt:
|
||||
if self._value.min_value is not None and self._value.min_value >= 0:
|
||||
# only non-negative ints
|
||||
allowed = '''0123456789'''
|
||||
else:
|
||||
# can be negative
|
||||
allowed = '''-0123456789'''
|
||||
else:
|
||||
# can be float
|
||||
allowed = '''0123456789.-'''
|
||||
case _:
|
||||
assert False, f'UI_Element.process_input_box: unhandled type {input_type}'
|
||||
|
||||
data = {'orig':None, 'text':None, 'idx':0, 'pos':None}
|
||||
|
||||
def preclean():
|
||||
if data['text'] is None:
|
||||
if type(self.value) is float:
|
||||
self.innerText = f'{self.value:0.4g}'
|
||||
else:
|
||||
self.innerText = f'{self.value}'
|
||||
else:
|
||||
self.innerText = data['text']
|
||||
# self.dirty_content(cause='preclean called')
|
||||
|
||||
def postflow():
|
||||
if data['text'] is None: return
|
||||
data['pos'] = self.get_text_pos(data['idx'])
|
||||
if self._ui_marker._absolute_size:
|
||||
if data['pos']:
|
||||
self._ui_marker.reposition(
|
||||
left=data['pos'].x - self._ui_marker._absolute_size.width / 2,
|
||||
top=data['pos'].y,
|
||||
clamp_position=(self.scrollLeft <= 0),
|
||||
)
|
||||
cursor_postflow()
|
||||
else:
|
||||
# sometimes, content can change too quickly, so data isn't filled
|
||||
# in this case, just dirty ourselves so that we will re-render
|
||||
self.dirty_content()
|
||||
def cursor_postflow():
|
||||
if data['text'] is None: return
|
||||
self._setup_ltwh()
|
||||
self._ui_marker._setup_ltwh()
|
||||
vl = self._l + self._mbp_left
|
||||
vr = self._r - self._mbp_right
|
||||
vw = self._w - self._mbp_width
|
||||
if self._ui_marker._r > vr:
|
||||
dx = self._ui_marker._r - vr + 2
|
||||
self.scrollLeft = self.scrollLeft + dx
|
||||
self._setup_ltwh()
|
||||
if self._ui_marker._l < vl:
|
||||
dx = self._ui_marker._l - vl - 2
|
||||
self.scrollLeft = self.scrollLeft + dx
|
||||
self._setup_ltwh()
|
||||
|
||||
def set_cursor(e):
|
||||
data['idx'] = self.get_text_index(e.mouse)
|
||||
data['pos'] = self.get_text_pos(data['idx'])
|
||||
self.dirty_flow()
|
||||
|
||||
def focus(e):
|
||||
s = f'{self.value:0.4g}' if type(self.value) is float else str(self.value)
|
||||
data['orig'] = data['text'] = s
|
||||
self._ui_marker.is_visible = True
|
||||
set_cursor(e)
|
||||
def blur(e):
|
||||
changed = data['orig'] != data['text']
|
||||
self.value = data['text']
|
||||
data['text'] = None
|
||||
self._ui_marker.is_visible = False
|
||||
if changed: self.dispatch_event('on_change')
|
||||
|
||||
def mouseup(e):
|
||||
if not e.button[0]: return
|
||||
# if not self.is_focused: return
|
||||
set_cursor(e)
|
||||
def mousemove(e):
|
||||
if data['text'] is None: return
|
||||
if not e.button[0]: return
|
||||
set_cursor(e)
|
||||
def mousedown(e):
|
||||
if data['text'] is None: return
|
||||
if not e.button[0]: return
|
||||
set_cursor(e)
|
||||
|
||||
def keypress(e):
|
||||
if data['text'] == None: return
|
||||
if e.key == 'Backspace':
|
||||
if data['idx'] == 0: return
|
||||
data['text'] = data['text'][0:data['idx']-1] + data['text'][data['idx']:]
|
||||
data['idx'] -= 1
|
||||
elif e.key == 'Enter':
|
||||
self.blur()
|
||||
elif e.key == 'Escape':
|
||||
data['text'] = data['orig']
|
||||
self.blur()
|
||||
elif e.key == 'End':
|
||||
data['idx'] = len(data['text'])
|
||||
self.dirty()
|
||||
self.dirty_flow()
|
||||
elif e.key == 'Home':
|
||||
data['idx'] = 0
|
||||
self.dirty()
|
||||
self.dirty_flow()
|
||||
elif e.key == 'ArrowLeft':
|
||||
data['idx'] = max(data['idx'] - 1, 0)
|
||||
self.dirty()
|
||||
self.dirty_flow()
|
||||
elif e.key == 'ArrowRight':
|
||||
data['idx'] = min(data['idx'] + 1, len(data['text']))
|
||||
self.dirty()
|
||||
self.dirty_flow()
|
||||
elif e.key == 'Delete':
|
||||
if data['idx'] == len(data['text']): return
|
||||
data['text'] = data['text'][0:data['idx']] + data['text'][data['idx']+1:]
|
||||
elif len(e.key) > 1:
|
||||
return
|
||||
elif allowed is None or e.key in allowed:
|
||||
newtext = data['text'][:data['idx']] + e.key + data['text'][data['idx']:]
|
||||
if self.maxlength is not None and len(newtext) > self.maxlength: return
|
||||
data['text'] = newtext
|
||||
data['idx'] += 1
|
||||
preclean()
|
||||
def paste(e):
|
||||
if data['text'] == None: return
|
||||
clipboardData = str(e.clipboardData)
|
||||
if allowed: clipboardData = ''.join(c for c in clipboardData if c in allowed)
|
||||
if self.maxlength is not None:
|
||||
# only insert enough chars to prevent going above maxlength
|
||||
origlen, cliplen = len(data['text']), len(clipboardData)
|
||||
if origlen + cliplen > self.maxlength:
|
||||
clipboardData = clipboardData[:(self.maxlength - origlen)]
|
||||
data['text'] = data['text'][:data['idx']] + clipboardData + data['text'][data['idx']:]
|
||||
data['idx'] += len(clipboardData)
|
||||
preclean()
|
||||
|
||||
self.preclean = preclean
|
||||
self.postflow = postflow
|
||||
|
||||
self.add_eventListener('on_focus', focus)
|
||||
self.add_eventListener('on_blur', blur)
|
||||
self.add_eventListener('on_keypress', keypress)
|
||||
self.add_eventListener('on_paste', paste)
|
||||
self.add_eventListener('on_mousedown', mousedown)
|
||||
self.add_eventListener('on_mousemove', mousemove)
|
||||
self.add_eventListener('on_mouseup', mouseup)
|
||||
|
||||
preclean()
|
||||
|
||||
def _process_input_box(self):
|
||||
if self._ui_marker is None:
|
||||
# just got focus, so create a cursor element
|
||||
self._ui_marker = self._generate_new_ui_elem(
|
||||
tagName=self._tagName,
|
||||
type=self._type,
|
||||
classes=self._classes_str,
|
||||
pseudoelement='marker',
|
||||
)
|
||||
self._ui_marker.is_visible = False
|
||||
else:
|
||||
self._new_content = True
|
||||
self._children_gen += [self._ui_marker]
|
||||
return [*self._children, self._ui_marker]
|
||||
|
||||
is_focused, was_focused = self.is_focused, getattr(self, '_was_focused', None)
|
||||
self._was_focused = is_focused
|
||||
|
||||
if not is_focused:
|
||||
# not focused, so no cursor!
|
||||
if was_focused:
|
||||
self._ui_marker = None
|
||||
self._selectionStart = None
|
||||
self._selectionEnd = None
|
||||
return self._children
|
||||
|
||||
if not was_focused:
|
||||
# was not focused, but has focus now
|
||||
# store current text in case ESC is pressed to cancel (revert to original)
|
||||
self._innerText_original = self._innerText
|
||||
|
||||
if not self._ui_marker:
|
||||
# just got focus, so create a cursor element
|
||||
self._ui_marker = self._generate_new_ui_elem(
|
||||
tagName=self._tagName,
|
||||
type=self._type,
|
||||
classes=self._classes_str,
|
||||
pseudoelement='marker',
|
||||
)
|
||||
else:
|
||||
self._new_content = True
|
||||
self._children_gen += [self._ui_marker]
|
||||
|
||||
return [*self._children, self._ui_marker]
|
||||
|
||||
def _process_input_range(self):
|
||||
assert self._value_bound, f'{self} must have bound value ({self.value})'
|
||||
if not getattr(self, '_processed_input_range', False):
|
||||
self._processed_input_range = True
|
||||
ui_left = self.append_new_child(tagName='span', classes='inputrange-left')
|
||||
ui_handle = self.append_new_child(tagName='span', classes='inputrange-handle')
|
||||
ui_right = self.append_new_child(tagName='span', classes='inputrange-right')
|
||||
|
||||
state = Dict()
|
||||
state.reset = delay_exec('''state.set(grabbed=False, down=None, initval=None, cancelled=False)''')
|
||||
state.cancel = delay_exec('''state.value = state.initval; state.cancelled = True''')
|
||||
state.reset()
|
||||
|
||||
def postflow():
|
||||
if not self.is_visible: return
|
||||
# since ui_left, ui_right, and ui_handle are all absolutely positioned UI elements,
|
||||
# we can safely move them around without dirtying (the UI system does not need to
|
||||
# clean anything or reflow the elements)
|
||||
|
||||
w, W = ui_handle.width_scissor, self.width_scissor
|
||||
if w == 'auto' or W == 'auto': return # UI system is not ready yet
|
||||
W -= self._mbp_width
|
||||
|
||||
mw = W - w # max dist the handle can move
|
||||
p = self._value.bounded_ratio # convert value to [0,1] based on min,max
|
||||
hl = p * mw # find where handle (left side) should be
|
||||
m = hl + (w / 2) # compute center of handle
|
||||
|
||||
ui_left.width_override = math.floor(m)
|
||||
ui_handle._alignment_offset = Vec2D((math.floor(hl), 0))
|
||||
ui_right.width_override = math.floor(W-m)
|
||||
ui_right._alignment_offset = Vec2D((math.ceil(m), 0))
|
||||
|
||||
ui_left.dirty(cause='input range value changed', properties='renderbuf')
|
||||
ui_right.dirty(cause='input range value changed', properties='renderbuf')
|
||||
|
||||
def handle_mousedown(e):
|
||||
if e.button[2] and state['grabbed']:
|
||||
# right mouse button cancels
|
||||
state.cancel()
|
||||
e.stop_propagation()
|
||||
return
|
||||
if not e.button[0]: return
|
||||
state.set(
|
||||
grabbed=True,
|
||||
down=e.mouse,
|
||||
initval=self._value.value,
|
||||
cancelled=False,
|
||||
)
|
||||
e.stop_propagation()
|
||||
def handle_mouseup(e):
|
||||
if e.button[0]: return
|
||||
e.stop_propagation()
|
||||
state.reset()
|
||||
def handle_mousemove(e):
|
||||
if not state.grabbed or state.cancelled: return
|
||||
m, M = self._value.min_value, self._value.max_value
|
||||
p = (e.mouse.x - state['down'].x) / self.width_pixels
|
||||
self._value.value = state.initval + p * (M - m)
|
||||
e.stop_propagation()
|
||||
postflow()
|
||||
def handle_keypress(e):
|
||||
if not state.grabbed or state.cancelled: return
|
||||
if e.key == 'ESC':
|
||||
state.cancel()
|
||||
e.stop_propagation()
|
||||
self.add_eventListener('on_mousemove', handle_mousemove, useCapture=True)
|
||||
self.add_eventListener('on_mousedown', handle_mousedown, useCapture=True)
|
||||
self.add_eventListener('on_mouseup', handle_mouseup, useCapture=True)
|
||||
self.add_eventListener('on_keypress', handle_keypress, useCapture=True)
|
||||
|
||||
ui_handle.postflow = postflow
|
||||
self._value.on_change(postflow)
|
||||
return self._children
|
||||
|
||||
def _process_label(self):
|
||||
if not getattr(self, '_processed_label', False):
|
||||
self._processed_label = True
|
||||
def mouseclick(e):
|
||||
if not e.target.is_descendant_of(self): return
|
||||
ui_for = self.get_for_element()
|
||||
if not ui_for: return
|
||||
if ui_for == e.target: return
|
||||
ui_for.dispatch_event('on_mouseclick')
|
||||
self.add_eventListener('on_mouseclick', mouseclick, useCapture=True)
|
||||
return self._children
|
||||
|
||||
|
||||
def _process_input_checkbox(self):
|
||||
if self._ui_marker is None:
|
||||
self._ui_marker = self._generate_new_ui_elem(
|
||||
tagName=self._tagName,
|
||||
type=self._type,
|
||||
checked=self.checked,
|
||||
classes=self._classes_str,
|
||||
pseudoelement='marker',
|
||||
)
|
||||
self.add_eventListener('on_mouseclick', delay_exec('''self.checked = not bool(self.checked)'''))
|
||||
else:
|
||||
self._children_gen += [self._ui_marker]
|
||||
self._new_content = True
|
||||
return [self._ui_marker, *self._children]
|
||||
|
||||
def _init_input_radio(self):
|
||||
def on_input(e):
|
||||
if not self.checked: return
|
||||
ui_elements = self.get_root().getElementsByName(self.name)
|
||||
for ui_element in ui_elements:
|
||||
if ui_element != self:
|
||||
ui_element.checked = False
|
||||
def on_click(e):
|
||||
self.checked = True
|
||||
self.add_eventListener('on_mouseclick', on_click)
|
||||
self.add_eventListener('on_input', on_input)
|
||||
def _process_input_radio(self):
|
||||
if self._ui_marker is None:
|
||||
self._ui_marker = self._generate_new_ui_elem(
|
||||
tagName=self._tagName,
|
||||
type=self._type,
|
||||
checked=self.checked,
|
||||
classes=self._classes_str,
|
||||
pseudoelement='marker',
|
||||
)
|
||||
else:
|
||||
self._children_gen += [self._ui_marker]
|
||||
self._new_content = True
|
||||
return [self._ui_marker, *self._children]
|
||||
|
||||
def _process_details(self):
|
||||
is_open, was_open = self.open, getattr(self, '_was_open', None)
|
||||
self._was_open = is_open
|
||||
|
||||
if not getattr(self, '_processed_details', False):
|
||||
self._processed_details = True
|
||||
def mouseclick(e):
|
||||
doit = False
|
||||
doit |= e.target == self # clicked on <details>
|
||||
doit |= e.target.tagName == 'summary' and e.target._parent == self # clicked on <summary> of <details>
|
||||
if not doit: return
|
||||
self.open = not self.open
|
||||
self.add_eventListener('on_mouseclick', mouseclick)
|
||||
|
||||
if self._get_child_tagName(0) != 'summary':
|
||||
# <details> does not have a <summary>, so create a default one
|
||||
if self._ui_marker is None:
|
||||
self._ui_marker = self.prepend_new_child(tagName='summary', innerText='Details')
|
||||
summary = self._ui_marker
|
||||
contents = self._children if is_open else []
|
||||
else:
|
||||
summary = self._children[0]
|
||||
contents = self._children[1:] if is_open else []
|
||||
|
||||
# set _new_content to show contents if open is toggled
|
||||
self._new_content |= was_open != is_open
|
||||
return [summary, *contents]
|
||||
|
||||
def _process_summary(self):
|
||||
marker = self._generate_new_ui_elem(
|
||||
tagName='summary',
|
||||
classes=self._classes_str,
|
||||
pseudoelement='marker'
|
||||
)
|
||||
return [marker, *self._children]
|
||||
|
||||
def _process_dialog(self):
|
||||
if not self.has_class('framed'):
|
||||
return self._children
|
||||
|
||||
if self._get_child_tagName(0) != 'h1':
|
||||
self.prepend_new_child(tagName='h1', innerText='Window')
|
||||
|
||||
return self._children
|
||||
|
||||
def _process_progress(self):
|
||||
# print('=====================')
|
||||
# print('PROCESSING PROGRESS')
|
||||
if self._ui_marker is None:
|
||||
self._ui_marker = self.append_new_child(
|
||||
tagName='progressmarker', #self._tagName,
|
||||
classes=self._classes_str,
|
||||
# pseudoelement='marker',
|
||||
)
|
||||
|
||||
prev = -1
|
||||
|
||||
def update_progress():
|
||||
nonlocal prev
|
||||
try:
|
||||
percent = float(self.value or 0) / float(self.valueMax or 100)
|
||||
except Exception as e:
|
||||
percent = random.random()
|
||||
print(f'Caught {e} with {self.value=} and {self.valueMax=}')
|
||||
percent = int(100 * percent)
|
||||
if percent == prev: return
|
||||
prev = percent
|
||||
|
||||
self._ui_marker.style = f'width:{percent}%'
|
||||
# self._ui_marker.style_width = f'{percent}%'
|
||||
self.dirty()
|
||||
self.dirty_flow()
|
||||
self._ui_marker.dirty()
|
||||
self._ui_marker.dirty_flow()
|
||||
# tag_redraw_all('update progress')
|
||||
# self.document.force_dirty_all()
|
||||
update_progress()
|
||||
self.add_eventListener('on_input', update_progress)
|
||||
|
||||
# else:
|
||||
# self._children_gen = [self._ui_marker]
|
||||
# self._new_content = True
|
||||
|
||||
|
||||
return self._children # [self._ui_marker]
|
||||
|
||||
|
||||
def _process_h1(self):
|
||||
if self._parent and self._parent._tagName == 'dialog' and self._parent._children[0] == self:
|
||||
dialog = self._parent
|
||||
if not dialog.has_class('framed'):
|
||||
return self._children
|
||||
|
||||
if not getattr(self, '_processed_dialog', False):
|
||||
self._processed_dialog = True
|
||||
|
||||
# add minimize button to <h1> (only visible if dialog has minimizeable class)
|
||||
def minimize():
|
||||
dialog.is_visible = False
|
||||
dialog.dispatch_event('on_toggle') # hijack the toggle event to catch minimize events
|
||||
self.prepend_new_child(tagName='button', title="Minimize dialog", classes='dialog-minimize dialog-action', on_mouseclick=minimize)
|
||||
|
||||
# add close button to <h1> (only visible if dialog has closeable class)
|
||||
def close():
|
||||
if dialog._parent is None: return
|
||||
dialog._parent.delete_child(dialog)
|
||||
dialog.dispatch_event('on_close')
|
||||
self.prepend_new_child(tagName='button', title="Close dialog", classes='dialog-close dialog-action', on_mouseclick=close)
|
||||
|
||||
# add event handlers to <h1> for dragging window around (only moveable if dialog has moveable class)
|
||||
state = Dict(
|
||||
is_dragging=False,
|
||||
mousedown_pos=None,
|
||||
original_pos=None,
|
||||
)
|
||||
def mousedown(e):
|
||||
if not dialog.has_class('moveable'): return
|
||||
if e.target != self and e.target != self: return
|
||||
dialog.document.ignore_hover_change = True
|
||||
state.is_dragging = True
|
||||
state.mousedown_pos = e.mouse
|
||||
l = dialog.left_pixels
|
||||
if l is None or l == 'auto': l = 0
|
||||
t = dialog.top_pixels
|
||||
if t is None or t == 'auto': t = 0
|
||||
state.original_pos = Point2D((float(l), float(t)))
|
||||
def mouseup(e):
|
||||
if not dialog.has_class('moveable'): return
|
||||
state.is_dragging = False
|
||||
dialog.document.ignore_hover_change = False
|
||||
def mousemove(e):
|
||||
if not dialog.has_class('moveable'): return
|
||||
if not state.is_dragging: return
|
||||
delta = e.mouse - state.mousedown_pos
|
||||
new_pos = state.original_pos + delta
|
||||
dialog.reposition(left=new_pos.x, top=new_pos.y)
|
||||
self.add_eventListener('on_mousedown', mousedown)
|
||||
self.add_eventListener('on_mouseup', mouseup)
|
||||
self.add_eventListener('on_mousemove', mousemove)
|
||||
|
||||
return self._children
|
||||
|
||||
def _process_li(self):
|
||||
if self._parent and self._parent._tagName == 'ul':
|
||||
# <ul><li>...
|
||||
if not self._ui_marker:
|
||||
self._ui_marker = self.prepend_new_child(tagName='li', classes=self._classes_str, pseudoelement='marker')
|
||||
return self._children
|
||||
|
||||
elif self._parent and self._parent._tagName == 'ol':
|
||||
# <ol><li>...
|
||||
if not self._ui_marker:
|
||||
idx = next((i+1 for (i,c) in enumerate(self._parent._children) if self==c), 0)
|
||||
self._ui_marker = self.prepend_new_child(tagName='li', classes=self._classes_str, pseudoelement='marker', innerText=f'{idx}.')
|
||||
return self._children
|
||||
|
||||
return self._children
|
||||
|
||||
def _setup_element(self):
|
||||
processors = {
|
||||
'input text': lambda: self._init_input_box('text'),
|
||||
'input number': lambda: self._init_input_box('number'),
|
||||
'input radio': self._init_input_radio,
|
||||
}
|
||||
processor = processors.get(self.tagType, None)
|
||||
return processor() if processor else None
|
||||
|
||||
def _process_children(self):
|
||||
if self._innerTextAsIs is not None: return []
|
||||
if self._pseudoelement == 'marker': return self._children
|
||||
|
||||
processors = {
|
||||
'input radio': self._process_input_radio,
|
||||
'input checkbox': self._process_input_checkbox,
|
||||
'input text': self._process_input_box,
|
||||
'input number': self._process_input_box,
|
||||
'input range': self._process_input_range,
|
||||
'details': self._process_details,
|
||||
'summary': self._process_summary,
|
||||
'label': self._process_label,
|
||||
'dialog': self._process_dialog,
|
||||
'h1': self._process_h1,
|
||||
'li': self._process_li,
|
||||
'progress': self._process_progress,
|
||||
}
|
||||
processor = processors.get(self.tagType, None)
|
||||
|
||||
return processor() if processor else self._children
|
||||
@@ -0,0 +1,137 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from inspect import signature
|
||||
|
||||
from .ui_event import UI_Event
|
||||
|
||||
from .profiler import profiler, time_it
|
||||
|
||||
|
||||
class UI_Core_Events:
|
||||
def _init_events(self):
|
||||
# all events with their respective callbacks
|
||||
# NOTE: values of self._events are list of tuples, where:
|
||||
# - first item is bool indicating type of callback, where True=capturing and False=bubbling
|
||||
# - second item is the callback function, possibly wrapped with lambda
|
||||
# - third item is the original callback function
|
||||
self._events = {
|
||||
'on_load': [], # called when document is set
|
||||
'on_focus': [], # focus is gained (:foces is added)
|
||||
'on_blur': [], # focus is lost (:focus is removed)
|
||||
'on_focusin': [], # focus is gained to self or a child
|
||||
'on_focusout': [], # focus is lost from self or a child
|
||||
'on_keydown': [], # key is pressed down
|
||||
'on_keyup': [], # key is released
|
||||
'on_keypress': [], # key is entered (down+up)
|
||||
'on_paste': [], # user is pasting from clipboard
|
||||
'on_mouseenter': [], # mouse enters self (:hover is added)
|
||||
'on_mousemove': [], # mouse moves over self
|
||||
'on_mousedown': [], # mouse button is pressed down
|
||||
'on_mouseup': [], # mouse button is released
|
||||
'on_mouseclick': [], # mouse button is clicked (down+up while remaining on self)
|
||||
'on_mousedblclick': [], # mouse button is pressed twice in quick succession
|
||||
'on_mouseleave': [], # mouse leaves self (:hover is removed)
|
||||
'on_scroll': [], # self is being scrolled
|
||||
'on_input': [], # occurs immediately after value has changed
|
||||
'on_change': [], # occurs after blur if value has changed
|
||||
'on_toggle': [], # occurs when open attribute is toggled
|
||||
'on_close': [], # dialog is closed
|
||||
'on_visibilitychange': [], # element became visible or hidden
|
||||
}
|
||||
|
||||
|
||||
def add_eventListener(self, event, callback, useCapture=False):
|
||||
ovent = event
|
||||
event = event if event.startswith('on_') else f'on_{event}'
|
||||
assert event in self._events, f'Attempting to add unhandled event handler type "{oevent}"'
|
||||
sig = signature(callback)
|
||||
old_callback = callback
|
||||
if len(sig.parameters) == 0:
|
||||
callback = lambda e: old_callback()
|
||||
self._events[event] += [(useCapture, callback, old_callback)]
|
||||
|
||||
def remove_eventListener(self, event, callback):
|
||||
# returns True if callback was successfully removed
|
||||
oevent = event
|
||||
event = event if event.startswith('on_') else f'on_{event}'
|
||||
assert event in self._events, f'Attempting to remove unhandled event handler type "{ovent}"'
|
||||
l = len(self._events[event])
|
||||
self._events[event] = [(capture,cb,old_cb) for (capture,cb,old_cb) in self._events[event] if old_cb != callback]
|
||||
return l != len(self._events[event])
|
||||
|
||||
def _fire_event(self, event, details):
|
||||
ph = details.event_phase
|
||||
cap, bub, df = details.capturing, details.bubbling, not details.default_prevented
|
||||
try:
|
||||
if (cap and ph == 'capturing') or (df and ph == 'at target'):
|
||||
for (cap,cb,old_cb) in self._events[event]:
|
||||
if not cap: continue
|
||||
cb(details)
|
||||
if (bub and ph == 'bubbling') or (df and ph == 'at target'):
|
||||
for (cap,cb,old_cb) in self._events[event]:
|
||||
if cap: continue
|
||||
cb(details)
|
||||
except Exception as e:
|
||||
print(f'COOKIE CUTTER >> Exception Caught while trying to callback event handlers')
|
||||
print(f'UI_Element: {self}')
|
||||
print(f'event: {event}')
|
||||
print(f'exception: {e}')
|
||||
raise e
|
||||
|
||||
# @profiler.function
|
||||
def dispatch_event(self, event, mouse=None, button=None, key=None, clipboardData=None, ui_event=None, stop_at=None):
|
||||
event = event if event.startswith('on_') else f'on_{event}'
|
||||
if self._document:
|
||||
if mouse is None:
|
||||
mouse = self._document.actions.mouse
|
||||
if button is None:
|
||||
button = (
|
||||
self._document.actions.using('LEFTMOUSE'),
|
||||
self._document.actions.using('MIDDLEMOUSE'),
|
||||
self._document.actions.using('RIGHTMOUSE')
|
||||
)
|
||||
# else:
|
||||
# if mouse is None or button is None:
|
||||
# print(f'UI_Element.dispatch_event: {event} dispatched on {self}, but self.document = {self.document} (root={self.get_root()}')
|
||||
|
||||
if ui_event is None:
|
||||
ui_event = UI_Event(target=self, mouse=mouse, button=button, key=key, clipboardData=clipboardData)
|
||||
|
||||
path = self.get_pathToRoot()[1:] # skipping first item, which is self
|
||||
if stop_at is not None and stop_at in path:
|
||||
path = path[:path.index(stop_at)]
|
||||
|
||||
ui_event.event_phase = 'capturing'
|
||||
for cur in path[::-1]:
|
||||
cur._fire_event(event, ui_event)
|
||||
if not ui_event.capturing: return ui_event.default_prevented
|
||||
|
||||
ui_event.event_phase = 'at target'
|
||||
self._fire_event(event, ui_event)
|
||||
|
||||
ui_event.event_phase = 'bubbling'
|
||||
if not ui_event.bubbling: return ui_event.default_prevented
|
||||
for cur in path:
|
||||
cur._fire_event(event, ui_event)
|
||||
if not ui_event.bubbling: return ui_event.default_prevented
|
||||
|
||||
return ui_event.default_prevented
|
||||
@@ -0,0 +1,93 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
from .blender import tag_redraw_all, get_path_from_addon_common, get_path_from_addon_root
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .fontmanager import FontManager
|
||||
|
||||
|
||||
fontmap = {
|
||||
'serif': {
|
||||
'normal': {
|
||||
'normal': 'DroidSerif-Regular.ttf',
|
||||
'bold': 'DroidSerif-Bold.ttf',
|
||||
},
|
||||
'italic': {
|
||||
'normal': 'DroidSerif-Italic.ttf',
|
||||
'bold': 'DroidSerif-BoldItalic.ttf',
|
||||
},
|
||||
},
|
||||
'sans-serif': {
|
||||
'normal': {
|
||||
'normal': 'DroidSans-Blender.ttf',
|
||||
'bold': 'OpenSans-Bold.ttf',
|
||||
},
|
||||
'italic': {
|
||||
'normal': 'OpenSans-Italic.ttf',
|
||||
'bold': 'OpenSans-BoldItalic.ttf',
|
||||
},
|
||||
},
|
||||
'monospace': {
|
||||
'normal': {
|
||||
'normal': 'DejaVuSansMono.ttf',
|
||||
'bold': 'DejaVuSansMono.ttf',
|
||||
},
|
||||
'italic': {
|
||||
'normal': 'DejaVuSansMono.ttf',
|
||||
'bold': 'DejaVuSansMono.ttf',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@add_cache('_cache', {})
|
||||
@add_cache('_paths', [
|
||||
get_path_from_addon_common('common', 'fonts'),
|
||||
get_path_from_addon_common('common'),
|
||||
get_path_from_addon_root('fonts'),
|
||||
])
|
||||
def get_font_path(fn, ext=None):
|
||||
cache = get_font_path._cache
|
||||
if ext: fn = f'{fn}.{ext}'
|
||||
if fn not in cache:
|
||||
cache[fn] = None
|
||||
for path in get_font_path._paths:
|
||||
p = os.path.join(path, fn)
|
||||
if os.path.exists(p):
|
||||
cache[fn] = p
|
||||
break
|
||||
return get_font_path._cache[fn]
|
||||
|
||||
def setup_font(fontid):
|
||||
FontManager.aspect(1, fontid)
|
||||
|
||||
def get_font(fontfamily, fontstyle=None, fontweight=None):
|
||||
if not fontstyle: fontstyle = 'normal'
|
||||
if not fontweight: fontweight = 'normal'
|
||||
# translate fontfamily, fontstyle, fontweight into a .ttf
|
||||
if fontfamily in fontmap: fontfamily = fontmap[fontfamily][fontstyle][fontweight]
|
||||
path = get_font_path(fontfamily)
|
||||
assert path, f'could not find font "{fontfamily}"'
|
||||
fontid = FontManager.load(path, setup_font)
|
||||
return fontid
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
import gpu
|
||||
|
||||
from .blender import tag_redraw_all, get_path_from_addon_common, get_path_from_addon_root
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
from ..ext import png
|
||||
from ..ext.apng import APNG
|
||||
|
||||
|
||||
def get_image_path(fn, ext=None, subfolders=None):
|
||||
'''
|
||||
If subfolders is not given, this function will look in folders shown below
|
||||
<addon_root>
|
||||
addon_common/
|
||||
common/
|
||||
ui_core.py <- this file
|
||||
images/ <- will look here
|
||||
<...>
|
||||
<...>
|
||||
icons/ <- and here (if exists)
|
||||
<...>
|
||||
images/ <- and here (if exists)
|
||||
<...>
|
||||
help/ <- and here (if exists)
|
||||
<...>
|
||||
<...>
|
||||
returns first path where fn is found
|
||||
order of search: <addon_root>/icons, <addon_root>/images, <addon_root>/help, <addon_root>/addon_common/common/images
|
||||
'''
|
||||
assert not subfolders, f'Subfolders arg for get_image_path not implemented, yet'
|
||||
if ext: fn = f'{fn}.{ext}'
|
||||
return iter_head(
|
||||
[
|
||||
path
|
||||
for path in [
|
||||
get_path_from_addon_root('icons', fn),
|
||||
get_path_from_addon_root('images', fn),
|
||||
get_path_from_addon_root('help', fn),
|
||||
get_path_from_addon_root('help', 'images', fn),
|
||||
get_path_from_addon_common('common', 'images', fn),
|
||||
]
|
||||
if os.path.exists(path)
|
||||
],
|
||||
default=None,
|
||||
)
|
||||
|
||||
def load_image_png(path):
|
||||
# note: assuming 4 channels (rgba) per pixel!
|
||||
width, height, data, m = png.Reader(path).asRGBA()
|
||||
img = [[row[i:i+4] for i in range(0, width*4, 4)] for row in data]
|
||||
return img
|
||||
|
||||
def load_image_apng(path):
|
||||
im_apng = APNG.open(path)
|
||||
print('load_image_apng', path, im_apng, im_apng.frames, im_apng.num_plays)
|
||||
im,control = im_apng.frames[0]
|
||||
w,h = control.width,control.height
|
||||
img = [[r[i:i+4] for i in range(0,w*4,4)] for r in d]
|
||||
return img
|
||||
|
||||
@add_cache('_cache', {})
|
||||
def load_image(fn):
|
||||
# important: assuming all images have distinct names!
|
||||
if fn not in load_image._cache:
|
||||
# have not seen this image before
|
||||
path = get_image_path(fn)
|
||||
_,ext = os.path.splitext(fn)
|
||||
# print(f'UI: Loading image "{fn}" (path={path})')
|
||||
if ext == '.png': img = load_image_png(path)
|
||||
elif ext == '.apng': img = load_image_apng(path)
|
||||
else: assert False, f'load_image: unhandled type ({ext}) for {fn}'
|
||||
load_image._cache[fn] = img
|
||||
return load_image._cache[fn]
|
||||
|
||||
@add_cache('_image', None)
|
||||
def get_unfound_image():
|
||||
if not get_unfound_image._image:
|
||||
c0, c1 = [128,128,128,0], [128,128,128,128]
|
||||
w, h = 10, 10
|
||||
image = []
|
||||
for y in range(h):
|
||||
row = []
|
||||
for x in range(w):
|
||||
c = c0 if (x+y)%2 == 0 else c1
|
||||
row.append(c)
|
||||
image.append(row)
|
||||
get_unfound_image._image = image
|
||||
return get_unfound_image._image
|
||||
|
||||
@add_cache('_image', None)
|
||||
def get_loading_image(fn):
|
||||
base, _ = os.path.splitext(fn)
|
||||
nfn = f'{base}.thumb.png'
|
||||
return load_image(nfn) if get_image_path(nfn) else get_unfound_image()
|
||||
|
||||
def is_image_cached(fn):
|
||||
return fn in load_image._cache
|
||||
|
||||
def has_thumbnail(fn):
|
||||
nfn = f'{os.path.splitext(fn)[0]}.thumb.png'
|
||||
return get_image_path(nfn) is not None
|
||||
|
||||
def set_image_cache(fn, img):
|
||||
if fn in load_image._cache: return
|
||||
load_image._cache[fn] = img
|
||||
|
||||
def preload_image(*fns):
|
||||
return [ (fn, load_image(fn)) for fn in fns ]
|
||||
|
||||
@add_cache('_cache', {})
|
||||
def load_texture(fn_image, image=None):
|
||||
if fn_image not in load_texture._cache:
|
||||
if image is None: image = load_image(fn_image)
|
||||
# print(f'UI: Buffering texture "{fn_image}"')
|
||||
height,width,depth = len(image),len(image[0]),len(image[0][0])
|
||||
assert depth == 4, 'Expected texture %s to have 4 channels per pixel (RGBA), not %d' % (fn_image, depth)
|
||||
image = reversed(image) # flip image
|
||||
image_flat = [d for r in image for c in r for d in c]
|
||||
buffer = gpu.types.Buffer('FLOAT', (width * height * 4), [v / 255.0 for v in image_flat])
|
||||
gputexture = gpu.types.GPUTexture((width, height), format='RGBA16F', data=buffer)
|
||||
|
||||
load_texture._cache[fn_image] = {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'depth': depth,
|
||||
'texid': None, #texid,
|
||||
'gputexture': gputexture,
|
||||
}
|
||||
return load_texture._cache[fn_image]
|
||||
|
||||
def async_load_image(fn_image, callback):
|
||||
img = load_image(fn_image)
|
||||
callback(img)
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from itertools import dropwhile, zip_longest
|
||||
|
||||
from .ui_core_utilities import UI_Core_Utils
|
||||
from . import ui_settings
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from .blender import tag_redraw_all
|
||||
from .ui_linefitter import LineFitter
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
from .ui_core_utilities import helper_wraptext, convert_token_to_cursor
|
||||
from .fsm import FSM
|
||||
|
||||
from .boundvar import BoundVar
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .drawing import Drawing
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .maths import floor_if_finite, ceil_if_finite
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
|
||||
class UI_Core_Layout:
|
||||
'''
|
||||
layout each block into lines. if a content box of child element is too wide to fit in line and the child
|
||||
is not the only element on the current line, then end current line, start a new line, relayout the child.
|
||||
|
||||
NOTE: this function does not set the final position and size for element.
|
||||
|
||||
through this function, we are calculating and committing to a certain width and height
|
||||
although the parent element might give us something different. if we end up with a
|
||||
different width and height in self.position() below, we will need to improvise by
|
||||
adjusting margin (if bigger) or using scrolling (if smaller)
|
||||
|
||||
TODO: allow for horizontal growth rather than biasing for vertical
|
||||
TODO: handle flex layouts
|
||||
TODO: allow for different line alignments other than top (bottom, baseline)
|
||||
TODO: percent_of (style width, height, etc.) could be of last non-static element or document
|
||||
TODO: position based on bottom-right,etc.
|
||||
|
||||
NOTE: parent ultimately controls layout and viewing area of child, but uses this layout function to "ask"
|
||||
child how much space it would like
|
||||
|
||||
given size might by inf. given can be ignored due to style. constraints applied at end.
|
||||
positioning (with definitive size) should happen
|
||||
|
||||
IMPORTANT: as currently written, this function needs to be able to be run multiple times!
|
||||
DO NOT PREVENT THIS, otherwise layout bugs will occur!
|
||||
'''
|
||||
|
||||
def _layout2(self, **kwargs):
|
||||
if self._defer_clean or not self.is_visible: return
|
||||
|
||||
styles = self._computed_styles
|
||||
display = styles.get('display', 'block')
|
||||
|
||||
layout_fns = {
|
||||
'inline': self._layout_inline,
|
||||
'block': self._layout_block,
|
||||
'table': self._layout_table,
|
||||
'table-row': self._layout_table_row,
|
||||
'table-cell': self._layout_table_cell,
|
||||
}
|
||||
layout = layout_fns.get(display, self._layout_block)
|
||||
layout(*kwargs)
|
||||
|
||||
|
||||
def _layout_inline(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _layout_block(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _layout_table(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _layout_table_row(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _layout_table_cell(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def _layout(self, **kwargs):
|
||||
if not self.is_visible: return
|
||||
if self._defer_clean: return
|
||||
|
||||
# linefitter = kwargs['linefitter']
|
||||
|
||||
fitting_size = kwargs.get('fitting_size', None) # size from parent that we should try to fit in (only max)
|
||||
fitting_pos = kwargs.get('fitting_pos', None) # top-left position wrt parent where we go if not absolute or fixed
|
||||
parent_size = kwargs.get('parent_size', None) # size of inside of parent
|
||||
nonstatic_elem = kwargs.get('nonstatic_elem', None) # last non-static element
|
||||
tabled = kwargs.get('table_data', {}) # data structure for current table (could be empty)
|
||||
table_elem = tabled.get('element', None) # parent table element
|
||||
table_index2D = tabled.get('index2D', None) # current position in table (i=row,j=col)
|
||||
table_cells = tabled.get('cells', None) # cells of table as tuples (element, size)
|
||||
|
||||
styles = self._computed_styles
|
||||
style_pos = styles.get('position', 'static')
|
||||
|
||||
self._fitting_pos = fitting_pos
|
||||
self._fitting_size = fitting_size
|
||||
self._parent_size = parent_size
|
||||
self._absolute_pos = None
|
||||
self._nonstatic_elem = nonstatic_elem
|
||||
self._tablecell_table = None
|
||||
self._tablecell_pos = None
|
||||
self._tablecell_size = None
|
||||
|
||||
self.update_position()
|
||||
|
||||
if not self._dirtying_flow and not self._dirtying_children_flow and not tabled:
|
||||
return
|
||||
|
||||
if ui_settings.DEBUG_LIST:
|
||||
self._debug_list.append(f'{time.ctime()} layout self={self._dirtying_flow} children={self._dirtying_children_flow} fitting_size={fitting_size}')
|
||||
|
||||
if self._dirtying_children_flow:
|
||||
for child in self._children_all:
|
||||
child.dirty_flow(parent=False)
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' reflowing children')
|
||||
self._dirtying_children_flow = False
|
||||
|
||||
self._all_lines = None
|
||||
|
||||
self._clean_debugging['layout'] = time.time()
|
||||
|
||||
dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
display = styles.get('display', 'block')
|
||||
is_nonstatic = style_pos in {'absolute','relative','fixed','sticky'}
|
||||
is_contribute = style_pos not in {'absolute', 'fixed'}
|
||||
next_nonstatic_elem = self if is_nonstatic else nonstatic_elem
|
||||
parent_width = parent_size.get_width_midmaxmin() or 0
|
||||
parent_height = parent_size.get_height_midmaxmin() or 0
|
||||
# --> NOTE: width,height,min_*,max_* could be 'auto'!
|
||||
width = self._get_style_num('width', def_v='auto', percent_of=parent_width, scale=dpi_mult) # override_v=self._style_width)
|
||||
height = self._get_style_num('height', def_v='auto', percent_of=parent_height, scale=dpi_mult) # override_v=self._style_height)
|
||||
min_width = self._get_style_num('min-width', def_v='auto', percent_of=parent_width, scale=dpi_mult)
|
||||
min_height = self._get_style_num('min-height', def_v='auto', percent_of=parent_height, scale=dpi_mult)
|
||||
max_width = self._get_style_num('max-width', def_v='auto', percent_of=parent_width, scale=dpi_mult)
|
||||
max_height = self._get_style_num('max-height', def_v='auto', percent_of=parent_height, scale=dpi_mult)
|
||||
overflow_x = styles.get('overflow-x', 'visible')
|
||||
overflow_y = styles.get('overflow-y', 'visible')
|
||||
|
||||
# border_width = self._get_style_num('border-width', 0, scale=dpi_mult)
|
||||
# margin_top, margin_right, margin_bottom, margin_left = self._get_style_trbl('margin', scale=dpi_mult)
|
||||
# padding_top, padding_right, padding_bottom, padding_left = self._get_style_trbl('padding', scale=dpi_mult)
|
||||
sc = self._style_cache
|
||||
margin_top, margin_right, margin_bottom, margin_left = sc['margin-top'], sc['margin-right'], sc['margin-bottom'], sc['margin-left']
|
||||
padding_top, padding_right, padding_bottom, padding_left = sc['padding-top'], sc['padding-right'], sc['padding-bottom'], sc['padding-left']
|
||||
border_width = sc['border-width']
|
||||
mbp_left = (margin_left + border_width + padding_left)
|
||||
mbp_right = (padding_right + border_width + margin_right)
|
||||
mbp_top = (margin_top + border_width + padding_top)
|
||||
mbp_bottom = (padding_bottom + border_width + margin_bottom)
|
||||
mbp_width = mbp_left + mbp_right
|
||||
mbp_height = mbp_top + mbp_bottom
|
||||
|
||||
self._mbp_left = mbp_left
|
||||
self._mbp_top = mbp_top
|
||||
self._mbp_right = mbp_right
|
||||
self._mbp_bottom = mbp_bottom
|
||||
self._mbp_width = mbp_width
|
||||
self._mbp_height = mbp_height
|
||||
|
||||
self._computed_min_width = min_width
|
||||
self._computed_min_height = min_height
|
||||
self._computed_max_width = max_width
|
||||
self._computed_max_height = max_height
|
||||
|
||||
inside_size = Size2D()
|
||||
if fitting_size.max_width is not None: inside_size.max_width = max(0, fitting_size.max_width - mbp_width)
|
||||
if fitting_size.max_height is not None: inside_size.max_height = max(0, fitting_size.max_height - mbp_height)
|
||||
if width != 'auto': inside_size.width = max(0, width - mbp_width)
|
||||
if height != 'auto': inside_size.height = max(0, height - mbp_height)
|
||||
if max_width != 'auto': inside_size.max_width = max(0, max_width - mbp_width)
|
||||
if max_height != 'auto': inside_size.max_height = max(0, max_height - mbp_height)
|
||||
if min_width != 'auto': inside_size.min_width = max(0, min_width - mbp_width)
|
||||
if min_height != 'auto': inside_size.min_height = max(0, min_height - mbp_height)
|
||||
|
||||
inside_size.width = floor_if_finite(inside_size.width)
|
||||
inside_size.height = floor_if_finite(inside_size.height)
|
||||
inside_size.max_width = floor_if_finite(inside_size.max_width)
|
||||
inside_size.max_height = floor_if_finite(inside_size.max_height)
|
||||
inside_size.min_width = floor_if_finite(inside_size.min_width)
|
||||
inside_size.min_height = floor_if_finite(inside_size.min_height)
|
||||
|
||||
dw, dh = 0, 0
|
||||
|
||||
if self._static_content_size:
|
||||
# self has static content size: images and text blocks
|
||||
|
||||
dw, dh = self._static_content_size.size
|
||||
|
||||
if self._src in {'image' ,'image loading'}:
|
||||
def scale_dw_dh(num, den):
|
||||
nonlocal dw,dh
|
||||
sc = 0 if den == 0 else num / den
|
||||
dw, dh = dw*sc, dh*sc
|
||||
# image will scale based on inside_size
|
||||
if inside_size.max_width is not None and dw > inside_size.max_width: scale_dw_dh(inside_size.max_width, dw)
|
||||
if inside_size.width is not None: scale_dw_dh(inside_size.width, dw)
|
||||
if inside_size.min_width is not None and dw < inside_size.min_width: scale_dw_dh(inside_size.min_width, dw)
|
||||
if inside_size.max_height is not None and dw > inside_size.max_height: scale_dw_dh(inside_size.max_height, dh)
|
||||
if inside_size.height is not None: scale_dw_dh(inside_size.height, dh)
|
||||
if inside_size.min_height is not None and dw < inside_size.min_height: scale_dw_dh(inside_size.min_height, dh)
|
||||
|
||||
elif self._blocks:
|
||||
# self has no static content, so flow and size is determined from children
|
||||
# note: will keep track of accumulated size and possibly update inside size as needed
|
||||
# note: style size overrides passed fitting size
|
||||
|
||||
# print(f'{self} {self._blocks}')
|
||||
|
||||
if self._innerText is not None and self._whitespace in {'nowrap', 'pre'}:
|
||||
inside_size.min_width = inside_size.width = inside_size.max_width = float('inf')
|
||||
|
||||
if display == 'table':
|
||||
table_elem = self
|
||||
table_index2D = Index2D(0, 0)
|
||||
table_cells = {}
|
||||
tabled = { 'elem': table_elem, 'index2D': table_index2D, 'cells': table_cells }
|
||||
|
||||
working_width = (inside_size.width if inside_size.width is not None else (inside_size.max_width if inside_size.max_width is not None else float('inf')))
|
||||
working_height = (inside_size.height if inside_size.height is not None else (inside_size.max_height if inside_size.max_height is not None else float('inf')))
|
||||
if overflow_y in {'scroll', 'auto'}: working_height = float('inf')
|
||||
|
||||
def flatten(block):
|
||||
if type(block) is list: return [element for e in block for element in flatten(e)]
|
||||
# assuming block is UI_Element
|
||||
if block._pseudoelement == 'text': return flatten(block._children_all)
|
||||
display = block._computed_styles.get('display', 'block')
|
||||
if display == 'none': return []
|
||||
if display in {'block', 'table', 'table-row', 'table-cell'}: return [block]
|
||||
if display in {'inline'}: return flatten(block._children_all)
|
||||
# print(f'flatten {self} {display}')
|
||||
return [block]
|
||||
|
||||
# fitter = LineFitter(left=mbp_left, top=mbp_top, width=working_width, height=working_height)
|
||||
|
||||
accum_lines, accum_width, accum_height = [], 0, 0
|
||||
# accum_width: max width for all lines; accum_height: sum heights for all lines
|
||||
cur_line, cur_width, cur_height = [], 0, 0
|
||||
for block in self._blocks:
|
||||
# each block might be wrapped onto multiple lines
|
||||
cur_line, cur_width, cur_height = [], 0, 0
|
||||
for element in block:
|
||||
if not element.is_visible: continue
|
||||
position = element._computed_styles.get('position', 'static')
|
||||
c = position not in {'absolute', 'fixed'}
|
||||
sx = element._computed_styles.get('overflow-x', 'visible')
|
||||
sy = element._computed_styles.get('overflow-y', 'visible')
|
||||
while True:
|
||||
rw, rh = working_width - cur_width, working_height - accum_height
|
||||
remaining = Size2D(max_width=rw, max_height=rh)
|
||||
pos = Point2D((mbp_left + cur_width, -(mbp_top + accum_height)))
|
||||
element._layout(
|
||||
# linefitter=fitter,
|
||||
fitting_size=remaining,
|
||||
fitting_pos=pos,
|
||||
parent_size=inside_size,
|
||||
nonstatic_elem=next_nonstatic_elem,
|
||||
table_data=tabled,
|
||||
)
|
||||
w, h = math.ceil(element._dynamic_full_size.width), math.ceil(element._dynamic_full_size.height)
|
||||
element_fits = False
|
||||
element_fits |= not cur_line # always add child to an empty line
|
||||
element_fits |= c and w<=rw and h<=rh # child fits on current line
|
||||
element_fits |= not c # child does not contribute to our size
|
||||
element_fits |= self._innerText is not None and self._whitespace in {'nowrap', 'pre'}
|
||||
if element_fits:
|
||||
if c:
|
||||
cur_line.append(element)
|
||||
#cur_line.extend(flatten(element))
|
||||
# clamp width and height only if scrolling (respectively)
|
||||
if sx == 'scroll': w = remaining.clamp_width(w)
|
||||
if sy == 'scroll': h = remaining.clamp_height(h)
|
||||
w, h = math.ceil(w), math.ceil(h)
|
||||
sz = Size2D(width=w, height=h)
|
||||
element.set_view_size(sz)
|
||||
if position != 'fixed':
|
||||
cur_width += w
|
||||
cur_height = max(cur_height, h)
|
||||
break # done processing current element
|
||||
else:
|
||||
# element does not fit! finish of current line, then reprocess current element
|
||||
accum_lines.append((cur_line, cur_width, cur_height))
|
||||
accum_height += cur_height
|
||||
accum_width = max(accum_width, cur_width)
|
||||
cur_line, cur_width, cur_height = [], 0, 0
|
||||
element.dirty_flow(parent=False, children=True)
|
||||
if cur_line:
|
||||
accum_lines.append((cur_line, cur_width, cur_height))
|
||||
accum_height += cur_height
|
||||
accum_width = max(accum_width, cur_width)
|
||||
self._all_lines = accum_lines
|
||||
dw = accum_width
|
||||
dh = accum_height
|
||||
# print(f'{self}:')
|
||||
# for l,w,h in accum_lines:
|
||||
# print(f' {len(l)} {l}')
|
||||
|
||||
# possibly override with text size
|
||||
if self._children_text_min_size:
|
||||
dw = max(dw, self._children_text_min_size.width)
|
||||
dh = max(dh, self._children_text_min_size.height)
|
||||
|
||||
self._dynamic_content_size = Size2D(width=dw, height=dh)
|
||||
|
||||
dw += mbp_width
|
||||
dh += mbp_height
|
||||
|
||||
# override with style settings
|
||||
if width != 'auto': dw = width
|
||||
if height != 'auto': dh = height
|
||||
if min_width != 'auto': dw = max(min_width, dw)
|
||||
if min_height != 'auto': dh = max(min_height, dh)
|
||||
if max_width != 'auto': dw = min(max_width, dw)
|
||||
if max_height != 'auto': dh = min(max_height, dh)
|
||||
|
||||
dw = math.ceil(dw) if math.isfinite(dw) else 100000
|
||||
dh = math.ceil(dh) if math.isfinite(dh) else 100000
|
||||
|
||||
self._dynamic_full_size = Size2D(width=dw, height=dh)
|
||||
|
||||
|
||||
# handle table elements
|
||||
if display == 'table-row':
|
||||
table_index2D.update(i=0, j_off=1)
|
||||
|
||||
elif display == 'table-cell':
|
||||
idx = table_index2D.to_tuple()
|
||||
table_cells[idx] = (self, self._dynamic_full_size)
|
||||
table_index2D.update(i_off=1)
|
||||
|
||||
elif display == 'table':
|
||||
inds = table_cells.keys()
|
||||
ind_is = sorted({ i for (i,j) in inds })
|
||||
ind_js = sorted({ j for (i,j) in inds })
|
||||
ind_is_js = { i:sorted({ j for (_i,j) in inds if i==_i }) for i in ind_is }
|
||||
ind_js_is = { j:sorted({ i for (i,_j) in inds if j==_j }) for j in ind_js }
|
||||
ws = { i:max(table_cells[(i,j)][1].width for j in ind_is_js[i]) for i in ind_is }
|
||||
hs = { j:max(table_cells[(i,j)][1].height for i in ind_js_is[j]) for j in ind_js }
|
||||
# override dynamic full size
|
||||
px,py = mbp_left,mbp_top
|
||||
for i in ind_is:
|
||||
for j in ind_is_js[i]:
|
||||
element = table_cells[(i,j)][0]
|
||||
element._tablecell_table = self
|
||||
element._tablecell_pos = RelPoint2D((px, -py))
|
||||
element._tablecell_size = Size2D(width=ws[i], height=hs[j])
|
||||
py += hs[j]
|
||||
px += ws[i]
|
||||
py = mbp_top
|
||||
fw = sum(ws.values())
|
||||
fh = sum(hs.values())
|
||||
self._dynamic_content_size = Size2D(width=fw, height=fh)
|
||||
self._dynamic_full_size = Size2D(width=math.ceil(fw+mbp_width), height=math.ceil(fh+mbp_height))
|
||||
|
||||
|
||||
# reposition
|
||||
self.update_position()
|
||||
|
||||
|
||||
# position all absolute positioned children
|
||||
for element in self._blocks_abs:
|
||||
if not element.is_visible: continue
|
||||
position = element._computed_styles.get('position', 'static')
|
||||
if position == 'absolute':
|
||||
# fitting_size = Size2D(max_width=self._dynamic_content_size.width, max_height=self._dynamic_content_size.height)
|
||||
fitting_size = Size2D(max_width=float('inf'), max_height=float('inf'))
|
||||
parent_size = self._dynamic_full_size
|
||||
elif position == 'fixed':
|
||||
fitting_size = Size2D(max_width=self._document.body._dynamic_content_size.width, max_height=self._document.body._dynamic_content_size.height)
|
||||
parent_size = self._document.body._dynamic_full_size
|
||||
element._layout(
|
||||
# linefitter=LineFitter(),
|
||||
fitting_size=fitting_size,
|
||||
fitting_pos=Point2D((0, 0)),
|
||||
parent_size=parent_size,
|
||||
nonstatic_elem=next_nonstatic_elem,
|
||||
table_data={},
|
||||
)
|
||||
w, h = math.ceil(element._dynamic_full_size.width), math.ceil(element._dynamic_full_size.height)
|
||||
sz = Size2D(width=w, height=h)
|
||||
element.set_view_size(sz)
|
||||
|
||||
self._dirtying_flow = False
|
||||
self._dirtying_children_flow = False
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def update_position(self):
|
||||
styles = self._computed_styles
|
||||
style_pos = styles.get('position', 'static')
|
||||
pl,pt = self.left_pixels,self.top_pixels
|
||||
dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
|
||||
# cache elements to determine if anything changed
|
||||
relative_element = self._relative_element
|
||||
relative_pos = self._relative_pos
|
||||
relative_offset = self._relative_offset
|
||||
|
||||
# position element
|
||||
if self._tablecell_table:
|
||||
relative_element = self._tablecell_table
|
||||
relative_pos = RelPoint2D(self._tablecell_pos)
|
||||
relative_offset = RelPoint2D((0, 0))
|
||||
|
||||
elif style_pos in {'fixed', 'absolute'}:
|
||||
relative_element = self._document.body if style_pos == 'fixed' else self._nonstatic_elem
|
||||
if relative_element is None or relative_element == self:
|
||||
mbp_left = mbp_top = 0
|
||||
else:
|
||||
mbp_left = relative_element._mbp_left
|
||||
mbp_top = relative_element._mbp_top
|
||||
if pl == 'auto': pl = 0
|
||||
if pt == 'auto': pt = 0
|
||||
if relative_element and relative_element != self and self._clamp_to_parent:
|
||||
parent_width = (relative_element._dynamic_full_size or self._parent_size).get_width_midmaxmin() or 0
|
||||
parent_height = (relative_element._dynamic_full_size or self._parent_size).get_height_midmaxmin() or 0
|
||||
width = self._get_style_num('width', def_v='auto', percent_of=parent_width, scale=dpi_mult)
|
||||
height = self._get_style_num('height', def_v='auto', percent_of=parent_height, scale=dpi_mult)
|
||||
w = width if width != 'auto' else (self.width_pixels if self.width_pixels != 'auto' else 0)
|
||||
h = height if height != 'auto' else (self.height_pixels if self.height_pixels != 'auto' else 0)
|
||||
pl = clamp(pl, 0, relative_element.width_pixels - relative_element._mbp_width - w)
|
||||
pt = clamp(pt, -(relative_element.height_pixels - relative_element._mbp_height - h), 0)
|
||||
# pt = clamp(pt, h + relative_element._mbp_bottom, relative_element.height_pixels - relative_element._mbp_top)
|
||||
relative_pos = RelPoint2D((pl, pt))
|
||||
relative_offset = RelPoint2D((mbp_left, -mbp_top))
|
||||
|
||||
elif style_pos == 'relative':
|
||||
if pl == 'auto': pl = 0
|
||||
if pt == 'auto': pt = 0
|
||||
relative_element = self._parent
|
||||
relative_pos = RelPoint2D(self._fitting_pos)
|
||||
relative_offset = RelPoint2D((pl, pt))
|
||||
|
||||
else:
|
||||
relative_element = self._parent
|
||||
relative_pos = RelPoint2D(self._fitting_pos)
|
||||
relative_offset = RelPoint2D((0, 0))
|
||||
|
||||
# has anything changed?
|
||||
changed = False
|
||||
changed |= relative_element != self._relative_element
|
||||
changed |= relative_pos != self._relative_pos
|
||||
changed |= relative_offset != self._relative_offset
|
||||
if changed:
|
||||
self._relative_element = relative_element
|
||||
self._relative_pos = relative_pos
|
||||
self._relative_offset = relative_offset
|
||||
self._alignment_offset = None
|
||||
self.dirty_renderbuf(cause='position changed')
|
||||
|
||||
|
||||
# @profiler.function
|
||||
def set_view_size(self, size:Size2D):
|
||||
# parent is telling us how big we will be. note: this does not trigger a reflow!
|
||||
# TODO: clamp scroll
|
||||
# TODO: handle vertical and horizontal element alignment
|
||||
# TODO: handle justified and right text alignment
|
||||
if self.width_override is not None or self.height_override is not None:
|
||||
size = size.clone()
|
||||
if self.width_override is not None: size.set_all_widths( self.width_override)
|
||||
if self.height_override is not None: size.set_all_heights(self.height_override)
|
||||
self._absolute_size = size
|
||||
self.scrollLeft = self.scrollLeft
|
||||
self.scrollTop = self.scrollTop
|
||||
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} set_view_size({size})')
|
||||
|
||||
if self._all_lines:
|
||||
w = size.width - self._mbp_width
|
||||
nlines = len(self._all_lines)
|
||||
align = self._computed_styles.get("text-align", "left")
|
||||
for i_line, (line, line_width, line_height) in enumerate(self._all_lines):
|
||||
if i_line == nlines - 1:
|
||||
# override justify text alignment, unless CSS explicitly specifies
|
||||
align = self._computed_styles.get("text-align-last", 'left' if align == 'justify' else align)
|
||||
|
||||
offset_x, offset_between = 0, 0
|
||||
if align == 'right': offset_x = w - line_width
|
||||
elif align == 'center': offset_x = (w - line_width) / 2
|
||||
elif align == 'justify': offset_between = (w - line_width) / len(line)
|
||||
#if offset_x <= 0 and offset_between <= 0: continue
|
||||
offset_x = Vec2D((offset_x, 0))
|
||||
offset_between = Vec2D((offset_between, 0))
|
||||
for i,el in enumerate(line):
|
||||
el._alignment_offset = offset_x + offset_between * i
|
||||
|
||||
#if self._src_str:
|
||||
# print(self._src_str, self._dynamic_full_size, self._dynamic_content_size, self._absolute_size)
|
||||
|
||||
# @UI_Core_Utils.add_option_callback('layout:flexbox')
|
||||
# def layout_flexbox(self):
|
||||
# style = self._computed_styles
|
||||
# direction = style.get('flex-direction', 'row')
|
||||
# wrap = style.get('flex-wrap', 'nowrap')
|
||||
# justify = style.get('justify-content', 'flex-start')
|
||||
# align_items = style.get('align-items', 'flex-start')
|
||||
# align_content = style.get('align-content', 'flex-start')
|
||||
|
||||
# @UI_Core_Utils.add_option_callback('layout:block')
|
||||
# def layout_block(self):
|
||||
# pass
|
||||
|
||||
# @UI_Core_Utils.add_option_callback('layout:inline')
|
||||
# def layout_inline(self):
|
||||
# pass
|
||||
|
||||
# @UI_Core_Utils.add_option_callback('layout:none')
|
||||
# def layout_none(self):
|
||||
# pass
|
||||
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import time
|
||||
import types
|
||||
import codecs
|
||||
import struct
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import functools
|
||||
import urllib.request
|
||||
from itertools import chain
|
||||
|
||||
import bpy
|
||||
|
||||
from .ui_core_utilities import UIRender_Block, UIRender_Inline
|
||||
from .utils import kwargopts, kwargs_translate, kwargs_splitter, iter_head
|
||||
from .ui_styling import UI_Styling
|
||||
|
||||
from .blender import get_path_from_addon_root, get_path_from_addon_common
|
||||
from .boundvar import BoundVar, BoundFloat, BoundInt, BoundString, BoundStringToBool, BoundBool
|
||||
from .decorators import blender_version_wrapper
|
||||
from .globals import Globals
|
||||
from .maths import Point2D, Vec2D, clamp, mid, Color, Box2D, Size2D, NumberUnit
|
||||
from .markdown import Markdown
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import Dict, delay_exec, get_and_discard, strshort
|
||||
from . import html_to_unicode
|
||||
|
||||
|
||||
'''
|
||||
Notes about addon_common's UI system
|
||||
|
||||
- The system is designed similarly to how the Browser will render HTML+CSS
|
||||
- All UI elements are containers
|
||||
- All classes herein are simply "starter" UI elements
|
||||
- You can freely change all properties to make any element turn into another
|
||||
- Styling
|
||||
- Styling specified here is base styling for UI elements of same type
|
||||
- Base styling specified here are overridden by stylesheet, which is overridden by custom styling
|
||||
- Note: changing tagname will not reset the base styling. in other words, if the element starts off
|
||||
as a UI_Button, changing tagname to "flexbox" will not change base styling from what is
|
||||
specified in UI_Button.
|
||||
|
||||
|
||||
Implementation details
|
||||
|
||||
- root element will be sized to entire 3D view region
|
||||
- each element
|
||||
- is responsible for communicating with children
|
||||
- will estimate its size (min, max, preferred), but these are only suggestions for the parent
|
||||
- dictates size and position of its children
|
||||
- must submit to the sizing and position given by the parent
|
||||
|
||||
See top comment in `ui_core_utilities.py` for links to useful resources.
|
||||
'''
|
||||
|
||||
|
||||
def get_mdown_path(fn, ext=None, subfolders=None):
|
||||
# if no subfolders are given, assuming image path is <root>/icons
|
||||
# or <root>/images where <root> is the 2 levels above this file
|
||||
if subfolders is None:
|
||||
subfolders = ['help']
|
||||
if ext: fn = f'{fn}.{ext}'
|
||||
paths = [get_path_from_addon_root(subfolder, fn) for subfolder in subfolders]
|
||||
paths += [get_path_from_addon_common('common', 'images', fn)]
|
||||
paths = [p for p in paths if os.path.exists(p)]
|
||||
return iter_head(paths, default=None)
|
||||
|
||||
def load_text_file(path):
|
||||
try: return open(path, 'rt').read()
|
||||
except: pass
|
||||
try: return codecs.open(path, encoding='utf-8').read()
|
||||
except: pass
|
||||
try: return codecs.open(path, encoding='utf-16').read()
|
||||
except Exception as e:
|
||||
print('Could not load text file:', path)
|
||||
print('Exception:', e)
|
||||
assert False
|
||||
|
||||
|
||||
class UI_Core_Markdown:
|
||||
# @profiler.function
|
||||
def set_markdown(self, mdown=None, *, mdown_path=None, preprocess_fns=None, f_globals=None, f_locals=None, frame_depth=1, frames_deep=1, remove_indentation=True, **kwargs):
|
||||
if f_globals and f_locals:
|
||||
f_globals = f_globals
|
||||
f_locals = dict(f_locals)
|
||||
else:
|
||||
ff_globals, ff_locals = {}, {}
|
||||
frame = inspect.currentframe()
|
||||
for i in range(frame_depth + frames_deep):
|
||||
if i >= frame_depth:
|
||||
ff_globals = frame.f_globals | ff_globals
|
||||
ff_locals = frame.f_locals | ff_locals
|
||||
frame = frame.f_back
|
||||
f_globals = f_globals or ff_globals
|
||||
f_locals = dict(f_locals or ff_locals)
|
||||
f_locals |= kwargs
|
||||
|
||||
# if f_globals is None or f_locals is None:
|
||||
# frame = inspect.currentframe() # get frame of calling function
|
||||
# for _ in range(frame_depth): frame = frame.f_back
|
||||
# if f_globals is None: f_globals = frame.f_globals # get globals of calling function
|
||||
# if f_locals is None: f_locals = frame.f_locals # get locals of calling function
|
||||
|
||||
self._src_mdown_path = mdown_path or ''
|
||||
|
||||
if mdown_path:
|
||||
mdown = load_text_file(get_mdown_path(mdown_path))
|
||||
if remove_indentation and mdown:
|
||||
indent = min((
|
||||
len(line) - len(line.lstrip())
|
||||
for line in mdown.splitlines()
|
||||
if line.strip()
|
||||
), default=0)
|
||||
mdown = '\n'.join(
|
||||
line if not line.strip() else line[indent:]
|
||||
for line in mdown.splitlines()
|
||||
)
|
||||
if preprocess_fns:
|
||||
for preprocess_fn in preprocess_fns:
|
||||
mdown = preprocess_fn(mdown)
|
||||
mdown = Markdown.preprocess(mdown or '') # preprocess mdown
|
||||
if getattr(self, '__mdown', None) == mdown: return # ignore updating if it's exactly the same as previous
|
||||
self.__mdown = mdown # record the mdown to prevent reprocessing same
|
||||
|
||||
def process_words(text, word_fn):
|
||||
build = ''
|
||||
while text:
|
||||
word,text = Markdown.split_word(text)
|
||||
build += word
|
||||
#word_fn(word)
|
||||
word_fn(build)
|
||||
|
||||
def process_para(container, para, **kwargs):
|
||||
with container.defer_dirty('creating new children'):
|
||||
opts = kwargopts(kwargs, classes='')
|
||||
|
||||
# break each ui_item onto it's own line
|
||||
para = re.sub(r'\n', ' ', para) # join sentences of paragraph
|
||||
para = re.sub(r' +', ' ', para) # 1+ spaces => 1 space
|
||||
|
||||
# TODO: revisit this, and create an actual parser
|
||||
para = para.lstrip()
|
||||
while para:
|
||||
t,m = Markdown.match_inline(para)
|
||||
match t:
|
||||
case None:
|
||||
build = ''
|
||||
while t is None and para:
|
||||
word,para = Markdown.split_word(para)
|
||||
build += word
|
||||
t,m = Markdown.match_inline(para)
|
||||
container.append_new_child(tagName='text', innerText=build, pseudoelement='text')
|
||||
continue
|
||||
|
||||
case 'br':
|
||||
container.append_new_child(tagName='BR')
|
||||
|
||||
case 'arrow':
|
||||
d = html_to_unicode.arrows[f"&{m.group('dir')};"]
|
||||
container.append_new_child(tagName='span', classes='html-arrow', innerText=f'{d}')
|
||||
|
||||
case 'img':
|
||||
style = m.group('style').strip() or None
|
||||
container.append_new_child(tagName='img', classes='inline', style=style, src=m.group('filename'), title=m.group('caption'))
|
||||
|
||||
case 'code':
|
||||
container.append_new_child(tagName='code', innerText=m.group('text'))
|
||||
|
||||
case 'link':
|
||||
link = m.group('link')
|
||||
title = 'Click to open URL in default web browser' if Markdown.is_url(link) else 'Click to open help'
|
||||
def mouseclick():
|
||||
if Markdown.is_url(link):
|
||||
bpy.ops.wm.url_open(url=link)
|
||||
else:
|
||||
self.set_markdown(mdown_path=link, preprocess_fns=preprocess_fns, f_globals=f_globals, f_locals=f_locals)
|
||||
process_words(m.group('text'), lambda word: container.append_new_child(tagName='a', innerText=word, href=link, title=title, on_mouseclick=mouseclick))
|
||||
|
||||
case 'bold':
|
||||
process_words(m.group('text'), lambda word: container.append_new_child(tagName='b', innerText=word))
|
||||
|
||||
case 'italic':
|
||||
process_words(m.group('text'), lambda word: container.append_new_child(tagName='i', innerText=word))
|
||||
|
||||
case 'html':
|
||||
ui = container.append_new_children_fromHTML(m.group(), f_globals=f_globals, f_locals=f_locals)
|
||||
|
||||
case _:
|
||||
assert False, f'Unhandled inline markdown type "{t}" ("{m}") with "{line}"'
|
||||
|
||||
para = para[m.end():]
|
||||
|
||||
# case 'checkbox':
|
||||
# params = m.group('params')
|
||||
# innertext = m.group('innertext')
|
||||
# value = None
|
||||
# for param in re.finditer(r'(?P<key>[a-zA-Z]+)(="(?P<val>.*?)")?', params):
|
||||
# key = param.group('key')
|
||||
# val = param.group('val')
|
||||
# if key == 'type':
|
||||
# pass
|
||||
# elif key == 'value':
|
||||
# value = val
|
||||
# else:
|
||||
# assert False, 'Unhandled checkbox parameter key="%s", val="%s" (%s)' % (key,val,param)
|
||||
# assert value is not None, 'Unhandled checkbox parameters: expected value (%s)' % (params)
|
||||
# # print('CREATING input_checkbox(label="%s", checked=BoundVar("%s", ...)' % (innertext, value))
|
||||
# ui_label = container.append_new_child(tagName='label')
|
||||
# ui_label.append_new_child(tagName='input', type='checkbox', checked=BoundVar(value, f_globals=f_globals, f_locals=f_locals))
|
||||
# ui_label.append_new_child(tagName='text', innerText=innertext, pseudoelement='text')
|
||||
# case 'button':
|
||||
# ui_element = self.fromHTML(m.group(0), f_globals=f_globals, f_locals=f_locals)[0]
|
||||
# container.append_child(ui_element)
|
||||
# case 'progress':
|
||||
# ui_element = self.fromHTML(m.group(0), f_globals=f_globals, f_locals=f_locals)[0]
|
||||
# container.append_child(ui_element)
|
||||
|
||||
def process_mdown(ui_container, mdown):
|
||||
#paras = mdown.split('\n\n') # split into paragraphs
|
||||
paras = re.split(r'\n\n(?! )', mdown)
|
||||
for para in paras:
|
||||
t,m = Markdown.match_line(para)
|
||||
|
||||
match t:
|
||||
case None:
|
||||
p_element = ui_container.append_new_child(tagName='p')
|
||||
process_para(p_element, para)
|
||||
|
||||
case 'h1' | 'h2' | 'h3':
|
||||
ui_hn = ui_container.append_new_child(tagName=t)
|
||||
process_para(ui_hn, m.group('text'))
|
||||
|
||||
case 'ul':
|
||||
ui_ul = ui_container.append_new_child(tagName='ul')
|
||||
with ui_ul.defer_dirty('creating ul children'):
|
||||
# add newline at beginning so that we can skip the first item (before "- ")
|
||||
skip_first = True
|
||||
para = f'\n{para}'
|
||||
for litext in re.split(r'\n- ', para):
|
||||
if skip_first:
|
||||
skip_first = False
|
||||
continue
|
||||
ui_li = ui_ul.append_new_child(tagName='li')
|
||||
if '\n' in litext:
|
||||
# add extra newline for nested ul
|
||||
if '\n - ' in litext:
|
||||
idx = litext.index('\n - ')
|
||||
litext = litext[:idx] + '\n' + litext[idx:]
|
||||
# remove leading spaces
|
||||
litext = '\n'.join(l.lstrip() for l in litext.split('\n'))
|
||||
process_mdown(ui_li, litext)
|
||||
else:
|
||||
process_para(ui_li, litext)
|
||||
|
||||
case 'ol':
|
||||
ui_ol = ui_container.append_new_child(tagName='ol')
|
||||
with ui_ol.defer_dirty('creating ol children'):
|
||||
# add newline at beginning so that we can skip the first item (before "- ")
|
||||
skip_first = True
|
||||
para = f'\n{para}'
|
||||
for ili,litext in enumerate(re.split(r'\n\d+\. ', para)):
|
||||
if skip_first:
|
||||
skip_first = False
|
||||
continue
|
||||
ui_li = ui_ol.append_new_child(tagName='li')
|
||||
#ui_li.append_new_child(tagName='span', classes='number', innerText=f'{ili}.')
|
||||
#span_element = ui_li.append_new_child(tagName='span', classes='text')
|
||||
if '\n' in litext:
|
||||
# remove leading spaces
|
||||
litext = '\n'.join(l.strip() for l in litext.split('\n'))
|
||||
process_mdown(ui_li, litext)
|
||||
else:
|
||||
process_para(ui_li, litext)
|
||||
|
||||
case 'img':
|
||||
style = m.group('style').strip() or None
|
||||
ui_container.append_new_child(tagName='img', style=style, src=m.group('filename'), title=m.group('caption'))
|
||||
|
||||
case 'table':
|
||||
# table!
|
||||
def split_row(row):
|
||||
row = re.sub(r'^\| ', r'', row)
|
||||
row = re.sub(r' \|$', r'', row)
|
||||
return [col.strip() for col in row.split(' | ')]
|
||||
data = [l for l in para.split('\n')]
|
||||
header = split_row(data[0])
|
||||
add_header = any(header)
|
||||
align = data[1]
|
||||
data = [split_row(row) for row in data[2:]]
|
||||
rows,cols = len(data),len(data[0])
|
||||
table_element = ui_container.append_new_child(tagName='table')
|
||||
with table_element.defer_dirty('creating table children'):
|
||||
if add_header:
|
||||
tr_element = table_element.append_new_child(tagName='tr')
|
||||
for c in range(cols):
|
||||
tr_element.append_new_child(tagName='th', innerText=header[c])
|
||||
for r in range(rows):
|
||||
tr_element = table_element.append_new_child(tagName='tr')
|
||||
for c in range(cols):
|
||||
td_element = tr_element.append_new_child(tagName='td')
|
||||
process_para(td_element, data[r][c])
|
||||
|
||||
case _:
|
||||
assert False, f'Unhandled markdown line type "{t}" ("{m}") with "{para}"'
|
||||
|
||||
if self._document: self._document.defer_cleaning = True
|
||||
|
||||
self.defer_clean = True
|
||||
with self.defer_dirty('creating new children'):
|
||||
self.clear_children()
|
||||
self.scrollToTop(force=True)
|
||||
process_mdown(self, mdown)
|
||||
if self.parent: self.parent.scrollToTop(force=True)
|
||||
self.defer_clean = False
|
||||
|
||||
if self._document: self._document.defer_cleaning = False
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class UI_Core_PreventMultiCalls:
|
||||
multicalls = {}
|
||||
|
||||
@staticmethod
|
||||
def reset_multicalls():
|
||||
# print(UI_Core_PreventMultiCalls.multicalls)
|
||||
UI_Core_PreventMultiCalls.multicalls = {}
|
||||
|
||||
def record_multicall(self, label):
|
||||
# returns True if already called!
|
||||
d = UI_Core_PreventMultiCalls.multicalls
|
||||
if label not in d: d[label] = { self._uid }
|
||||
elif self._uid not in d[label]: d[label].add(self._uid)
|
||||
else: return True
|
||||
return False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import time
|
||||
|
||||
from . import ui_settings # needs to be first
|
||||
|
||||
from .ui_core_defaults import UI_Core_Defaults
|
||||
from .ui_core_fonts import get_font
|
||||
from .ui_core_utilities import UI_Core_Utils
|
||||
from .ui_draw import ui_draw
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
|
||||
class UI_Core_Style:
|
||||
|
||||
# @profiler.function
|
||||
def _rebuild_style_selector(self):
|
||||
sel_parent = (None if not self._parent else self._parent._selector) or []
|
||||
|
||||
# TEST!!
|
||||
# sel_parent = [re.sub(r':(active|hover)', '', s) for s in sel_parent]
|
||||
|
||||
|
||||
selector_before = None
|
||||
selector_after = None
|
||||
if self._innerTextAsIs is not None:
|
||||
# this is a text element
|
||||
selector = [*sel_parent, '*text*']
|
||||
# elif self._pseudoelement:
|
||||
# # this has a pseudoelement: ::before, ::after, ::marker
|
||||
# selector = [*sel_parent[:-1], f'{sel_parent[-1]}::{self._pseudoelement}']
|
||||
else:
|
||||
ui_for = self.get_for_element()
|
||||
|
||||
attribvals = {}
|
||||
type_val = self.type_with_for(ui_for)
|
||||
if type_val: attribvals['type'] = type_val
|
||||
value_val = self.value_with_for(ui_for)
|
||||
if value_val: attribvals['value'] = value_val
|
||||
name_val = self.name
|
||||
if name_val: attribvals['name'] = name_val
|
||||
|
||||
is_disabled = False
|
||||
is_disabled |= self._value_bound and self._value.disabled
|
||||
is_disabled |= self._checked_bound and self._checked.disabled
|
||||
|
||||
sel_tagName = self._tagName
|
||||
sel_id = f'#{self._id}' if self._id else ''
|
||||
sel_cls = join('.', self._classes, preSep='.')
|
||||
sel_attribs = join('][', attribvals.keys(), preSep='[', postSep=']')
|
||||
sel_attribvals = join('][', attribvals.items(), preSep='[', postSep=']', toStr=lambda kv:f'{kv[0]}="{kv[1]}"')
|
||||
sel_pseudocls = join(':', self.pseudoclasses_with_for(ui_for), preSep=':')
|
||||
sel_pseudoelem = f'::{self._pseudoelement}' if self._pseudoelement else ''
|
||||
if is_disabled:
|
||||
sel_pseudocls += ':disabled'
|
||||
if self.checked_with_for(ui_for):
|
||||
sel_attribs += '[checked]'
|
||||
sel_attribvals += '[checked="checked"]'
|
||||
sel_pseudocls += ':checked'
|
||||
if self.open:
|
||||
sel_attribs += '[open]'
|
||||
|
||||
self_selector = f'{sel_tagName}{sel_id}{sel_cls}{sel_attribs}{sel_attribvals}{sel_pseudocls}{sel_pseudoelem}'
|
||||
if self._pseudoelement not in {None, '', 'text'}:
|
||||
selector = [*sel_parent[:-1], self_selector]
|
||||
else:
|
||||
selector = [*sel_parent, self_selector]
|
||||
#selector_before = sel_parent + [sel_tagName + sel_id + sel_cls + sel_pseudocls + '::before']
|
||||
#selector_after = sel_parent + [sel_tagName + sel_id + sel_cls + sel_pseudocls + '::after']
|
||||
|
||||
# if selector hasn't changed, don't recompute trimmed styling
|
||||
if selector == self._selector and selector_before == self._selector_before and selector_after == self._selector_after:
|
||||
return False
|
||||
styling_trimmed = UI_Styling.trim_styling(selector, ui_defaultstylings, ui_draw.default_stylesheet)
|
||||
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} selector: {" ".join(selector)}')
|
||||
|
||||
self._last_selector = selector
|
||||
self._selector = selector
|
||||
self._selector_before = selector_before
|
||||
self._selector_after = selector_after
|
||||
self._styling_trimmed = styling_trimmed
|
||||
self._style_trbl_cache = {}
|
||||
if self._last_selector and self._last_selector[-1] == self._selector[-1]:
|
||||
self.dirty_style_parent(cause='changing parent selector (possibly) dirties style')
|
||||
else:
|
||||
self.dirty_style(cause='changing selector dirties style')
|
||||
return True
|
||||
|
||||
|
||||
@UI_Core_Utils.add_cleaning_callback('selector', {'style', 'style parent'})
|
||||
# @profiler.function
|
||||
def _compute_selector(self):
|
||||
if self.defer_clean: return
|
||||
if 'selector' not in self._dirty_properties:
|
||||
self.defer_clean = True
|
||||
if True: # with profiler.code('selector.calling back callbacks'):
|
||||
for e in list(self._dirty_callbacks.get('selector', [])):
|
||||
# print(self,'->', e)
|
||||
e._compute_selector()
|
||||
self._dirty_callbacks['selector'].clear()
|
||||
self.defer_clean = False
|
||||
return
|
||||
|
||||
self._clean_debugging['selector'] = time.time()
|
||||
self._rebuild_style_selector()
|
||||
if self._children:
|
||||
for child in self._children: child._compute_selector()
|
||||
# if self._children_text:
|
||||
# for child in self._children_text: child._compute_selector()
|
||||
if self._children_gen:
|
||||
for child in self._children_gen: child._compute_selector()
|
||||
# if self._child_before or self._child_after:
|
||||
# if self._child_before: self._child_before._compute_selector()
|
||||
# if self._child_after: self._child_after._compute_selector()
|
||||
self._dirty_properties.discard('selector')
|
||||
self._dirty_callbacks['selector'].clear()
|
||||
|
||||
|
||||
@UI_Core_Utils.add_cleaning_callback('style', {'size', 'content', 'renderbuf'})
|
||||
@UI_Core_Utils.add_cleaning_callback('style parent', {'size', 'content', 'renderbuf'})
|
||||
# @profiler.function
|
||||
def _compute_style(self):
|
||||
'''
|
||||
rebuilds self._selector and computes the stylesheet, propagating computation to children
|
||||
|
||||
IMPORTANT: as current written, this function needs to be able to be run multiple times!
|
||||
DO NOT PREVENT THIS, otherwise infinite loop bugs will occur!
|
||||
'''
|
||||
|
||||
if self.defer_clean: return
|
||||
|
||||
if all(p not in self._dirty_properties for p in ['style', 'style parent']):
|
||||
self.defer_clean = True
|
||||
if True: # with profiler.code('style.calling back callbacks'):
|
||||
for e in list(self._dirty_callbacks.get('style', [])):
|
||||
# print(self,'->', e)
|
||||
e._compute_style()
|
||||
for e in list(self._dirty_callbacks.get('style parent', [])):
|
||||
# print(self,'->', e)
|
||||
e._compute_style()
|
||||
self._dirty_callbacks['style'].clear()
|
||||
self._dirty_callbacks['style parent'].clear()
|
||||
self.defer_clean = False
|
||||
return
|
||||
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f'{time.ctime()} style')
|
||||
|
||||
was_visible = self.is_visible
|
||||
self._draw_dirty_style += 1
|
||||
self._clean_debugging['style'] = time.time()
|
||||
|
||||
# self.defer_dirty_propagation = True
|
||||
|
||||
# self._rebuild_style_selector()
|
||||
|
||||
if True: # with profiler.code('style.initialize styles in order: parent, default, custom'):
|
||||
# default, focus, active, hover, hover+active
|
||||
|
||||
# TODO: inherit parent styles with other elements (not just *text*)
|
||||
# if self._styling_parent is None:
|
||||
# if self._parent:
|
||||
# # keep = {
|
||||
# # 'font-family', 'font-style', 'font-weight', 'font-size',
|
||||
# # 'color',
|
||||
# # }
|
||||
# # decllist = {k:v for (k,v) in self._parent._computed_styles.items() if k in keep}
|
||||
# # self._styling_parent = UI_Styling.from_decllist(decllist)
|
||||
# self._styling_parent = None
|
||||
|
||||
# compute custom styles
|
||||
if self._styling_custom is None and self._style_str:
|
||||
self._styling_custom = UI_Styling(f'*{{{self._style_str};}}', inline=True)
|
||||
|
||||
self._styling_list = [
|
||||
self._styling_trimmed,
|
||||
# self._styling_parent,
|
||||
self._styling_custom
|
||||
]
|
||||
self._computed_styles = UI_Styling.compute_style(self._selector, *self._styling_list)
|
||||
|
||||
if True: # with profiler.code('style.filling style cache'):
|
||||
if self._is_visible and not self._pseudoelement:
|
||||
# need to compute ::before and ::after styles to know whether there is content to compute and render
|
||||
self._computed_styles_before = None # UI_Styling.compute_style(self._selector_before, *styling_list)
|
||||
self._computed_styles_after = None # UI_Styling.compute_style(self._selector_after, *styling_list)
|
||||
else:
|
||||
self._computed_styles_before = None
|
||||
self._computed_styles_after = None
|
||||
self._is_scrollable_x = (self._computed_styles.get('overflow-x', 'visible') == 'scroll')
|
||||
self._is_scrollable_y = (self._computed_styles.get('overflow-y', 'visible') == 'scroll')
|
||||
|
||||
dpi_mult = Globals.drawing.get_dpi_mult()
|
||||
self._style_cache = {}
|
||||
sc = self._style_cache
|
||||
if self._innerTextAsIs is None:
|
||||
sc['left'] = self._computed_styles.get('left', 'auto')
|
||||
sc['right'] = self._computed_styles.get('right', 'auto')
|
||||
sc['top'] = self._computed_styles.get('top', 'auto')
|
||||
sc['bottom'] = self._computed_styles.get('bottom', 'auto')
|
||||
sc['margin-top'], sc['margin-right'], sc['margin-bottom'], sc['margin-left'] = self._get_style_trbl('margin', scale=dpi_mult)
|
||||
sc['padding-top'], sc['padding-right'], sc['padding-bottom'], sc['padding-left'] = self._get_style_trbl('padding', scale=dpi_mult)
|
||||
sc['border-width'] = self._get_style_num('border-width', def_v=NumberUnit.zero, scale=dpi_mult)
|
||||
sc['border-radius'] = self._computed_styles.get('border-radius', 0)
|
||||
sc['border-left-color'] = self._computed_styles.get('border-left-color', Color.transparent)
|
||||
sc['border-right-color'] = self._computed_styles.get('border-right-color', Color.transparent)
|
||||
sc['border-top-color'] = self._computed_styles.get('border-top-color', Color.transparent)
|
||||
sc['border-bottom-color'] = self._computed_styles.get('border-bottom-color', Color.transparent)
|
||||
sc['background-color'] = self._computed_styles.get('background-color', Color.transparent)
|
||||
sc['width'] = self._computed_styles.get('width', 'auto')
|
||||
sc['height'] = self._computed_styles.get('height', 'auto')
|
||||
else:
|
||||
sc['left'] = 'auto'
|
||||
sc['right'] = 'auto'
|
||||
sc['top'] = 'auto'
|
||||
sc['bottom'] = 'auto'
|
||||
sc['margin-top'], sc['margin-right'], sc['margin-bottom'], sc['margin-left'] = 0, 0, 0, 0
|
||||
sc['padding-top'], sc['padding-right'], sc['padding-bottom'], sc['padding-left'] = 0, 0, 0, 0
|
||||
sc['border-width'] = 0
|
||||
sc['border-radius'] = 0
|
||||
sc['border-left-color'] = Color.transparent
|
||||
sc['border-right-color'] = Color.transparent
|
||||
sc['border-top-color'] = Color.transparent
|
||||
sc['border-bottom-color'] = Color.transparent
|
||||
sc['background-color'] = Color.transparent
|
||||
sc['width'] = 'auto'
|
||||
sc['height'] = 'auto'
|
||||
|
||||
if self._pseudoelement == 'text':
|
||||
text_styles = self._parent._computed_styles if self._parent else self._computed_styles
|
||||
else:
|
||||
text_styles = self._computed_styles
|
||||
|
||||
self._fontid = get_font(
|
||||
text_styles.get('font-family', UI_Core_Defaults.font_family),
|
||||
text_styles.get('font-style', UI_Core_Defaults.font_style),
|
||||
text_styles.get('font-weight', UI_Core_Defaults.font_weight),
|
||||
)
|
||||
self._fontsize = text_styles.get('font-size', UI_Core_Defaults.font_size).val()
|
||||
self._fontcolor = text_styles.get('color', UI_Core_Defaults.font_color)
|
||||
self._whitespace = text_styles.get('white-space', UI_Core_Defaults.whitespace)
|
||||
ts = text_styles.get('text-shadow', 'none')
|
||||
self._textshadow = None if ts == 'none' else (ts[0].val(), ts[1].val(), ts[-1])
|
||||
|
||||
# tell children to recompute selector
|
||||
# NOTE: self._children_all has not been constructed, yet!
|
||||
if self.is_visible:
|
||||
if self._children:
|
||||
for child in self._children: child._compute_style()
|
||||
# if self._children_text:
|
||||
# for child in self._children_text: child._compute_style()
|
||||
if self._children_gen:
|
||||
for child in self._children_gen: child._compute_style()
|
||||
# if self._child_before or self._child_after:
|
||||
# if self._child_before: self._child_before._compute_style()
|
||||
# if self._child_after: self._child_after._compute_style()
|
||||
|
||||
if True: # with profiler.code('style.hashing for cache'):
|
||||
# style changes => content changes
|
||||
style_content_hash = Hasher(
|
||||
self.is_visible,
|
||||
self.src, # image is loaded in compute_content
|
||||
self.innerText, # innerText => UI_Elements in compute content
|
||||
self._fontid, self._fontsize, self._whitespace, # these properties affect innerText UI_Elements
|
||||
self._computed_styles_before.get('content', None) if self._computed_styles_before else None,
|
||||
self._computed_styles_after.get('content', None) if self._computed_styles_after else None,
|
||||
)
|
||||
if style_content_hash != getattr(self, '_style_content_hash', None) or self._children_gen:
|
||||
self.dirty_content(cause='style change might have changed content (::before / ::after)')
|
||||
self.dirty_renderbuf(cause='style change might have changed content (::before / ::after)')
|
||||
# self.dirty(cause='style change might have changed content (::before / ::after)', properties='content')
|
||||
# self.dirty(cause='style change might have changed content (::before / ::after)', properties='renderbuf')
|
||||
self.dirty_flow(children=False)
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' possible content change')
|
||||
# self._innerTextWrapped = None
|
||||
self._style_content_hash = style_content_hash
|
||||
|
||||
# style changes => size changes
|
||||
style_size_hash = Hasher(
|
||||
self._fontid, self._fontsize, self._whitespace,
|
||||
{k:sc[k] for k in [
|
||||
'left', 'right', 'top', 'bottom',
|
||||
'margin-top','margin-right','margin-bottom','margin-left',
|
||||
'padding-top','padding-right','padding-bottom','padding-left',
|
||||
'border-width',
|
||||
'width', 'height', #'min-width','min-height','max-width','max-height',
|
||||
]},
|
||||
)
|
||||
if style_size_hash != getattr(self, '_style_size_hash', None):
|
||||
self.dirty_size(cause='style change might have changed size')
|
||||
self.dirty_renderbuf(cause='style change might have changed size')
|
||||
self.dirty_flow(children=False)
|
||||
if ui_settings.DEBUG_LIST: self._debug_list.append(f' possible size change')
|
||||
# self._innerTextWrapped = None
|
||||
self._style_size_hash = style_size_hash
|
||||
|
||||
# style changes => render changes
|
||||
style_render_hash = Hasher(
|
||||
self._fontcolor,
|
||||
self._computed_styles.get('background-color', None),
|
||||
self._computed_styles.get('border-color', None),
|
||||
)
|
||||
if style_render_hash != getattr(self, '_style_render_hash', None):
|
||||
self.dirty_renderbuf(cause='style changed renderbuf')
|
||||
self._style_render_hash = style_render_hash
|
||||
|
||||
self._dirty_properties.discard('style')
|
||||
self._dirty_properties.discard('style parent')
|
||||
self._dirty_callbacks['style'].clear()
|
||||
self._dirty_callbacks['style parent'].clear()
|
||||
|
||||
if self.is_visible != was_visible:
|
||||
self.dispatch_event('on_visibilitychange')
|
||||
|
||||
# self.defer_dirty_propagation = False
|
||||
@@ -0,0 +1,344 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from functools import lru_cache
|
||||
from itertools import dropwhile, zip_longest
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
|
||||
from gpu_extras.presets import draw_texture_2d
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .blender import tag_redraw_all
|
||||
from .fsm import FSM
|
||||
|
||||
from .boundvar import BoundVar
|
||||
from .colors import colorname_to_color
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .drawing import Drawing
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .maths import floor_if_finite, ceil_if_finite
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
from ..ext import png
|
||||
from ..ext.apng import APNG
|
||||
|
||||
|
||||
|
||||
'''
|
||||
Links to useful resources
|
||||
|
||||
- How Browsers Work: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork
|
||||
- WebCore Rendering
|
||||
- https://webkit.org/blog/114/webcore-rendering-i-the-basics/
|
||||
- https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/
|
||||
- https://webkit.org/blog/116/webcore-rendering-iii-layout-basics/
|
||||
- https://webkit.org/blog/117/webcore-rendering-iv-absolutefixed-and-relative-positioning/
|
||||
- https://webkit.org/blog/118/webcore-rendering-v-floats/
|
||||
- Mozilla's Layout Engine: https://www-archive.mozilla.org/newlayout/doc/layout-2006-12-14/master.xhtml
|
||||
- Mozilla's Notes on HTML Reflow: https://www-archive.mozilla.org/newlayout/doc/reflow.html
|
||||
- How Browser Rendering Works: http://dbaron.github.io/browser-rendering/
|
||||
- Render-tree Construction, Layout, and Paint: https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction
|
||||
- Beginner's Guide to Choose Between CSS Grid and Flexbox: https://medium.com/youstart-labs/beginners-guide-to-choose-between-css-grid-and-flexbox-783005dd2412
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class UI_Core_Utils:
|
||||
@staticmethod
|
||||
def defer_dirty_wrapper(cause, properties=None, parent=True, children=False):
|
||||
''' prevents dirty propagation until the wrapped fn has finished '''
|
||||
def wrapper(fn):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
self._defer_dirty = True
|
||||
ret = fn(self, *args, **kwargs)
|
||||
self._defer_dirty = False
|
||||
self.dirty(cause=f'dirtying deferred dirtied properties now: {cause}', properties=properties, parent=parent, children=children)
|
||||
return ret
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
@contextlib.contextmanager
|
||||
def defer_dirty(self, cause, properties=None, parent=True, children=False):
|
||||
''' prevents dirty propagation until the end of with has finished '''
|
||||
self._defer_dirty = True
|
||||
self.defer_dirty_propagation = True
|
||||
yield
|
||||
self.defer_dirty_propagation = False
|
||||
self._defer_dirty = False
|
||||
self.dirty(cause=f'dirtying deferred dirtied properties now: {cause}', properties=properties, parent=parent, children=children)
|
||||
|
||||
_option_callbacks = {}
|
||||
@staticmethod
|
||||
def add_option_callback(option):
|
||||
def wrapper(fn):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
ret = fn(self, *args, **kwargs)
|
||||
return ret
|
||||
UI_Core_Utils._option_callbacks[option] = wrapped
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
def call_option_callback(self, option, default, *args, **kwargs):
|
||||
option = option if option not in UI_Core_Utils._option_callbacks else default
|
||||
UI_Core_Utils._option_callbacks[option](self, *args, **kwargs)
|
||||
|
||||
_cleaning_graph = {}
|
||||
_cleaning_graph_roots = set()
|
||||
_cleaning_graph_nodes = set()
|
||||
@staticmethod
|
||||
def add_cleaning_callback(label, labels_dirtied=None):
|
||||
# NOTE: this function decorator does NOT call self.dirty!
|
||||
UI_Core_Utils._cleaning_graph_nodes.add(label)
|
||||
g = UI_Core_Utils._cleaning_graph
|
||||
labels_dirtied = list(labels_dirtied) if labels_dirtied else []
|
||||
for l in [label]+labels_dirtied: g.setdefault(l, {'fn':None, 'children':[], 'parents':[]})
|
||||
def wrapper(fn):
|
||||
g[label]['name'] = label
|
||||
g[label]['fn'] = fn
|
||||
g[label]['children'] = labels_dirtied
|
||||
for l in labels_dirtied: g[l]['parents'].append(label)
|
||||
|
||||
# find roots of graph (any label that is not dirtied by another cleaning callback)
|
||||
UI_Core_Utils._cleaning_graph_roots = set(k for (k,v) in g.items() if not v['parents'])
|
||||
assert UI_Core_Utils._cleaning_graph_roots, 'cycle detected in cleaning callbacks'
|
||||
# TODO: also detect cycles such as: a->b->c->d->b->...
|
||||
# done in call_cleaning_callbacks, but could be done here instead?
|
||||
|
||||
return fn
|
||||
return wrapper
|
||||
|
||||
|
||||
#####################################################################
|
||||
# helper functions
|
||||
# these functions use self._computed_style, so these functions
|
||||
# MUST BE CALLED AFTER `compute_style()` METHOD IS CALLED!
|
||||
|
||||
def _get_style_num(self, k, def_v=None, percent_of=None, scale=None):
|
||||
v = self._computed_styles.get(k, 'auto')
|
||||
if v == 'auto': v = def_v or 'auto'
|
||||
if v == 'auto': return 'auto'
|
||||
# v must be NumberUnit here!
|
||||
if v.unit == '%': scale = None
|
||||
v = v.val(base=(float(def_v) if percent_of is None else percent_of))
|
||||
v = float(v)
|
||||
if scale is not None: v *= scale
|
||||
return floor_if_finite(v)
|
||||
|
||||
def _get_style_trbl(self, kb, scale=None):
|
||||
cache = self._style_trbl_cache
|
||||
key = f'{kb} {scale}'
|
||||
if key not in cache:
|
||||
t = self._get_style_num(f'{kb}-top', def_v=NumberUnit.zero, scale=scale)
|
||||
r = self._get_style_num(f'{kb}-right', def_v=NumberUnit.zero, scale=scale)
|
||||
b = self._get_style_num(f'{kb}-bottom', def_v=NumberUnit.zero, scale=scale)
|
||||
l = self._get_style_num(f'{kb}-left', def_v=NumberUnit.zero, scale=scale)
|
||||
cache[key] = (t, r, b, l)
|
||||
return cache[key]
|
||||
|
||||
|
||||
|
||||
|
||||
###########################################################################
|
||||
# below is a helper class for drawing ui
|
||||
|
||||
|
||||
|
||||
class UIRender:
|
||||
def __init__(self):
|
||||
self._children = []
|
||||
def append_child(self, child):
|
||||
self._children.append(child)
|
||||
|
||||
class UIRender_Block(UIRender):
|
||||
def __init__(self):
|
||||
super.__init__(self)
|
||||
|
||||
class UIRender_Inline(UIRender):
|
||||
def __init__(self):
|
||||
super.__init__(self)
|
||||
|
||||
|
||||
|
||||
# dictionary to convert cursor name to Blender cursor enum
|
||||
# https://docs.blender.org/api/blender2.8/bpy.types.Window.html#bpy.types.Window.cursor_modal_set
|
||||
# DEFAULT, NONE, WAIT, HAND,
|
||||
# CROSSHAIR, TEXT,
|
||||
# PAINT_BRUSH, EYEDROPPER, KNIFE,
|
||||
# MOVE_X, MOVE_Y,
|
||||
# SCROLL_X, SCROLL_Y, SCROLL_XY
|
||||
cursorname_to_cursor = {
|
||||
'default': 'DEFAULT', 'auto': 'DEFAULT', 'initial': 'DEFAULT',
|
||||
'none': 'NONE',
|
||||
'wait': 'WAIT',
|
||||
'grab': 'HAND',
|
||||
'crosshair': 'CROSSHAIR', 'pointer': 'CROSSHAIR',
|
||||
'text': 'TEXT',
|
||||
'e-resize': 'MOVE_X', 'w-resize': 'MOVE_X', 'ew-resize': 'MOVE_X',
|
||||
'n-resize': 'MOVE_Y', 's-resize': 'MOVE_Y', 'ns-resize': 'MOVE_Y',
|
||||
'all-scroll': 'SCROLL_XY',
|
||||
}
|
||||
|
||||
|
||||
# @debug_test_call('rgb( 255,128, 64 )')
|
||||
# @debug_test_call('rgba(255, 128, 64, 0.5)')
|
||||
# @debug_test_call('hsl(0, 100%, 50%)')
|
||||
# @debug_test_call('hsl(240, 100%, 50%)')
|
||||
# @debug_test_call('hsl(147, 50%, 47%)')
|
||||
# @debug_test_call('hsl(300, 76%, 72%)')
|
||||
# @debug_test_call('hsl(39, 100%, 50%)')
|
||||
# @debug_test_call('hsla(248, 53%, 58%, 0.5)')
|
||||
# @debug_test_call('#FFc080')
|
||||
# @debug_test_call('transparent')
|
||||
# @debug_test_call('white')
|
||||
# @debug_test_call('black')
|
||||
def convert_token_to_color(c):
|
||||
r,g,b,a = 0,0,0,1
|
||||
if type(c) is re.Match: c = c.group(0)
|
||||
|
||||
if c in colorname_to_color:
|
||||
c = colorname_to_color[c]
|
||||
if len(c) == 3: r,g,b = c
|
||||
else: r,g,b,a = c
|
||||
|
||||
elif c.startswith('#'):
|
||||
r,g,b = map(lambda v:int(v,16), [c[1:3],c[3:5],c[5:7]])
|
||||
|
||||
elif c.startswith('rgb(') or c.startswith('rgba('):
|
||||
c = c.replace('rgb(','').replace('rgba(','').replace(')','').replace(' ','').split(',')
|
||||
c = list(map(float, c))
|
||||
r,g,b = c[:3]
|
||||
if len(c) == 4: a = c[3]
|
||||
|
||||
elif c.startswith('hsl(') or c.startswith('hsla('):
|
||||
c = c.replace('hsl(','').replace('hsla(','').replace(')','').replace(' ','').replace('%', '').split(',')
|
||||
c = list(map(float, c))
|
||||
h,s,l = c[0]/360, c[1]/100, c[2]/100
|
||||
if len(c) == 4: a = c[3]
|
||||
# https://gist.github.com/mjackson/5311256
|
||||
# TODO: use equations on https://www.rapidtables.com/convert/color/hsl-to-rgb.html
|
||||
if s <= 0.00001:
|
||||
r = g = b = l*255
|
||||
else:
|
||||
def hue2rgb(p, q, t):
|
||||
t %= 1
|
||||
if t < 1/6: return p + (q - p) * 6 * t
|
||||
if t < 1/2: return q
|
||||
if t < 2/3: return p + (q - p) * (2/3 - t) * 6
|
||||
return p
|
||||
q = (l * ( 1 + s)) if l < 0.5 else (l + s - l * s)
|
||||
p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1/3) * 255
|
||||
g = hue2rgb(p, q, h) * 255
|
||||
b = hue2rgb(p, q, h - 1/3) * 255
|
||||
|
||||
else:
|
||||
assert 'could not convert "%s" to color' % c
|
||||
|
||||
c = Color((r/255, g/255, b/255, a))
|
||||
c.freeze()
|
||||
return c
|
||||
|
||||
def convert_token_to_cursor(c):
|
||||
if c is None: return c
|
||||
if type(c) is re.Match: c = c.group(0)
|
||||
if c in cursorname_to_cursor: return cursorname_to_cursor[c]
|
||||
if c in cursorname_to_cursor.values(): return c
|
||||
assert False, 'could not convert "%s" to cursor' % c
|
||||
|
||||
def convert_token_to_number(n):
|
||||
if type(n) is re.Match: n = n.group('num')
|
||||
return float(n)
|
||||
|
||||
def convert_token_to_numberunit(n):
|
||||
assert type(n) is re.Match
|
||||
return NumberUnit(n.group('num'), n.group('unit'))
|
||||
|
||||
def skip_token(n):
|
||||
return None
|
||||
|
||||
def convert_token_to_string(s):
|
||||
if type(s) is re.Match: s = s.group(0)
|
||||
return str(s)
|
||||
|
||||
def get_converter_to_string(group):
|
||||
def getter(s):
|
||||
if type(s) is re.Match: s = s.group(group)
|
||||
return str(s)
|
||||
return getter
|
||||
|
||||
|
||||
#####################################################################################
|
||||
# below are various helper functions for ui functions
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def helper_wraptext(text='', width=float('inf'), fontid=0, fontsize=12, preserve_newlines=False, collapse_spaces=True, wrap_text=True, **kwargs):
|
||||
if type(text) is not str:
|
||||
assert False, 'unknown type: %s (%s)' % (str(type(text)), str(text))
|
||||
# TODO: get textwidth of space and each word rather than rebuilding the string
|
||||
size_prev = Globals.drawing.set_font_size(fontsize, fontid=fontid, force=True)
|
||||
tw = Globals.drawing.get_text_width
|
||||
wrap_text &= math.isfinite(width)
|
||||
|
||||
if not preserve_newlines: text = re.sub(r'\n', ' ', text)
|
||||
if collapse_spaces: text = re.sub(r' +', ' ', text)
|
||||
if wrap_text:
|
||||
cline,*ltext = text.split(' ')
|
||||
nlines = []
|
||||
for cword in ltext:
|
||||
if not collapse_spaces and cword == '': cword = ' '
|
||||
nline = f'{cline} {cword}'
|
||||
if tw(nline) <= width: cline = nline
|
||||
else: nlines,cline = nlines+[cline],cword
|
||||
nlines += [cline]
|
||||
text = '\n'.join(nlines)
|
||||
|
||||
Globals.drawing.set_font_size(size_prev, fontid=fontid, force=True)
|
||||
if False: print('wrapped ' + str(random.random()))
|
||||
return text
|
||||
|
||||
|
||||
@add_cache('guid', 0)
|
||||
def get_unique_ui_id(prefix='', postfix=''):
|
||||
get_unique_ui_id.guid += 1
|
||||
return f'{prefix}{get_unique_ui_id.guid}{postfix}'
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from itertools import dropwhile
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from gpu_extras.presets import draw_texture_2d
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from . import gpustate
|
||||
|
||||
from . import ui_settings
|
||||
from .gpustate import ScissorStack
|
||||
from .ui_linefitter import LineFitter
|
||||
from .ui_core import UI_Element
|
||||
from .ui_core_preventmulticalls import UI_Core_PreventMultiCalls
|
||||
from .blender import tag_redraw_all
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
from .ui_core_utilities import helper_wraptext, convert_token_to_cursor
|
||||
from .fsm import FSM
|
||||
|
||||
from .useractions import ActionHandler
|
||||
|
||||
from .boundvar import BoundVar
|
||||
from .blender import get_view3d_area, get_view3d_region
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head
|
||||
|
||||
from ..ext import png
|
||||
from ..ext.apng import APNG
|
||||
|
||||
|
||||
|
||||
class UI_Document:
|
||||
default_keymap = {
|
||||
'commit': {'RET',},
|
||||
'cancel': {'ESC',},
|
||||
'keypress':
|
||||
{c for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'} |
|
||||
{'NUMPAD_%d'%i for i in range(10)} | {'NUMPAD_PERIOD','NUMPAD_MINUS','NUMPAD_PLUS','NUMPAD_SLASH','NUMPAD_ASTERIX'} |
|
||||
{'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE'} |
|
||||
{'PERIOD', 'MINUS', 'SPACE', 'SEMI_COLON', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET'},
|
||||
'scroll top': {'HOME'},
|
||||
'scroll bottom': {'END'},
|
||||
'scroll up': {'WHEELUPMOUSE', 'PAGE_UP', 'UP_ARROW', },
|
||||
'scroll down': {'WHEELDOWNMOUSE', 'PAGE_DOWN', 'DOWN_ARROW', },
|
||||
'scroll': {'TRACKPADPAN'},
|
||||
}
|
||||
|
||||
doubleclick_time = bpy.context.preferences.inputs.mouse_double_click_time / 1000 # 0.25
|
||||
wheel_scroll_lines = 3 # bpy.context.preferences.inputs.wheel_scroll_lines, see https://developer.blender.org/rBbec583951d736776d2096368ef8d2b764287ac11
|
||||
allow_disabled_to_blur = False
|
||||
show_tooltips = True
|
||||
tooltip_delay = 0.50
|
||||
max_click_dist = 10 # allows mouse to travel off element and still register a click event
|
||||
allow_click_time = 0.50 # allows for very fast clicking. ignore max_click_dist if time(mouseup-mousedown) is at most allow_click_time
|
||||
|
||||
def __init__(self):
|
||||
self._context = None
|
||||
self._area = None
|
||||
self._exception_callbacks = []
|
||||
self._ui_scale = Globals.drawing.get_dpi_mult()
|
||||
self._draw_count = 0
|
||||
self._draw_time = 0
|
||||
self._draw_fps = 0
|
||||
|
||||
def add_exception_callback(self, fn):
|
||||
self._exception_callbacks += [fn]
|
||||
|
||||
def _callback_exception_callbacks(self, e):
|
||||
for fn in self._exception_callbacks:
|
||||
try:
|
||||
fn(e)
|
||||
except Exception as e2:
|
||||
print(f'UI_Document: Caught exception while calling back exception callbacks: {fn.__name__}')
|
||||
print(f' original: {e}')
|
||||
print(f' additional: {e2}')
|
||||
debugger.print_exception()
|
||||
|
||||
# @profiler.function
|
||||
def init(self, context, **kwargs):
|
||||
self._callbacks = {
|
||||
'preclean': set(),
|
||||
'postclean': set(),
|
||||
'postflow': set(),
|
||||
'postflow once': set(),
|
||||
}
|
||||
self.defer_cleaning = False
|
||||
|
||||
self._context = context
|
||||
self._area = get_view3d_area(context)
|
||||
self.actions = ActionHandler(context, UI_Document.default_keymap)
|
||||
self._body = UI_Element(tagName='body', document=self) # root level element
|
||||
self._tooltip = UI_Element(tagName='dialog', classes='tooltip', can_hover=False, parent=self._body)
|
||||
self._tooltip.is_visible = False
|
||||
self._tooltip_message = None
|
||||
self._tooltip_wait = None
|
||||
self._tooltip_mouse = None
|
||||
self._reposition_tooltip_before_draw = False
|
||||
|
||||
self.fsm = FSM(self, start='main')
|
||||
|
||||
self.ignore_hover_change = False
|
||||
|
||||
self._sticky_dist = 20
|
||||
self._sticky_element = None # allows the mouse to drift a few pixels off before handling mouseleave
|
||||
|
||||
self._under_mouse = None
|
||||
self._under_mousedown = None
|
||||
self._under_down = None
|
||||
self._focus = None
|
||||
self._focus_full = False
|
||||
|
||||
self._last_mx = -1
|
||||
self._last_my = -1
|
||||
self._last_mouse = None
|
||||
self._last_under_mouse = None
|
||||
self._last_under_click = None
|
||||
self._last_click_time = 0
|
||||
self._last_sz = None
|
||||
self._last_w = -1
|
||||
self._last_h = -1
|
||||
|
||||
def update_callbacks(self, ui_element, force_remove=False):
|
||||
for cb,fn in [('preclean', ui_element.preclean), ('postclean', ui_element.postclean), ('postflow', ui_element.postflow)]:
|
||||
if force_remove or not fn:
|
||||
self._callbacks[cb].discard(ui_element)
|
||||
else:
|
||||
self._callbacks[cb].add(ui_element)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@property
|
||||
def activeElement(self):
|
||||
return self._focus
|
||||
|
||||
def center_on_mouse(self, element):
|
||||
# centers element under mouse, must be done between first and second layout calls
|
||||
if element is None: return
|
||||
def center():
|
||||
element._relative_pos = None
|
||||
mx, my = self.actions.mouse if self.actions.mouse else (10, 10)
|
||||
# w,h = element.width_pixels,element.height_pixels
|
||||
w, h = element.width_pixels, element._dynamic_full_size.height
|
||||
l = mx-w/2
|
||||
t = -self._body.height_pixels + my + h/2
|
||||
element.reposition(left=l, top=t)
|
||||
self._callbacks['postflow once'].add(center)
|
||||
|
||||
def _reposition_tooltip(self, force=False):
|
||||
if self._tooltip_mouse == self.actions.mouse and not force: return
|
||||
self._tooltip_mouse = self.actions.mouse
|
||||
if self._tooltip.width_pixels is None or type(self._tooltip.width_pixels) is str or self._tooltip._mbp_width is None or self._tooltip.height_pixels is None or type(self._tooltip.height_pixels) is str or self._tooltip._mbp_height is None:
|
||||
ttl,ttt = self.actions.mouse
|
||||
else:
|
||||
ttl = self.actions.mouse.x if self.actions.mouse.x < self._body.width_pixels/2 else self.actions.mouse.x - (self._tooltip.width_pixels + (self._tooltip._mbp_width or 0))
|
||||
ttt = self.actions.mouse.y if self.actions.mouse.y > self._body.height_pixels/2 else self.actions.mouse.y + (self._tooltip.height_pixels + (self._tooltip._mbp_height or 0))
|
||||
hp = self._body.height_pixels if type(self._body.height_pixels) is not str else 0.0
|
||||
self._tooltip.reposition(left=ttl, top=ttt - hp)
|
||||
|
||||
def removed_element(self, ui_element):
|
||||
if self._under_mouse and self._under_mouse.is_descendant_of(ui_element):
|
||||
self._under_mouse = None
|
||||
if self._under_mousedown and self._under_mousedown.is_descendant_of(ui_element):
|
||||
self._under_mousedown = None
|
||||
if self._focus and self._focus.is_descendant_of(ui_element):
|
||||
self._focus = None
|
||||
|
||||
def force_dirty_all(self):
|
||||
self._body.dirty(children=True)
|
||||
self._body.dirty_styling()
|
||||
self._body.dirty_flow()
|
||||
tag_redraw_all('Force Dirty All')
|
||||
|
||||
# @profiler.function
|
||||
def update(self, context, event):
|
||||
self._context = context
|
||||
self._area = get_view3d_area(context)
|
||||
# if context.area != self._area: return
|
||||
# self._ui_scale = Globals.drawing.get_dpi_mult()
|
||||
|
||||
UI_Core_PreventMultiCalls.reset_multicalls()
|
||||
|
||||
region = get_view3d_region(context)
|
||||
w,h = region.width, region.height
|
||||
if self._last_w != w or self._last_h != h:
|
||||
# print('Document:', (self._last_w, self._last_h), (w,h))
|
||||
self._last_w,self._last_h = w,h
|
||||
self._body.dirty(cause='changed document size', children=True)
|
||||
self._body.dirty_flow()
|
||||
tag_redraw_all("UI_Element update: w,h change")
|
||||
|
||||
if ui_settings.DEBUG_COLOR_CLEAN: tag_redraw_all("UI_Element DEBUG_COLOR_CLEAN")
|
||||
|
||||
#self.actions.update(context, event, self._timer, print_actions=False)
|
||||
# self.actions.update(context, event, print_actions=False)
|
||||
|
||||
if self._sticky_element and not self._sticky_element.is_visible:
|
||||
self._sticky_element = None
|
||||
|
||||
self._mx,self._my = self.actions.mouse if self.actions.mouse else (-1,-1)
|
||||
if not self.ignore_hover_change:
|
||||
self._under_mouse = self._body.get_under_mouse(self.actions.mouse)
|
||||
if self._sticky_element:
|
||||
if self._sticky_element.get_mouse_distance(self.actions.mouse) < self._sticky_dist * self._ui_scale:
|
||||
if self._under_mouse is None or not self._under_mouse.is_descendant_of(self._sticky_element):
|
||||
self._under_mouse = self._sticky_element
|
||||
|
||||
next_message = None
|
||||
if self._under_mouse and self._under_mouse.title_with_for(): # and not self._under_mouse.disabled:
|
||||
next_message = self._under_mouse.title_with_for()
|
||||
if self._under_mouse.disabled:
|
||||
next_message = f'(Disabled) {next_message}'
|
||||
if self._tooltip_message != next_message:
|
||||
self._tooltip_message = next_message
|
||||
self._tooltip_mouse = None
|
||||
self._tooltip_wait = time.time() + self.tooltip_delay
|
||||
self._tooltip.is_visible = False
|
||||
if self._tooltip_message and time.time() > self._tooltip_wait:
|
||||
if self._tooltip_mouse != self.actions.mouse or self._tooltip.innerText != self._tooltip_message or not self._tooltip.is_visible:
|
||||
# TODO: markdown support??
|
||||
self._tooltip.innerText = self._tooltip_message
|
||||
self._tooltip.is_visible = True and self.show_tooltips
|
||||
self._reposition_tooltip_before_draw = True
|
||||
tag_redraw_all("reposition tooltip")
|
||||
|
||||
self.fsm.update()
|
||||
|
||||
self._last_mx = self._mx
|
||||
self._last_my = self._my
|
||||
self._last_mouse = self.actions.mouse
|
||||
if not self.ignore_hover_change: self._last_under_mouse = self._under_mouse
|
||||
|
||||
uictrld = False
|
||||
uictrld |= self._under_mouse is not None and self._under_mouse != self._body
|
||||
uictrld |= self.fsm.state != 'main'
|
||||
uictrld |= self._focus_full
|
||||
# uictrld |= self._focus is not None
|
||||
|
||||
return {'hover'} if uictrld else None
|
||||
|
||||
|
||||
def _addrem_pseudoclass(self, pseudoclass, remove_from=None, add_to=None):
|
||||
rem = remove_from.get_pathToRoot() if remove_from else []
|
||||
add = add_to.get_pathToRoot() if add_to else []
|
||||
rem.reverse()
|
||||
add.reverse()
|
||||
roots = []
|
||||
if rem: roots.append(rem[0])
|
||||
if add: roots.append(add[0])
|
||||
while rem and add and rem[0] == add[0]:
|
||||
rem = rem[1:]
|
||||
add = add[1:]
|
||||
# print(f'addrem_pseudoclass: {pseudoclass} {rem} {add}')
|
||||
self.defer_cleaning = True
|
||||
for root in roots: root.defer_dirty_propagation = True
|
||||
for e in rem: e.del_pseudoclass(pseudoclass)
|
||||
for e in add: e.add_pseudoclass(pseudoclass)
|
||||
for root in roots: root.defer_dirty_propagation = False
|
||||
self.defer_cleaning = False
|
||||
|
||||
def debug_print(self):
|
||||
print('')
|
||||
print('UI_Document.debug_print')
|
||||
self._body.debug_print(0, set())
|
||||
def debug_print_toroot(self, fromHovered=True, fromFocused=False):
|
||||
print('')
|
||||
print('UI_Document.debug_print_toroot')
|
||||
if fromHovered: self._debug_print(self._under_mouse)
|
||||
if fromFocused: self._debug_print(self._focus)
|
||||
def _debug_print(self, ui_from):
|
||||
# debug print!
|
||||
path = ui_from.get_pathToRoot()
|
||||
for i,ui_elem in enumerate(reversed(path)):
|
||||
def tprint(*args, extra=0, **kwargs):
|
||||
print(' '*(i+extra), end='')
|
||||
print(*args, **kwargs)
|
||||
tprint(str(ui_elem))
|
||||
tprint(f'selector={ui_elem._selector}', extra=1)
|
||||
tprint(f'l={ui_elem._l} t={ui_elem._t} w={ui_elem._w} h={ui_elem._h}', extra=1)
|
||||
|
||||
@property
|
||||
def sticky_element(self):
|
||||
return self._sticky_element
|
||||
@sticky_element.setter
|
||||
def sticky_element(self, element):
|
||||
self._sticky_element = element
|
||||
|
||||
def clear_last_under(self):
|
||||
self._last_under_mouse = None
|
||||
|
||||
def handle_hover(self, change_cursor=True):
|
||||
# handle :hover, on_mouseenter, on_mouseleave
|
||||
if self.ignore_hover_change: return
|
||||
|
||||
if change_cursor and self._under_mouse and self._under_mouse._tagName != 'body':
|
||||
cursor = self._under_mouse._computed_styles.get('cursor', 'default')
|
||||
Globals.cursors.set(convert_token_to_cursor(cursor))
|
||||
|
||||
if self._under_mouse == self._last_under_mouse: return
|
||||
if self._under_mouse and not self._under_mouse.can_hover: return
|
||||
|
||||
self._addrem_pseudoclass('hover', remove_from=self._last_under_mouse, add_to=self._under_mouse)
|
||||
if self._last_under_mouse: self._last_under_mouse.dispatch_event('on_mouseleave')
|
||||
if self._under_mouse: self._under_mouse.dispatch_event('on_mouseenter')
|
||||
|
||||
def handle_mousemove(self, ui_element=None):
|
||||
ui_element = ui_element or self._under_mouse
|
||||
if ui_element is None: return
|
||||
if self._last_mouse == self.actions.mouse: return
|
||||
ui_element.dispatch_event('on_mousemove')
|
||||
|
||||
def handle_keypress(self, ui_element=None):
|
||||
ui_element = ui_element or self._focus
|
||||
|
||||
if self.actions.pressed('clipboard paste') and ui_element:
|
||||
ui_element.dispatch_event('on_paste', clipboardData=bpy.context.window_manager.clipboard)
|
||||
|
||||
pressed = self.actions.as_char(self.actions.just_pressed)
|
||||
|
||||
if pressed and ui_element:
|
||||
ui_element.dispatch_event('on_keypress', key=pressed)
|
||||
|
||||
|
||||
@FSM.on_state('main', 'enter')
|
||||
def modal_main_enter(self):
|
||||
Globals.cursors.set('DEFAULT')
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
# print('UI_Document.main', self.actions.event_type, time.time())
|
||||
|
||||
|
||||
if self.actions.just_pressed:
|
||||
pressed = self.actions.just_pressed
|
||||
if pressed not in {'WINDOW_DEACTIVATE'}:
|
||||
if self._focus and self._focus_full:
|
||||
self._focus.dispatch_event('on_keypress', key=pressed)
|
||||
elif self._under_mouse:
|
||||
self._under_mouse.dispatch_event('on_keypress', key=pressed)
|
||||
|
||||
self.handle_hover()
|
||||
self.handle_mousemove()
|
||||
|
||||
if self.actions.pressed('MIDDEMOUSE'):
|
||||
return 'scroll'
|
||||
|
||||
if self.actions.pressed('LEFTMOUSE', unpress=False, ignoremods=True, ignoremulti=True):
|
||||
if self._under_mouse == self._body:
|
||||
# clicking body always blurs focus
|
||||
self.blur()
|
||||
elif UI_Document.allow_disabled_to_blur and self._under_mouse and self._under_mouse.is_disabled:
|
||||
# user clicked on disabled element, so blur current focused element
|
||||
self.blur()
|
||||
return 'mousedown'
|
||||
|
||||
if self.actions.pressed('SHIFT+F10'):
|
||||
profiler.clear()
|
||||
return
|
||||
if self.actions.pressed('SHIFT+F11'):
|
||||
profiler.printout()
|
||||
self.debug_print()
|
||||
return
|
||||
if self.actions.pressed('CTRL+SHIFT+F11'):
|
||||
self.debug_print_toroot()
|
||||
print(f'{self._under_mouse._computed_styles}')
|
||||
return
|
||||
|
||||
# if self.actions.pressed('RIGHTMOUSE') and self._under_mouse:
|
||||
# self._debug_print(self._under_mouse)
|
||||
# #print('focus:', self._focus)
|
||||
|
||||
if self.actions.pressed({'scroll top', 'scroll bottom'}, unpress=False):
|
||||
move = 100000 * (-1 if self.actions.pressed({'scroll top'}) else 1)
|
||||
self.actions.unpress()
|
||||
if self._get_scrollable():
|
||||
self._scroll_element.scrollTop = self._scroll_last.y + move
|
||||
self._scroll_element._setup_ltwh(recurse_children=False)
|
||||
|
||||
if self.actions.pressed({'scroll', 'scroll up', 'scroll down'}, unpress=False):
|
||||
if self.actions.event_type == 'TRACKPADPAN':
|
||||
move = self.actions.scroll[1] # self.actions.mouse.y - self.actions.mouse_prev.y
|
||||
# print(f'UI_Document.update: trackpad pan {move}')
|
||||
else:
|
||||
d = self.wheel_scroll_lines * 8 * Globals.drawing.get_dpi_mult()
|
||||
move = Globals.drawing.scale(d) * (-1 if self.actions.pressed({'scroll up'}) else 1)
|
||||
self.actions.unpress()
|
||||
if self._get_scrollable():
|
||||
self._scroll_element.scrollTop = self._scroll_last.y + move
|
||||
self._scroll_element._setup_ltwh(recurse_children=False)
|
||||
|
||||
# if self.actions.pressed('F8') and self._under_mouse:
|
||||
# print('\n\n')
|
||||
# for e in self._under_mouse.get_pathFromRoot():
|
||||
# print(e)
|
||||
# print(e._dirty_causes)
|
||||
# for s in e._debug_list:
|
||||
# print(f' {s}')
|
||||
if False:
|
||||
print('---------------------------')
|
||||
if self._focus: print('FOCUS', self._focus, self._focus.pseudoclasses)
|
||||
else: print('FOCUS', None)
|
||||
if self._under_down: print('DOWN', self._under_down, self._under_down.pseudoclasses)
|
||||
else: print('DOWN', None)
|
||||
if under_mouse: print('UNDER', under_mouse, under_mouse.pseudoclasses)
|
||||
else: print('UNDER', None)
|
||||
|
||||
def _get_scrollable(self):
|
||||
# find first along root to path that can scroll
|
||||
if not self._under_mouse: return None
|
||||
self._scroll_element = next((e for e in self._under_mouse.get_pathToRoot() if e.is_scrollable_y), None)
|
||||
if self._scroll_element:
|
||||
self._scroll_last = RelPoint2D((self._scroll_element.scrollLeft, self._scroll_element.scrollTop))
|
||||
return self._scroll_element
|
||||
|
||||
@FSM.on_state('scroll', 'can enter')
|
||||
def scroll_canenter(self):
|
||||
if not self._get_scrollable(): return False
|
||||
|
||||
@FSM.on_state('scroll', 'enter')
|
||||
def scroll_enter(self):
|
||||
self._scroll_point = self.actions.mouse
|
||||
self.ignore_hover_change = True
|
||||
Globals.cursors.set('SCROLL_Y')
|
||||
|
||||
@FSM.on_state('scroll')
|
||||
def scroll_main(self):
|
||||
if self.actions.released('MIDDLEMOUSE', ignoremods=True, ignoremulti=True):
|
||||
# done scrolling
|
||||
return 'main'
|
||||
nx = self._scroll_element.scrollLeft + (self._scroll_point.x - self._mx)
|
||||
ny = self._scroll_element.scrollTop - (self._scroll_point.y - self._my)
|
||||
self._scroll_element.scrollLeft = nx
|
||||
self._scroll_element.scrollTop = ny
|
||||
self._scroll_point = self.actions.mouse
|
||||
self._scroll_element._setup_ltwh(recurse_children=False)
|
||||
|
||||
@FSM.on_state('scroll', 'exit')
|
||||
def scroll_exit(self):
|
||||
self.ignore_hover_change = False
|
||||
|
||||
|
||||
@FSM.on_state('mousedown', 'can enter')
|
||||
def mousedown_canenter(self):
|
||||
return self._focus or (
|
||||
self._under_mouse and self._under_mouse != self._body and not self._under_mouse.is_disabled
|
||||
)
|
||||
|
||||
@FSM.on_state('mousedown', 'enter')
|
||||
def mousedown_enter(self):
|
||||
self._mousedown_time = time.time()
|
||||
self._under_mousedown = self._under_mouse
|
||||
if not self._under_mousedown:
|
||||
# likely, self._under_mouse or an ancestor was deleted?
|
||||
# mousedown main event handler below will switch FSM back to main, effectively ignoring the mousedown event
|
||||
# see RetopoFlow issue #857
|
||||
self.blur()
|
||||
return
|
||||
self._addrem_pseudoclass('active', add_to=self._under_mousedown)
|
||||
self._under_mousedown.dispatch_event('on_mousedown')
|
||||
# print(self._under_mouse.get_pathToRoot())
|
||||
|
||||
change_focus = self._focus != self._under_mouse
|
||||
if change_focus:
|
||||
if self._under_mouse.can_focus:
|
||||
# element under mouse takes focus (or whichever it's for points to)
|
||||
if self._under_mouse.forId:
|
||||
f = self._under_mouse.get_for_element()
|
||||
if f and f.can_focus: self.focus(f)
|
||||
else: self.focus(self._under_mouse)
|
||||
else: self.focus(self._under_mouse)
|
||||
elif self._focus and self._is_ancestor(self._focus, self._under_mouse):
|
||||
# current focus is an ancestor of new element, so don't blur!
|
||||
pass
|
||||
else:
|
||||
self.blur()
|
||||
|
||||
@FSM.on_state('mousedown')
|
||||
def mousedown_main(self):
|
||||
if not self._under_mousedown:
|
||||
return 'main'
|
||||
if self.actions.released('LEFTMOUSE', ignoremods=True, ignoremulti=True):
|
||||
# done with mousedown
|
||||
return 'focus' if self._under_mousedown.can_focus else 'main'
|
||||
|
||||
if self.actions.pressed('RIGHTMOUSE', ignoremods=True, unpress=False):
|
||||
self._under_mousedown.dispatch_event('on_mousedown')
|
||||
|
||||
self.handle_hover(change_cursor=False)
|
||||
self.handle_mousemove(ui_element=self._under_mousedown)
|
||||
self.handle_keypress(ui_element=self._under_mousedown)
|
||||
|
||||
@FSM.on_state('mousedown', 'exit')
|
||||
def mousedown_exit(self):
|
||||
if not self._under_mousedown:
|
||||
# likely, self._under_mousedown or an ancestor was deleted while under mousedown
|
||||
# need to reset variables enough to get us back to main FSM state!
|
||||
self._last_under_click = None
|
||||
self._last_click_time = 0
|
||||
self.ignore_hover_change = False
|
||||
return
|
||||
self._under_mousedown.dispatch_event('on_mouseup')
|
||||
under_mouseclick = self._under_mousedown
|
||||
click = False
|
||||
click |= time.time() - self._mousedown_time < self.allow_click_time
|
||||
click |= self._under_mousedown.get_mouse_distance(self.actions.mouse) <= self.max_click_dist * self._ui_scale
|
||||
if not click:
|
||||
# find closest common ancestor of self._under_mouse and self._under_mousedown that is getting clicked
|
||||
ancestors0 = self._under_mousedown.get_pathFromRoot()
|
||||
ancestors1 = self._under_mouse.get_pathFromRoot() if self._under_mouse else []
|
||||
ancestors = [a0 for (a0, a1) in zip(ancestors0, ancestors1) if a0 == a1 and a0.get_mouse_distance(self.actions.mouse) < 1]
|
||||
if ancestors:
|
||||
under_mouseclick = ancestors[-1]
|
||||
click = True
|
||||
# print('mousedown_exit', time.time()-self._mousedown_time, self.allow_click_time, self.actions.mouse, self._under_mousedown.get_mouse_distance(self.actions.mouse), self.max_click_dist)
|
||||
if click:
|
||||
# old/simple: self._under_mouse == self._under_mousedown:
|
||||
dblclick = True
|
||||
dblclick &= under_mouseclick == self._last_under_click
|
||||
dblclick &= time.time() < self._last_click_time + self.doubleclick_time
|
||||
under_mouseclick.dispatch_event('on_mouseclick')
|
||||
self._last_under_click = under_mouseclick
|
||||
if dblclick:
|
||||
under_mouseclick.dispatch_event('on_mousedblclick')
|
||||
# self._last_under_click = None
|
||||
# if self._under_mousedown:
|
||||
# # if applicable, send mouseclick events to ui_element indicated by forId
|
||||
# ui_for = self._under_mousedown.get_for_element()
|
||||
# print(f'mousedown_exit:')
|
||||
# print(f' ui under: {self._under_mousedown}')
|
||||
# print(f' ui for: {ui_for}')
|
||||
# if ui_for: ui_for.dispatch_event('on_mouseclick')
|
||||
self._last_click_time = time.time()
|
||||
else:
|
||||
self._last_under_click = None
|
||||
self._last_click_time = 0
|
||||
self._addrem_pseudoclass('active', remove_from=self._under_mousedown)
|
||||
# self._under_mousedown.del_pseudoclass('active')
|
||||
|
||||
def _is_ancestor(self, ancestor, descendant):
|
||||
return ancestor in descendant.get_pathToRoot()
|
||||
|
||||
def blur(self, stop_at=None):
|
||||
self._focus_full = False
|
||||
if self._focus is None: return
|
||||
self._focus.del_pseudoclass('focus')
|
||||
self._focus.dispatch_event('on_blur')
|
||||
self._focus.dispatch_event('on_focusout', stop_at=stop_at)
|
||||
self._addrem_pseudoclass('active', remove_from=self._focus)
|
||||
self._focus = None
|
||||
|
||||
def focus(self, ui_element, full=False):
|
||||
if ui_element is None: return
|
||||
if self._focus == ui_element: return
|
||||
|
||||
stop_focus_at = None
|
||||
if self._focus:
|
||||
stop_blur_at = None
|
||||
p_focus = ui_element.get_pathFromRoot()
|
||||
p_blur = self._focus.get_pathFromRoot()
|
||||
for i in range(min(len(p_focus), len(p_blur))):
|
||||
if p_focus[i] != p_blur[i]:
|
||||
stop_focus_at = p_focus[i]
|
||||
stop_blur_at = p_blur[i]
|
||||
break
|
||||
self.blur(stop_at=stop_blur_at)
|
||||
#print('focusout to', p_blur, stop_blur_at)
|
||||
#print('focusin from', p_focus, stop_focus_at)
|
||||
self._focus_full = full
|
||||
self._focus = ui_element
|
||||
self._focus.add_pseudoclass('focus')
|
||||
self._focus.dispatch_event('on_focus')
|
||||
self._focus.dispatch_event('on_focusin', stop_at=stop_focus_at)
|
||||
|
||||
|
||||
@FSM.on_state('focus')
|
||||
def focus_main(self):
|
||||
if not self._focus:
|
||||
return 'main'
|
||||
|
||||
if self._focus_full:
|
||||
pass
|
||||
|
||||
if self.actions.pressed('LEFTMOUSE', unpress=False):
|
||||
return 'mousedown'
|
||||
# if self.actions.pressed('RIGHTMOUSE'):
|
||||
# self._debug_print(self._focus)
|
||||
# if self.actions.pressed('ESC'):
|
||||
# self.blur()
|
||||
# return 'main'
|
||||
|
||||
self.handle_hover()
|
||||
self.handle_mousemove()
|
||||
self.handle_keypress()
|
||||
|
||||
if not self._focus: return 'main'
|
||||
|
||||
def force_clean(self, context):
|
||||
if self.defer_cleaning: return
|
||||
|
||||
time_start = time.time()
|
||||
|
||||
region = get_view3d_region(context)
|
||||
w,h = region.width, region.height
|
||||
sz = Size2D(width=w, max_width=w, height=h, max_height=h)
|
||||
|
||||
UI_Core_PreventMultiCalls.reset_multicalls()
|
||||
|
||||
Globals.ui_draw.update()
|
||||
if Globals.drawing.get_dpi_mult() != self._ui_scale:
|
||||
print(f'DPI CHANGED: {self._ui_scale} -> {Globals.drawing.get_dpi_mult()}')
|
||||
self._ui_scale = Globals.drawing.get_dpi_mult()
|
||||
self._body.dirty(cause='DPI changed', children=True)
|
||||
self._body.dirty_styling()
|
||||
self._body.dirty_flow(children=True)
|
||||
if (w,h) != self._last_sz:
|
||||
self._last_sz = (w,h)
|
||||
self._body.dirty_flow()
|
||||
# self._body.dirty('region size changed', 'style', children=True)
|
||||
|
||||
# UI_Core_PreventMultiCalls.reset_multicalls()
|
||||
for o in self._callbacks['preclean']: o._call_preclean()
|
||||
self._body.clean()
|
||||
for o in self._callbacks['postclean']: o._call_postclean()
|
||||
self._body._layout(
|
||||
# linefitter=LineFitter(left=0, top=h-1, width=w, height=h),
|
||||
fitting_size=sz,
|
||||
fitting_pos=Point2D((0,h-1)),
|
||||
parent_size=sz,
|
||||
nonstatic_elem=self._body,
|
||||
table_data={},
|
||||
)
|
||||
self._body.set_view_size(sz)
|
||||
for o in self._callbacks['postflow']: o._call_postflow()
|
||||
for fn in self._callbacks['postflow once']: fn()
|
||||
self._callbacks['postflow once'].clear()
|
||||
|
||||
# UI_Core_PreventMultiCalls.reset_multicalls()
|
||||
self._body._layout(
|
||||
# linefitter=LineFitter(left=0, top=h-1, width=w, height=h),
|
||||
fitting_size=sz,
|
||||
fitting_pos=Point2D((0,h-1)),
|
||||
parent_size=sz,
|
||||
nonstatic_elem=self._body,
|
||||
table_data={},
|
||||
)
|
||||
self._body.set_view_size(sz)
|
||||
if self._reposition_tooltip_before_draw:
|
||||
self._reposition_tooltip_before_draw = False
|
||||
self._reposition_tooltip()
|
||||
|
||||
# @profiler.function
|
||||
def draw(self, context):
|
||||
self._context = context
|
||||
self._area = get_view3d_area(context)
|
||||
# if self._area != context.area: return
|
||||
Globals.drawing.glCheckError('UI_Document.draw: start')
|
||||
|
||||
time_start = time.time()
|
||||
|
||||
self.force_clean(context)
|
||||
|
||||
Globals.drawing.glCheckError('UI_Document.draw: setting options')
|
||||
ScissorStack.start(context)
|
||||
gpustate.blend('ALPHA')
|
||||
gpustate.scissor_test(True)
|
||||
gpustate.depth_test('NONE')
|
||||
|
||||
Globals.drawing.glCheckError('UI_Document.draw: drawing')
|
||||
self._body.draw()
|
||||
ScissorStack.end()
|
||||
|
||||
self._draw_count += 1
|
||||
self._draw_time += time.time() - time_start
|
||||
if self._draw_count % 100 == 0:
|
||||
fps = (self._draw_count / self._draw_time) if self._draw_time>0 else float('inf')
|
||||
self._draw_fps = fps
|
||||
# print('~%f fps (%f / %d = %f)' % (self._draw_fps, self._draw_time, self._draw_count, self._draw_time / self._draw_count))
|
||||
self._draw_count = 0
|
||||
self._draw_time = 0
|
||||
|
||||
Globals.drawing.glCheckError('UI_Document.draw: done')
|
||||
|
||||
ui_document = Globals.set(UI_Document())
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from gpu_extras.presets import draw_texture_2d
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
|
||||
from . import gpustate
|
||||
from .boundvar import BoundVar
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .drawing import Drawing
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .maths import floor_if_finite, ceil_if_finite
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
style_to_image_scale = {
|
||||
'fill': 0, # default. stretch/squash to fill entire container
|
||||
'contain': 1, # scaled to maintain aspect ratio, fit within container
|
||||
'cover': 2, # scaled to maintain aspect ratio, fill entire container
|
||||
'scale-down': 3, # same as none or contain, whichever is smaller
|
||||
'none': 4, # not resized
|
||||
}
|
||||
|
||||
image_scale_defines = {
|
||||
'IMAGE_SCALE_FILL': style_to_image_scale['fill'],
|
||||
'IMAGE_SCALE_CONTAIN': style_to_image_scale['contain'],
|
||||
'IMAGE_SCALE_COVER': style_to_image_scale['cover'],
|
||||
'IMAGE_SCALE_DOWN': style_to_image_scale['scale-down'],
|
||||
'IMAGE_SCALE_NONE': style_to_image_scale['none'],
|
||||
}
|
||||
|
||||
region_defines = {
|
||||
'REGION_MARGIN_LEFT': 0,
|
||||
'REGION_MARGIN_BOTTOM': 1,
|
||||
'REGION_MARGIN_RIGHT': 2,
|
||||
'REGION_MARGIN_TOP': 3,
|
||||
'REGION_BORDER_TOP': 4,
|
||||
'REGION_BORDER_RIGHT': 5,
|
||||
'REGION_BORDER_BOTTOM': 6,
|
||||
'REGION_BORDER_LEFT': 7,
|
||||
'REGION_BACKGROUND': 8,
|
||||
'REGION_OUTSIDE': 9,
|
||||
'REGION_ERROR': 10,
|
||||
}
|
||||
|
||||
# uncomment the following debug options to enable them
|
||||
enabled_debug_options = [
|
||||
# 'DEBUG_COLOR_MARGINS', # color fragments in margin (top, left, bottom, right)
|
||||
# 'DEBUG_COLOR_REGIONS', # color fragments based on region
|
||||
# 'DEBUG_IMAGE_CHECKER', # replace image with checker pattern to test scaling
|
||||
# 'DEBUG_IMAGE_OUTSIDE', # shift color if texcoord is outside [0,1] (in padding region)
|
||||
# 'DEBUG_SNAP_ALPHA', # snap alpha to 0 or 1 based on 0.25 threshold
|
||||
# 'DEBUG_DONT_DISCARD', # keep all fragments (do not discard any fragment)
|
||||
]
|
||||
|
||||
debug_defines = {
|
||||
# colors used if DEBUG_COLOR_MARGINS or DEBUG_COLOR_REGIONS are set to true
|
||||
'COLOR_MARGIN_LEFT': 'vec4(1.0, 0.0, 0.0, 0.25)',
|
||||
'COLOR_MARGIN_BOTTOM': 'vec4(0.0, 1.0, 0.0, 0.25)',
|
||||
'COLOR_MARGIN_RIGHT': 'vec4(0.0, 0.0, 1.0, 0.25)',
|
||||
'COLOR_MARGIN_TOP': 'vec4(0.0, 1.0, 1.0, 0.25)',
|
||||
|
||||
'COLOR_BORDER_TOP': 'vec4(0.5, 0.0, 0.0, 0.25)',
|
||||
'COLOR_BORDER_RIGHT': 'vec4(0.0, 0.5, 0.5, 0.25)',
|
||||
'COLOR_BORDER_BOTTOM': 'vec4(0.0, 0.5, 0.5, 0.25)',
|
||||
'COLOR_BORDER_LEFT': 'vec4(0.0, 0.5, 0.5, 0.25)',
|
||||
|
||||
'COLOR_BACKGROUND': 'vec4(0.5, 0.5, 0.0, 0.25)',
|
||||
|
||||
'COLOR_OUTSIDE': 'vec4(0.5, 0.5, 0.5, 0.25)',
|
||||
|
||||
'COLOR_ERROR': 'vec4(1.0, 0.0, 0.0, 1.00)',
|
||||
'COLOR_ERROR_NEVER': 'vec4(1.0, 0.0, 1.0, 1.00)',
|
||||
|
||||
'COLOR_DEBUG_IMAGE': 'vec4(0.0, 0.0, 0.0, 0.00)',
|
||||
'COLOR_CHECKER_00': 'vec4(0.0, 0.0, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_01': 'vec4(0.0, 0.0, 0.5, 1.00)',
|
||||
'COLOR_CHECKER_02': 'vec4(0.0, 0.5, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_03': 'vec4(0.0, 0.5, 0.5, 1.00)',
|
||||
'COLOR_CHECKER_04': 'vec4(0.5, 0.0, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_05': 'vec4(0.5, 0.0, 0.5, 1.00)',
|
||||
'COLOR_CHECKER_06': 'vec4(0.5, 0.5, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_07': 'vec4(0.5, 0.5, 0.5, 1.00)',
|
||||
'COLOR_CHECKER_08': 'vec4(0.3, 0.3, 0.3, 1.00)',
|
||||
'COLOR_CHECKER_09': 'vec4(0.0, 0.0, 1.0, 1.00)',
|
||||
'COLOR_CHECKER_10': 'vec4(0.0, 1.0, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_11': 'vec4(0.0, 1.0, 1.0, 1.00)',
|
||||
'COLOR_CHECKER_12': 'vec4(1.0, 0.0, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_13': 'vec4(1.0, 0.0, 1.0, 1.00)',
|
||||
'COLOR_CHECKER_14': 'vec4(1.0, 1.0, 0.0, 1.00)',
|
||||
'COLOR_CHECKER_15': 'vec4(1.0, 1.0, 1.0, 1.00)',
|
||||
}
|
||||
|
||||
if not bpy.app.background:
|
||||
draw_data = ( 'TRIS', { 'pos': [(0,0),(1,0),(1,1), (1,1),(0,1),(0,0)] } )
|
||||
defines = image_scale_defines | region_defines | debug_defines | { k:True for k in enabled_debug_options }
|
||||
vertex_shader, fragment_shader = gpustate.shader_parse_file('ui_element.glsl', includeVersion=False)
|
||||
ui_draw_shader, ui_draw_ubos = gpustate.gpu_shader('UI_Draw', vertex_shader, fragment_shader, defines=defines)
|
||||
ui_draw_batch = batch_for_shader(ui_draw_shader, *draw_data)
|
||||
|
||||
|
||||
class UI_Draw:
|
||||
default_stylesheet = None
|
||||
|
||||
@staticmethod
|
||||
def load_stylesheet(path):
|
||||
UI_Draw.default_stylesheet = UI_Styling.from_file(path)
|
||||
|
||||
def update(self): pass
|
||||
|
||||
def draw(self, left, top, width, height, dpi_mult, style, texture_id=None, gputexture=None, texture_fit='fill', background_override=None, depth=None):
|
||||
def_color = (0,0,0,0)
|
||||
def get_v(style_key, def_val):
|
||||
v = style.get(style_key, def_val)
|
||||
return v if not isinstance(v, NumberUnit) else (v.val() * dpi_mult)
|
||||
|
||||
ui_draw_shader.bind()
|
||||
ui_draw_ubos.options.uMVPMatrix = gpu.matrix.get_projection_matrix() @ gpu.matrix.get_model_view_matrix()
|
||||
ui_draw_ubos.options.lrtb = (float(left), float(left + (width - 1)), float(top), float(top - (height - 1)))
|
||||
ui_draw_ubos.options.wh = (float(width), float(height), 0, 0)
|
||||
ui_draw_ubos.options.depth = (depth, 0, 0, 0)
|
||||
ui_draw_ubos.options.margin_lrtb = [ get_v(f'margin-{p}', 0) for p in ['left', 'right', 'top', 'bottom'] ]
|
||||
ui_draw_ubos.options.padding_lrtb = [ get_v(f'padding-{p}', 0) for p in ['left', 'right', 'top', 'bottom'] ]
|
||||
ui_draw_ubos.options.border_width_radius = [ get_v('border-width', 0), get_v('border-radius', 0), 0, 0 ]
|
||||
ui_draw_ubos.options.border_left_color = Color.as_vec4(get_v('border-left-color', def_color))
|
||||
ui_draw_ubos.options.border_right_color = Color.as_vec4(get_v('border-right-color', def_color))
|
||||
ui_draw_ubos.options.border_top_color = Color.as_vec4(get_v('border-top-color', def_color))
|
||||
ui_draw_ubos.options.border_bottom_color = Color.as_vec4(get_v('border-bottom-color', def_color))
|
||||
ui_draw_ubos.options.background_color = Color.as_vec4(background_override if background_override else get_v('background-color', def_color))
|
||||
ui_draw_ubos.options.image_settings = [ (1 if gputexture is not None else 0), style_to_image_scale.get(texture_fit, 0), 0, 0 ]
|
||||
if gputexture: ui_draw_shader.uniform_sampler('image', gputexture)
|
||||
ui_draw_ubos.update_shader()
|
||||
ui_draw_batch.draw(ui_draw_shader)
|
||||
|
||||
|
||||
ui_draw = Globals.set(UI_Draw())
|
||||
@@ -0,0 +1,91 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
# https://www.w3schools.com/jsref/obj_event.asp
|
||||
# https://javascript.info/bubbling-and-capturing
|
||||
class UI_Event:
|
||||
phases = [
|
||||
'none',
|
||||
'capturing',
|
||||
'at target',
|
||||
'bubbling',
|
||||
]
|
||||
|
||||
def __init__(self, target=None, mouse=None, button=None, key=None, clipboardData=None):
|
||||
self._eventPhase = 'none'
|
||||
self._cancelBubble = False
|
||||
self._cancelCapture = False
|
||||
self._target = target
|
||||
self._mouse = mouse
|
||||
self._button = button
|
||||
self._key = key
|
||||
self._clipboardData = clipboardData
|
||||
self._defaultPrevented = False
|
||||
|
||||
def stop_propagation(self):
|
||||
self.stop_bubbling()
|
||||
self.stop_capturing()
|
||||
def stop_bubbling(self):
|
||||
self._cancelBubble = True
|
||||
def stop_capturing(self):
|
||||
self._cancelCapture = True
|
||||
|
||||
def prevent_default(self):
|
||||
self._defaultPrevented = True
|
||||
|
||||
@property
|
||||
def event_phase(self): return self._eventPhase
|
||||
@event_phase.setter
|
||||
def event_phase(self, v):
|
||||
assert v in self.phases, "attempting to set event_phase to unknown value (%s)" % str(v)
|
||||
self._eventPhase = v
|
||||
|
||||
@property
|
||||
def bubbling(self):
|
||||
return self._eventPhase == 'bubbling' and not self._cancelBubble
|
||||
@property
|
||||
def capturing(self):
|
||||
return self._eventPhase == 'capturing' and not self._cancelCapture
|
||||
@property
|
||||
def atTarget(self):
|
||||
return self._eventPhase == 'at target'
|
||||
|
||||
@property
|
||||
def target(self): return self._target
|
||||
|
||||
@property
|
||||
def mouse(self): return self._mouse
|
||||
|
||||
@property
|
||||
def button(self): return self._button
|
||||
|
||||
@property
|
||||
def key(self): return self._key
|
||||
|
||||
@property
|
||||
def clipboardData(self): return self._clipboardData
|
||||
|
||||
@property
|
||||
def default_prevented(self): return self._defaultPrevented
|
||||
|
||||
@property
|
||||
def eventPhase(self): return self._eventPhase
|
||||
@@ -0,0 +1,122 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import traceback
|
||||
import contextlib
|
||||
from math import floor, ceil
|
||||
from inspect import signature
|
||||
from itertools import dropwhile, zip_longest
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
|
||||
from .blender import tag_redraw_all
|
||||
from .ui_styling import UI_Styling, ui_defaultstylings
|
||||
from .ui_core_utilities import helper_wraptext, convert_token_to_cursor
|
||||
from .fsm import FSM
|
||||
|
||||
from .boundvar import BoundVar
|
||||
from .debug import debugger, dprint, tprint
|
||||
from .decorators import debug_test_call, blender_version_wrapper, add_cache
|
||||
from .drawing import Drawing
|
||||
from .globals import Globals
|
||||
from .hasher import Hasher
|
||||
from .maths import Vec2D, Color, mid, Box2D, Size1D, Size2D, Point2D, RelPoint2D, Index2D, clamp, NumberUnit
|
||||
from .maths import floor_if_finite, ceil_if_finite
|
||||
from .profiler import profiler, time_it
|
||||
from .utils import iter_head, any_args, join
|
||||
|
||||
|
||||
class LineFitter:
|
||||
def __init__(self, *, left, top, width, height):
|
||||
self.box = Box2D(left=left, top=top, width=width, height=height)
|
||||
self.max_width = 0
|
||||
self.sum_height = 0
|
||||
self.lines = []
|
||||
self.current_line = None
|
||||
self.new_line()
|
||||
|
||||
def new_line(self):
|
||||
# width: sum of all widths added to current line
|
||||
# height: max of all heights added to current line
|
||||
if not self.is_current_line_empty():
|
||||
self.max_width = max(self.max_width, self.current_width)
|
||||
self.sum_height = self.sum_height + self.current_height
|
||||
self.lines.append(self.current.elements)
|
||||
self.current_line = []
|
||||
self.current_width = 0
|
||||
self.current_height = 0
|
||||
|
||||
def is_current_line_empty(self):
|
||||
return not self.current_line
|
||||
|
||||
@property
|
||||
def remaining_width(self): return self.box.width - self.current_width
|
||||
@property
|
||||
def remaining_height(self): return self.box.height - self.sum_height
|
||||
|
||||
def get_next_box(self):
|
||||
return Box2D(
|
||||
left = self.box.left + self.current_width,
|
||||
top = -(self.box.top + self.sum_height),
|
||||
width = self.box.width - self.current_width,
|
||||
height = self.box.height - self.sum_height,
|
||||
)
|
||||
|
||||
def add_element(self, element, size):
|
||||
# assuming element is placed in correct spot in line
|
||||
if not self.fit(size): self.new_line()
|
||||
pos = Box2D(
|
||||
left = self.box.left + self.current_width,
|
||||
top = -(self.box.top + self.sum_height),
|
||||
width = size.smallest_width(),
|
||||
height = size.smallest_height(),
|
||||
)
|
||||
self.current_line.append(element)
|
||||
self.current_width += size.smallest_width()
|
||||
self.current_height = max(self.current_height, size.smallest_height())
|
||||
return pos
|
||||
|
||||
def fit(self, size):
|
||||
if size.smallest_width() > self.remaining_width: return False
|
||||
if size.smallest_height() > self.remaining_height: return False
|
||||
return True
|
||||
|
||||
|
||||
class TableFitter:
|
||||
def __init__(self):
|
||||
self._cells = {} # keys are Index2D
|
||||
self._index = Index2D(0, 0)
|
||||
|
||||
def new_row(self):
|
||||
self._index.update(i=0, j_off=1)
|
||||
def new_col(self):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
DEBUG_COLOR_CLEAN = False
|
||||
DEBUG_PROPERTY = 'style' # selector, style, content, size, layout, blocks
|
||||
DEBUG_COLOR = 1 # 0:time since change, 1:time of change
|
||||
|
||||
DEBUG_DIRTY = False
|
||||
|
||||
DEBUG_LIST = False
|
||||
|
||||
CACHE_METHOD = 2 # 0:none, 1:only root, 2:hierarchical, 3:text leaves, 4:hierarchical but random
|
||||
|
||||
ASYNC_IMAGE_LOADING = True
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class UndoStack:
|
||||
def __init__(self, fn_create_state, fn_restore_state, *, max_size=100):
|
||||
self._fn_step = namedtuple('UndoStep', 'key repeatable state')
|
||||
self._fn_create = fn_create_state
|
||||
self._fn_restore = fn_restore_state
|
||||
self._max_size = max_size
|
||||
self.clear()
|
||||
|
||||
def _pop(self, *, undo=True):
|
||||
stack = (self._undo if undo else self._redo)
|
||||
return stack.pop()
|
||||
|
||||
def _restore(self, step, *args, **kwargs):
|
||||
self._fn_restore(step.state, *args, **kwargs)
|
||||
|
||||
def _push_step(self, key, *, repeatable=False, undo=True, clear=True):
|
||||
step = self._fn_step(key, repeatable, self._fn_create(key))
|
||||
if undo:
|
||||
self._undo.append(step)
|
||||
if clear:
|
||||
self._redo.clear()
|
||||
# limit stack size
|
||||
while len(self._undo) > self._max_size:
|
||||
self._undo.pop(0)
|
||||
else:
|
||||
self._redo.append(step)
|
||||
|
||||
def _is_empty(self, *, undo=True):
|
||||
return not bool(self._undo if undo else self._redo)
|
||||
|
||||
def keys(self, *, undo=True):
|
||||
stack = reversed(self._undo if undo else self._redo)
|
||||
return [step.key for step in stack]
|
||||
|
||||
def _top(self, *, undo=True):
|
||||
stack = (self._undo if undo else self._redo)
|
||||
return stack[-1] if stack else None
|
||||
|
||||
def top_key(self, *, undo=True):
|
||||
top = self._top(undo=undo)
|
||||
return top.key if top else None
|
||||
|
||||
def clear(self):
|
||||
self._undo = []
|
||||
self._redo = []
|
||||
self._changes = 0
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
return self._changes
|
||||
|
||||
def push(self, key, *, repeatable=False):
|
||||
# skip pushing to undo if action is repeatable and we are repeating actions
|
||||
top = self._top()
|
||||
if repeatable and top and top.repeatable and top.key == key: return
|
||||
self._push_step(key, repeatable=repeatable)
|
||||
self._changes += 1
|
||||
|
||||
def pop(self, *args, undo=True, **kwargs):
|
||||
if self._is_empty(undo=undo): return
|
||||
key = 'undo' if undo else 'redo'
|
||||
self._push_step(key, undo=not undo, clear=undo)
|
||||
step = self._pop(undo=undo)
|
||||
self._restore(step, *args, **kwargs)
|
||||
self._changes += 1
|
||||
|
||||
#### the following code is not working??
|
||||
# def restore(self, *args, **kwargs):
|
||||
# if self._is_empty(): return
|
||||
# step = self._top()
|
||||
# self._restore(step, *args, **kwargs)
|
||||
# self._redo.clear()
|
||||
# self._changes += 1
|
||||
|
||||
def cancel(self, *args, **kwargs):
|
||||
if self._is_empty(): return
|
||||
step = self._pop()
|
||||
self._restore(step, *args, **kwargs)
|
||||
self._changes += 1
|
||||
|
||||
def break_repeatable(self):
|
||||
if self._is_empty(): return
|
||||
self._top().repeatable = False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,740 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import time
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
|
||||
import bpy
|
||||
|
||||
from .blender import get_view3d_area, get_view3d_space, get_view3d_region
|
||||
from .debug import dprint
|
||||
from .decorators import blender_version_wrapper
|
||||
from .human_readable import convert_actions_to_human_readable, convert_human_readable_to_actions
|
||||
from .maths import Point2D, Vec2D
|
||||
from .timerhandler import TimerHandler
|
||||
from .utils import Dict
|
||||
from . import blender_preferences as bprefs
|
||||
|
||||
|
||||
action_to_char = {
|
||||
'ZERO': '0', 'NUMPAD_0': '0',
|
||||
'ONE': '1', 'NUMPAD_1': '1',
|
||||
'TWO': '2', 'NUMPAD_2': '2',
|
||||
'THREE': '3', 'NUMPAD_3': '3',
|
||||
'FOUR': '4', 'NUMPAD_4': '4',
|
||||
'FIVE': '5', 'NUMPAD_5': '5',
|
||||
'SIX': '6', 'NUMPAD_6': '6',
|
||||
'SEVEN': '7', 'NUMPAD_7': '7',
|
||||
'EIGHT': '8', 'NUMPAD_8': '8',
|
||||
'NINE': '9', 'NUMPAD_9': '9',
|
||||
'PERIOD': '.', 'NUMPAD_PERIOD': '.',
|
||||
'PLUS': '+', 'NUMPAD_PLUS': '+',
|
||||
'MINUS': '-', 'NUMPAD_MINUS': '-',
|
||||
'SLASH': '/', 'NUMPAD_SLASH': '/',
|
||||
'NUMPAD_ASTERIX': '*',
|
||||
'BACK_SLASH': '\\',
|
||||
'SPACE': ' ',
|
||||
'EQUAL': '=',
|
||||
'SEMI_COLON': ';', 'COMMA': ',',
|
||||
'LEFT_BRACKET': '[', 'RIGHT_BRACKET': ']',
|
||||
'QUOTE': "'", 'ACCENT_GRAVE': '`',
|
||||
'GRLESS': '>',
|
||||
|
||||
'A':'a', 'B':'b', 'C':'c', 'D':'d',
|
||||
'E':'e', 'F':'f', 'G':'g', 'H':'h',
|
||||
'I':'i', 'J':'j', 'K':'k', 'L':'l',
|
||||
'M':'m', 'N':'n', 'O':'o', 'P':'p',
|
||||
'Q':'q', 'R':'r', 'S':'s', 'T':'t',
|
||||
'U':'u', 'V':'v', 'W':'w', 'X':'x',
|
||||
'Y':'y', 'Z':'z',
|
||||
|
||||
'SHIFT+A':'A', 'SHIFT+B':'B', 'SHIFT+C':'C', 'SHIFT+D':'D',
|
||||
'SHIFT+E':'E', 'SHIFT+F':'F', 'SHIFT+G':'G', 'SHIFT+H':'H',
|
||||
'SHIFT+I':'I', 'SHIFT+J':'J', 'SHIFT+K':'K', 'SHIFT+L':'L',
|
||||
'SHIFT+M':'M', 'SHIFT+N':'N', 'SHIFT+O':'O', 'SHIFT+P':'P',
|
||||
'SHIFT+Q':'Q', 'SHIFT+R':'R', 'SHIFT+S':'S', 'SHIFT+T':'T',
|
||||
'SHIFT+U':'U', 'SHIFT+V':'V', 'SHIFT+W':'W', 'SHIFT+X':'X',
|
||||
'SHIFT+Y':'Y', 'SHIFT+Z':'Z',
|
||||
|
||||
'SHIFT+ZERO': ')',
|
||||
'SHIFT+ONE': '!',
|
||||
'SHIFT+TWO': '@',
|
||||
'SHIFT+THREE': '#',
|
||||
'SHIFT+FOUR': '$',
|
||||
'SHIFT+FIVE': '%',
|
||||
'SHIFT+SIX': '^',
|
||||
'SHIFT+SEVEN': '&',
|
||||
'SHIFT+EIGHT': '*',
|
||||
'SHIFT+NINE': '(',
|
||||
'SHIFT+PERIOD': '>',
|
||||
'SHIFT+PLUS': '+',
|
||||
'SHIFT+MINUS': '_',
|
||||
'SHIFT+SLASH': '?',
|
||||
'SHIFT+BACK_SLASH': '|',
|
||||
'SHIFT+EQUAL': '+',
|
||||
'SHIFT+SEMI_COLON': ':', 'SHIFT+COMMA': '<',
|
||||
'SHIFT+LEFT_BRACKET': '{', 'SHIFT+RIGHT_BRACKET': '}',
|
||||
'SHIFT+QUOTE': '"', 'SHIFT+ACCENT_GRAVE': '~',
|
||||
'SHIFT+GRLESS': '<',
|
||||
|
||||
'ESC': 'Escape',
|
||||
'BACK_SPACE': 'Backspace',
|
||||
'RET': 'Enter', 'NUMPAD_ENTER': 'Enter',
|
||||
'HOME': 'Home', 'END': 'End',
|
||||
'LEFT_ARROW': 'ArrowLeft', 'RIGHT_ARROW': 'ArrowRight',
|
||||
'UP_ARROW': 'ArrowUp', 'DOWN_ARROW': 'ArrowDown',
|
||||
'PAGE_UP': 'PageUp', 'PAGE_DOWN': 'PageDown',
|
||||
'DEL': 'Delete',
|
||||
'TAB': 'Tab',
|
||||
}
|
||||
|
||||
translate_action = {
|
||||
'WHEELINMOUSE': 'WHEELUPMOUSE',
|
||||
'WHEELOUTMOUSE': 'WHEELDOWNMOUSE',
|
||||
}
|
||||
|
||||
re_blenderop = re.compile(r'(?P<keymap>.+?) *\| *(?P<operator>.+)')
|
||||
|
||||
|
||||
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
|
||||
# https://docs.blender.org/api/current/bpy_types_enum_items/event_type_items.html
|
||||
# https://docs.blender.org/api/current/bpy_types_enum_items/event_value_items.html
|
||||
ndof_actions = {
|
||||
'NDOF_MOTION',
|
||||
|
||||
'NDOF_BUTTON', 'NDOF_BUTTON_FIT',
|
||||
'NDOF_BUTTON_TOP', 'NDOF_BUTTON_BOTTOM',
|
||||
'NDOF_BUTTON_LEFT', 'NDOF_BUTTON_RIGHT',
|
||||
'NDOF_BUTTON_FRONT', 'NDOF_BUTTON_BACK',
|
||||
|
||||
'NDOF_BUTTON_ISO1', 'NDOF_BUTTON_ISO2',
|
||||
|
||||
'NDOF_BUTTON_ROLL_CW', 'NDOF_BUTTON_ROLL_CCW',
|
||||
'NDOF_BUTTON_SPIN_CW', 'NDOF_BUTTON_SPIN_CCW',
|
||||
'NDOF_BUTTON_TILT_CW', 'NDOF_BUTTON_TILT_CCW',
|
||||
'NDOF_BUTTON_ROTATE', 'NDOF_BUTTON_PANZOOM',
|
||||
|
||||
'NDOF_BUTTON_DOMINANT',
|
||||
|
||||
'NDOF_BUTTON_PLUS', 'NDOF_BUTTON_MINUS', 'NDOF_BUTTON_ESC',
|
||||
'NDOF_BUTTON_ALT', 'NDOF_BUTTON_SHIFT', 'NDOF_BUTTON_CTRL',
|
||||
|
||||
'NDOF_BUTTON_1', 'NDOF_BUTTON_2', 'NDOF_BUTTON_3', 'NDOF_BUTTON_4', 'NDOF_BUTTON_5',
|
||||
'NDOF_BUTTON_6', 'NDOF_BUTTON_7', 'NDOF_BUTTON_8', 'NDOF_BUTTON_9', 'NDOF_BUTTON_10',
|
||||
'NDOF_BUTTON_A', 'NDOF_BUTTON_B', 'NDOF_BUTTON_C',
|
||||
}
|
||||
|
||||
mousebutton_actions = {
|
||||
'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE',
|
||||
'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE',
|
||||
}
|
||||
|
||||
ignore_actions = {}
|
||||
|
||||
nonprintable_actions = {
|
||||
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
|
||||
'TIMER', 'TIMER_REPORT', 'TIMERREGION',
|
||||
}
|
||||
|
||||
reset_actions = {
|
||||
# any time these actions are received, all action states will be flushed
|
||||
'WINDOW_DEACTIVATE',
|
||||
}
|
||||
|
||||
timer_actions = {
|
||||
'TIMER'
|
||||
}
|
||||
|
||||
mousemove_actions = {
|
||||
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
|
||||
}
|
||||
|
||||
trackpad_actions = {
|
||||
'TRACKPADPAN', 'TRACKPADZOOM',
|
||||
'MOUSEROTATE', 'MOUSESMARTZOOM',
|
||||
}
|
||||
|
||||
modifier_actions = {
|
||||
'OSKEY',
|
||||
'LEFT_CTRL', 'LEFT_SHIFT', 'LEFT_ALT',
|
||||
'RIGHT_CTRL', 'RIGHT_SHIFT', 'RIGHT_ALT',
|
||||
}
|
||||
|
||||
blender_operator_keymaps = [
|
||||
{
|
||||
'name': 'navigate',
|
||||
'operators': [
|
||||
'3D View | view3d.rotate', # Rotate View
|
||||
'3D View | view3d.move', # Move View
|
||||
'3D View | view3d.zoom', # Zoom View
|
||||
'3D View | view3d.dolly', # Dolly View
|
||||
'3D View | view3d.view_pan', # View Pan
|
||||
'3D View | view3d.view_orbit', # View Orbit
|
||||
'3D View | view3d.view_persportho', # View Persp/Ortho
|
||||
'3D View | view3d.viewnumpad', # View Numpad
|
||||
'3D View | view3d.view_axis', # View Axis
|
||||
'3D View | view2d.ndof', # NDOF Pan Zoom
|
||||
'3D View | view3d.ndof_orbit_zoom', # NDOF Orbit View with Zoom
|
||||
'3D View | view3d.ndof_orbit', # NDOF Orbit View
|
||||
'3D View | view3d.ndof_pan', # NDOF Pan View
|
||||
'3D View | view3d.ndof_all', # NDOF Move View
|
||||
'3D View | view3d.view_roll', # NDOF View Roll
|
||||
'3D View | view3d.view_selected', # View Selected
|
||||
'3D View | view3d.view_center_cursor', # Center View to Cursor
|
||||
'3D View | view3d.view_center_pick', # Center View to Mouse
|
||||
# '3D View | view3d.navigate', # View Navigation
|
||||
],
|
||||
}, {
|
||||
'name': 'blender window action',
|
||||
'operators': [
|
||||
# COMMENTED OUT, BECAUSE THERE IS A BUG WITH CONTEXT CHANGING!!
|
||||
# 'Screen | screen.screen_full_area',
|
||||
# 'Window | wm.window_fullscreen_toggle',
|
||||
],
|
||||
}, {
|
||||
'name': 'blender save',
|
||||
'operators': [
|
||||
'Window | wm.save_mainfile',
|
||||
],
|
||||
}, {
|
||||
'name': 'blender undo',
|
||||
'operators': [
|
||||
'Screen | ed.undo',
|
||||
],
|
||||
}, {
|
||||
'name': 'blender redo',
|
||||
'operators': [
|
||||
'Screen | ed.redo',
|
||||
],
|
||||
}, {
|
||||
'name': 'clipboard paste',
|
||||
'operators': [
|
||||
'Text | text.paste',
|
||||
'3D View | view3d.pastebuffer',
|
||||
'Console | console.paste',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
def blenderop_to_kmis(blenderop):
|
||||
keymaps = bpy.context.window_manager.keyconfigs.user.keymaps
|
||||
i18n_translate = bpy.app.translations.pgettext # bpy.app.translations.pgettext tries to translate the given parameter
|
||||
|
||||
m = re_blenderop.match(blenderop)
|
||||
if not m:
|
||||
print(f'blenderop_to_kmis: {blenderop}')
|
||||
return set()
|
||||
okeymap, ooperator = m['keymap'], m['operator']
|
||||
tkeymap, toperator = i18n_translate(okeymap), i18n_translate(ooperator)
|
||||
keymap = keymaps.get(okeymap, None) or keymaps.get(tkeymap, None)
|
||||
if not keymap: return set()
|
||||
return {
|
||||
kmi
|
||||
for kmi in keymap.keymap_items
|
||||
if all([
|
||||
kmi.active,
|
||||
kmi.idname in {ooperator, toperator},
|
||||
getattr(kmi, 'direction', 'ANY') == 'ANY'
|
||||
])
|
||||
}
|
||||
|
||||
def kmi_to_op_properties(kmi):
|
||||
path = kmi.idname.split('.')
|
||||
op = getattr(getattr(bpy.ops, path[0]), path[1])
|
||||
props = { k: kmi.path_resolve(f'properties.{k}') for k in kmi.properties.keys() }
|
||||
return (op, props)
|
||||
|
||||
def blenderop_to_actions(blenderop):
|
||||
return { kmi_to_action(kmi) for kmi in blenderop_to_kmis(blenderop) }
|
||||
|
||||
def action_strip_mods(action, *, ctrl=True, shift=True, alt=True, oskey=True, click=True, double_click=True, drag_click=True, mouse=False):
|
||||
if action is None: return None
|
||||
if mouse and 'MOUSE' in action: return ''
|
||||
if ctrl: action = action.replace('CTRL+', '')
|
||||
if shift: action = action.replace('SHIFT+', '')
|
||||
if alt: action = action.replace('ALT+', '')
|
||||
if oskey: action = action.replace('OSKEY+', '')
|
||||
if click: action = action.replace('+CLICK', '')
|
||||
if double_click: action = action.replace('+DOUBLE', '')
|
||||
if drag_click: action = action.replace('+DRAG', '')
|
||||
return action
|
||||
|
||||
def action_add_mods(action, *, ctrl=False, shift=False, alt=False, oskey=False, click=False, double_click=False, drag_click=False):
|
||||
if not action: return action
|
||||
action = translate_action.get(action, action)
|
||||
return ''.join([
|
||||
('CTRL+' if ctrl else ''),
|
||||
('SHIFT+' if shift else ''),
|
||||
('ALT+' if alt else ''),
|
||||
('OSKEY+' if oskey else ''),
|
||||
action,
|
||||
('+CLICK' if click and not (double_click or drag_click) else ''),
|
||||
('+DOUBLE' if double_click and not drag_click else ''),
|
||||
('+DRAG' if drag_click else ''),
|
||||
])
|
||||
|
||||
def kmi_to_action(kmi, *, event_type=None, click=False, double_click=False, drag_click=False):
|
||||
return action_add_mods(
|
||||
event_type or kmi.type,
|
||||
ctrl=kmi.ctrl, shift=kmi.shift, alt=kmi.alt, oskey=kmi.oskey,
|
||||
click=(kmi.value=='CLICK' or click),
|
||||
double_click=(kmi.value=='DOUBLE_CLICK' or double_click),
|
||||
drag_click=(kmi.value=='CLICK_DRAG' or drag_click),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Actions:
|
||||
@staticmethod
|
||||
def get_instance(context):
|
||||
if not hasattr(Actions, '_instance'):
|
||||
Actions._create = True
|
||||
Actions._instance = Actions(context)
|
||||
del Actions._create
|
||||
return Actions._instance
|
||||
|
||||
@staticmethod
|
||||
def done():
|
||||
if not hasattr(Actions, '_instance'): return
|
||||
del Actions._instance
|
||||
|
||||
def __init__(self, context):
|
||||
assert hasattr(Actions, '_create'), 'Do not create new instance of Actions. Instead, use Actions.get_instance()'
|
||||
assert not hasattr(Actions, '_instance'), 'Only create one instance of Actions! Then use Actions.get_instance()'
|
||||
|
||||
self.update_context(context)
|
||||
|
||||
# keymaps
|
||||
self.keymaps_universal = Dict(get_default_fn=set)
|
||||
self.keymaps_contextual = Dict(get_default_fn=set)
|
||||
|
||||
self.keymaps_blender_operators = Dict(get_default_fn=list)
|
||||
|
||||
# fill in universal and action keymaps
|
||||
self.keymaps_universal['navigate'] = trackpad_actions | ndof_actions
|
||||
for group in blender_operator_keymaps:
|
||||
group_name, blenderops = group['name'], group['operators']
|
||||
|
||||
# self.keymaps_universal.setdefault(group_name, set())
|
||||
self.keymaps_universal[group_name] |= {
|
||||
action
|
||||
for blenderop in blenderops
|
||||
for action in blenderop_to_actions(blenderop)
|
||||
}
|
||||
|
||||
for blenderop in blenderops:
|
||||
for kmi in blenderop_to_kmis(blenderop):
|
||||
action, op_props = kmi_to_action(kmi), kmi_to_op_properties(kmi)
|
||||
self.keymaps_blender_operators[action] += [op_props]
|
||||
|
||||
self.timer = False # is action from timer?
|
||||
self.time_delta = 0 # elapsed time since last "step" (units=seconds)
|
||||
self.time_last = time.time()
|
||||
|
||||
# IMPORTANT: the following properties are updated external to Actions
|
||||
self.hit_pos = None # position of raytraced mouse to scene (updated externally!)
|
||||
self.hit_norm = None # normal of raytraced mouse to scene (updated externally!)
|
||||
|
||||
self.reset_state(all_state=True)
|
||||
|
||||
def update_context(self, context):
|
||||
self.context = context
|
||||
self.screen = context.screen
|
||||
self.window = context.window
|
||||
|
||||
# try to find area, region, space, region_3d
|
||||
try:
|
||||
self.area = get_view3d_area(context)
|
||||
self.region = get_view3d_region(context)
|
||||
self.space = get_view3d_space(context)
|
||||
self.size = Vec2D((self.region.width, self.region.height))
|
||||
self.r3d = self.space.region_3d
|
||||
except Exception as e:
|
||||
print(f'******************************************')
|
||||
print(f'Addon Common: Could not find VIEW_3D area!')
|
||||
print(f'Exception: {e}')
|
||||
self.area = None
|
||||
self.region = None
|
||||
self.size = None
|
||||
self.r3d = None
|
||||
|
||||
|
||||
def reset_state(self, all_state=False):
|
||||
self.actions_using = set()
|
||||
self.actions_pressed = set()
|
||||
self.actions_prevtime = dict() # previous time when action was pressed
|
||||
self.now_pressed = dict() # currently pressed keys. key=stripped event type, value=full event type (includes modifiers)
|
||||
self.just_pressed = None
|
||||
self.last_pressed = None
|
||||
self.event_type = None
|
||||
|
||||
# indicates if modifier keys are currently pressed
|
||||
self.ctrl = False # note: will be true if either ctrl_left or ctrl_right are true
|
||||
self.ctrl_left = False
|
||||
self.ctrl_right = False
|
||||
self.shift = False
|
||||
self.shift_left = False
|
||||
self.shift_right = False
|
||||
self.alt = False
|
||||
self.alt_left = False
|
||||
self.alt_right = False
|
||||
|
||||
# non-keyboard and non-mouse properties
|
||||
self.trackpad = False # is current action from trackpad?
|
||||
self.ndof = False # is current action from NDOF?
|
||||
self.scroll = (0, 0)
|
||||
|
||||
# mouse-related properties
|
||||
if all_state:
|
||||
self.mouse_select = bprefs.mouse_select()
|
||||
self.mouse = None # current mouse position
|
||||
self.mouse_prev = None # previous mouse position
|
||||
self.mouse_lastb = None # last button pressed on mouse
|
||||
self.mousemove = False # is the current action a mouse move?
|
||||
self.mousemove_prev = False # was the previous action a mouse move?
|
||||
self.mousemove_stop = False # did the mouse just stop moving?
|
||||
self.mousedown = None # mouse position when a mouse button was pressed
|
||||
self.mousedown_left = None # mouse position when LMB was pressed
|
||||
self.mousedown_middle = None # mouse position when MMB was pressed
|
||||
self.mousedown_right = None # mouse position when RMB was pressed
|
||||
self.mousedown_drag = False # is user dragging?
|
||||
|
||||
# indicates if currently navigating
|
||||
self.is_navigating = False
|
||||
|
||||
def call_action_operator(self, action, *args, **kwargs):
|
||||
ops_props = self.keymaps_blender_operators[action]
|
||||
if not ops_props: return
|
||||
try:
|
||||
op, props = ops_props[0]
|
||||
# print(f'Invoking {action} {op} {props}')
|
||||
ret = op('INVOKE_DEFAULT', *args, **kwargs, **props)
|
||||
except Exception as e:
|
||||
print(f'Actions.call_action_operator: Caught Exception while calling Blender operator')
|
||||
print(f' {action=}')
|
||||
print(f' {op=}')
|
||||
print(f' {props=}')
|
||||
print(e)
|
||||
ret = None
|
||||
return ret
|
||||
|
||||
actions_prevtime_default = (0, 0, float('inf'))
|
||||
def get_last_press_time(self, event_type):
|
||||
return self.actions_prevtime.get(event_type, self.actions_prevtime_default)
|
||||
|
||||
def update(self, context, event, fn_debug=None):
|
||||
if event.type in reset_actions:
|
||||
# print(f'Actions.update: resetting state')
|
||||
self.reset_state()
|
||||
return
|
||||
|
||||
self.unpress()
|
||||
|
||||
self.update_context(context)
|
||||
|
||||
event_type, pressed = event.type, (event.value == 'PRESS')
|
||||
|
||||
if pressed:
|
||||
_,prevtime,_ = self.get_last_press_time(event_type)
|
||||
curtime = time.time()
|
||||
self.actions_prevtime[event_type] = (prevtime, curtime, curtime - prevtime)
|
||||
|
||||
self.event_type = event_type
|
||||
self.mousemove_prev = self.mousemove
|
||||
self.timer = (event_type in timer_actions)
|
||||
self.mousemove = (event_type in mousemove_actions)
|
||||
self.trackpad = (event_type in trackpad_actions)
|
||||
self.ndof = (event_type in ndof_actions)
|
||||
self.mousemove_stop = not self.mousemove and self.mousemove_prev
|
||||
self.scroll = (0, 0) # to be set below
|
||||
|
||||
# record held modifiers
|
||||
self.ctrl = event.ctrl
|
||||
self.alt = event.alt
|
||||
self.shift = event.shift
|
||||
self.oskey = event.oskey
|
||||
|
||||
# handle completely ignorable actions (if any)
|
||||
if event_type in ignore_actions: return
|
||||
|
||||
if fn_debug and event_type not in nonprintable_actions:
|
||||
fn_debug('update start', event_type=event_type, event_value=event.value)
|
||||
|
||||
# ignore modifier key presses, as they do not "fire" pressed events
|
||||
if event_type in modifier_actions:
|
||||
return
|
||||
|
||||
# handle timer event
|
||||
if self.timer:
|
||||
time_cur = time.time()
|
||||
self.time_delta = self.time_last - time_cur
|
||||
self.time_last = time_cur
|
||||
return
|
||||
|
||||
self.is_navigating = False
|
||||
|
||||
# handle mouse move event
|
||||
if self.mousemove:
|
||||
self.mouse_prev = self.mouse
|
||||
self.mouse = Point2D((float(event.mouse_region_x), float(event.mouse_region_y)))
|
||||
|
||||
if not self.mousedown:
|
||||
self.mousedown_drag = False
|
||||
return
|
||||
if self.mousedown_drag: return
|
||||
if (self.mouse - self.mousedown).length <= bprefs.mouse_drag(): return
|
||||
|
||||
self.mousedown_drag = True
|
||||
# can user drag non-mouse keys??
|
||||
if self.mousedown_left: event_type = 'LEFTMOUSE'
|
||||
elif self.mousedown_middle: event_type = 'MIDDLEMOUSE'
|
||||
elif self.mousedown_right: event_type = 'RIGHTMOUSE'
|
||||
self.event_type = event_type
|
||||
pressed = True
|
||||
elif event_type in {'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE'} and not pressed:
|
||||
# release drag when mouse button is released
|
||||
# can user drag non-mouse keys??
|
||||
self.mousedown_drag = False
|
||||
|
||||
# handle trackpad event
|
||||
if self.trackpad:
|
||||
pressed = True
|
||||
self.scroll = (event.mouse_x - event.mouse_prev_x, event.mouse_y - event.mouse_prev_y)
|
||||
|
||||
# handle navigation event
|
||||
full_event_type = action_add_mods(
|
||||
event_type,
|
||||
ctrl=self.ctrl, alt=self.alt,
|
||||
shift=self.shift, oskey=self.oskey,
|
||||
drag_click=self.mousedown_drag,
|
||||
)
|
||||
|
||||
self.is_navigating = (full_event_type in self.keymaps_universal['navigate'])
|
||||
if self.is_navigating:
|
||||
self.unuse(full_event_type)
|
||||
|
||||
mouse_event = event_type in mousebutton_actions and not self.is_navigating
|
||||
if mouse_event:
|
||||
if pressed:
|
||||
if self.mouse_lastb != event_type: self.mousedown_drag = False
|
||||
self.mousedown = Point2D((float(event.mouse_region_x), float(event.mouse_region_y)))
|
||||
if event_type == 'LEFTMOUSE': self.mousedown_left = self.mousedown
|
||||
elif event_type == 'MIDDLEMOUSE': self.mousedown_middle = self.mousedown
|
||||
elif event_type == 'RIGHTMOUSE': self.mousedown_right = self.mousedown
|
||||
self.mouse_lastb = event_type
|
||||
else:
|
||||
self.mousedown = None
|
||||
self.mousedown_left = None
|
||||
self.mousedown_middle = None
|
||||
self.mousedown_right = None
|
||||
self.mousedown_drag = False
|
||||
|
||||
ftype = kmi_to_action(event, event_type=event_type, drag_click=self.mousedown_drag and mouse_event)
|
||||
if pressed:
|
||||
# if event_type not in self.now_pressed:
|
||||
# self.just_pressed = ftype
|
||||
self.just_pressed = ftype
|
||||
if 'WHEELUPMOUSE' in ftype or 'WHEELDOWNMOUSE' in ftype:
|
||||
# mouse wheel actions have no release, so handle specially
|
||||
self.just_pressed = ftype
|
||||
else:
|
||||
self.now_pressed[event_type] = ftype
|
||||
self.last_pressed = ftype
|
||||
else:
|
||||
if event_type in self.now_pressed:
|
||||
if event_type in mousebutton_actions and not self.mousedown_drag:
|
||||
_,_,deltatime = self.get_last_press_time(event_type)
|
||||
single = (deltatime > bprefs.mouse_doubleclick()) or (self.mouse_lastb != event_type)
|
||||
self.just_pressed = kmi_to_action(event, event_type=event_type, click=single, double_click=not single)
|
||||
else:
|
||||
del self.now_pressed[event_type]
|
||||
|
||||
if fn_debug and event_type not in nonprintable_actions:
|
||||
fn_debug(
|
||||
'update end',
|
||||
ftype=ftype,
|
||||
pressed=pressed,
|
||||
just_pressed=self.just_pressed,
|
||||
now_pressed=self.now_pressed,
|
||||
last_pressed=self.last_pressed,
|
||||
)
|
||||
|
||||
def _convert(self, action):
|
||||
return (self.keymaps_universal[action] | self.keymaps_contextual[action]) or { action }
|
||||
def convert(self, actions):
|
||||
match actions:
|
||||
case set(): pass # already a set; no need to do anything
|
||||
case str(): actions = { actions } # passed only a string
|
||||
case list(): actions = set(actions) # prevent duplicate actions by converting to set
|
||||
case _: actions = { actions } # catch all (should not happen)
|
||||
return { a for action in actions for a in self._convert(action) }
|
||||
|
||||
def to_human_readable(self, actions, *, sep=',', onlyfirst=None, visible=False):
|
||||
if type(actions) is str: actions = { actions }
|
||||
actions = [ act for action in actions for act in self.convert(action) ]
|
||||
return convert_actions_to_human_readable(actions, sep=sep, onlyfirst=onlyfirst, visible=visible)
|
||||
|
||||
def from_human_readable(self, actions):
|
||||
return convert_human_readable_to_actions(actions)
|
||||
|
||||
|
||||
def unuse(self, actions, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False):
|
||||
if not actions: return
|
||||
strip_mods = lambda p: action_strip_mods(
|
||||
p,
|
||||
ctrl = ignorectrl or ignoremods,
|
||||
shift = ignoreshift or ignoremods,
|
||||
alt = ignorealt or ignoremods,
|
||||
oskey = ignoreoskey or ignoremods,
|
||||
click = ignoreclick or ignoremulti,
|
||||
double_click = ignoredouble or ignoremulti,
|
||||
drag_click = ignoredrag or ignoremulti,
|
||||
)
|
||||
actions = [ strip_mods(p) for p in self.convert(actions) ]
|
||||
keys = [k for k,v in self.now_pressed.items() if strip_mods(v) in actions]
|
||||
for k in keys: del self.now_pressed[k]
|
||||
self.mousedown = None
|
||||
self.mousedown_left = None
|
||||
self.mousedown_middle = None
|
||||
self.mousedown_right = None
|
||||
self.mousedown_drag = False
|
||||
self.unpress()
|
||||
|
||||
def unpress(self):
|
||||
if not self.just_pressed: return
|
||||
just_pressed_no_mods = action_strip_mods(self.just_pressed)
|
||||
if just_pressed_no_mods in self.now_pressed:
|
||||
if '+CLICK' in self.just_pressed:
|
||||
del self.now_pressed[just_pressed_no_mods]
|
||||
elif '+DOUBLE' in self.just_pressed:
|
||||
del self.now_pressed[just_pressed_no_mods]
|
||||
self.just_pressed = None
|
||||
|
||||
def using(self, actions, using_all=False, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False):
|
||||
if actions is None: return False
|
||||
strip_mods = lambda p: action_strip_mods(
|
||||
p,
|
||||
ctrl = ignorectrl or ignoremods,
|
||||
shift = ignoreshift or ignoremods,
|
||||
alt = ignorealt or ignoremods,
|
||||
oskey = ignoreoskey or ignoremods,
|
||||
click = ignoreclick or ignoremulti,
|
||||
double_click = ignoredouble or ignoremulti,
|
||||
drag_click = ignoredrag or ignoremulti,
|
||||
)
|
||||
actions = [ strip_mods(p) for p in self.convert(actions) ]
|
||||
results = [ strip_mods(p) in actions for p in self.now_pressed.values() ]
|
||||
return all(results) if using_all else any(results)
|
||||
|
||||
def using_onlymods(self, actions, exact=True):
|
||||
if actions is None: return False
|
||||
def action_good(action):
|
||||
nonlocal exact
|
||||
act_c = 'CTRL+' in action
|
||||
act_s = 'SHIFT+' in action
|
||||
act_a = 'ALT+' in action
|
||||
ret = True
|
||||
if exact:
|
||||
ret &= act_c == self.ctrl
|
||||
ret &= act_s == self.shift
|
||||
ret &= act_a == self.alt
|
||||
else:
|
||||
ret &= not (act_c and self.ctrl)
|
||||
ret &= not (act_s and self.shift)
|
||||
ret &= not (act_a and self.alt)
|
||||
return ret
|
||||
return any(action_good(action) for action in self.convert(actions))
|
||||
|
||||
def pressed(self, actions, unpress=True, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False, ignoremouse=False):
|
||||
if actions is None: return False
|
||||
if not self.just_pressed: return False
|
||||
actions = self.convert(actions)
|
||||
just_pressed = action_strip_mods(
|
||||
self.just_pressed,
|
||||
ctrl = ignorectrl or ignoremods,
|
||||
shift = ignoreshift or ignoremods,
|
||||
alt = ignorealt or ignoremods,
|
||||
oskey = ignoreoskey or ignoremods,
|
||||
click = ignoreclick or ignoremulti,
|
||||
double_click = ignoredouble or ignoremulti,
|
||||
drag_click = ignoredrag or ignoremulti,
|
||||
mouse = ignoremouse,
|
||||
)
|
||||
if not just_pressed: return False
|
||||
ret = just_pressed in actions
|
||||
if ret and unpress: self.unpress()
|
||||
return ret
|
||||
|
||||
def released(self, actions, released_all=False, ignoredrag=True, **kwargs):
|
||||
if actions is None: return False
|
||||
return not self.using(actions, using_all=released_all, ignoredrag=ignoredrag, **kwargs)
|
||||
|
||||
def warp_mouse(self, xy:Point2D):
|
||||
rx,ry = self.region.x,self.region.y
|
||||
mx,my = xy
|
||||
self.context.window.cursor_warp(rx + mx, ry + my)
|
||||
|
||||
def valid_mouse(self):
|
||||
if self.mouse is None: return False
|
||||
mx,my = self.mouse
|
||||
sx,sy = self.size
|
||||
return 0 <= mx < sx and 0 <= my < sy
|
||||
|
||||
def as_char(self, ftype):
|
||||
return action_to_char.get(ftype, '')
|
||||
|
||||
def start_timer(self, hz, enabled=True):
|
||||
return TimerHandler(hz, context=self.context, enabled=enabled)
|
||||
|
||||
|
||||
class ActionHandler:
|
||||
_actions = None
|
||||
|
||||
def __init__(self, context, keymap={}):
|
||||
if not ActionHandler._actions:
|
||||
ActionHandler._actions = Actions.get_instance(context)
|
||||
|
||||
self.__dict__['_keymap'] = Dict({
|
||||
k: ({actions} if type(actions) is str else set(actions))
|
||||
for (k, actions) in keymap.items()
|
||||
}, get_default_fn=set)
|
||||
|
||||
def __getattr__(self, key):
|
||||
if not ActionHandler._actions: return None
|
||||
ActionHandler._actions.keymaps_contextual = self._keymap
|
||||
return getattr(ActionHandler._actions, key)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if not ActionHandler._actions: return
|
||||
ActionHandler._actions.keymaps_contextual = self._keymap
|
||||
return setattr(ActionHandler._actions, key, value)
|
||||
|
||||
def done(self):
|
||||
if not ActionHandler._actions: return
|
||||
ActionHandler._actions.done()
|
||||
ActionHandler._actions = None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import glob
|
||||
import time
|
||||
import inspect
|
||||
import operator
|
||||
import itertools
|
||||
import importlib
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .blender_preferences import get_preferences
|
||||
from .profiler import profiler
|
||||
from .debug import dprint, debugger
|
||||
|
||||
|
||||
def normalize_triplequote(
|
||||
s,
|
||||
*,
|
||||
remove_trailing_spaces=True,
|
||||
remove_first_leading_newline=True,
|
||||
remove_all_leading_newlines=False,
|
||||
dedent=True,
|
||||
remove_all_trailing_newlines=True,
|
||||
ensure_trailing_newline=True,
|
||||
):
|
||||
'''
|
||||
todo:
|
||||
- (re)wrap text to given line length
|
||||
- sub '\n\n' for '\n\n\n+'
|
||||
- replace HTML chars with UTF?
|
||||
'''
|
||||
if remove_trailing_spaces:
|
||||
s = '\n'.join(l.rstrip() for l in s.splitlines())
|
||||
if remove_all_leading_newlines:
|
||||
s = re.sub(r'^\n+', '', s)
|
||||
elif remove_first_leading_newline:
|
||||
s = re.sub(r'^\n', '', s)
|
||||
if dedent:
|
||||
lines = s.splitlines()
|
||||
indent = min((len(line) - len(line.lstrip()) for line in lines if line.lstrip()), default=0)
|
||||
s = '\n'.join(line[indent:] for line in lines)
|
||||
if remove_all_trailing_newlines:
|
||||
s = re.sub(r'\n+$', '', s)
|
||||
if ensure_trailing_newline:
|
||||
if not s.endswith('\n'):
|
||||
s += '\n'
|
||||
return s
|
||||
|
||||
|
||||
##################################################
|
||||
|
||||
StructRNA = bpy.types.bpy_struct
|
||||
def still_registered(self, oplist):
|
||||
if getattr(still_registered, 'is_broken', False): return False
|
||||
def is_registered():
|
||||
cur = bpy.ops
|
||||
for n in oplist:
|
||||
if not hasattr(cur, n): return False
|
||||
cur = getattr(cur, n)
|
||||
try: StructRNA.path_resolve(self, "properties")
|
||||
except:
|
||||
print('no properties!')
|
||||
return False
|
||||
return True
|
||||
if is_registered(): return True
|
||||
still_registered.is_broken = True
|
||||
print('bpy.ops.%s is no longer registered!' % '.'.join(oplist))
|
||||
return False
|
||||
|
||||
registered_objects = {}
|
||||
def registered_object_add(self):
|
||||
global registered_objects
|
||||
opid = self.operator_id
|
||||
print('Registering bpy.ops.%s' % opid)
|
||||
registered_objects[opid] = (self, opid.split('.'))
|
||||
|
||||
def registered_check():
|
||||
global registered_objects
|
||||
return all(still_registered(s, o) for (s, o) in registered_objects.values())
|
||||
|
||||
|
||||
#################################################
|
||||
|
||||
|
||||
# def find_and_import_all_subclasses(cls, root_path=None):
|
||||
# here_path = os.path.realpath(os.path.dirname(__file__))
|
||||
# if root_path is None:
|
||||
# root_path = os.path.realpath(os.path.join(here_path, '..'))
|
||||
|
||||
# touched_paths = set()
|
||||
# found_subclasses = set()
|
||||
|
||||
# def search(root):
|
||||
# nonlocal touched_paths, found_subclasses, here_path
|
||||
|
||||
# root = os.path.realpath(root)
|
||||
# if root in touched_paths: return
|
||||
# touched_paths.add(root)
|
||||
|
||||
# relpath = os.path.relpath(root, here_path)
|
||||
# #print(' relpath: %s' % relpath)
|
||||
|
||||
# for path in glob.glob(os.path.join(root, '*')):
|
||||
# if os.path.isdir(path):
|
||||
# if not path.endswith('__pycache__'):
|
||||
# search(path)
|
||||
# continue
|
||||
# if os.path.splitext(path)[1] != '.py':
|
||||
# continue
|
||||
|
||||
# try:
|
||||
# pyfile = os.path.splitext(os.path.basename(path))[0]
|
||||
# if pyfile == '__init__': continue
|
||||
# pyfile = os.path.join(relpath, pyfile)
|
||||
# pyfile = re.sub(r'\\', '/', pyfile)
|
||||
# if pyfile.startswith('./'): pyfile = pyfile[2:]
|
||||
# level = pyfile.count('..')
|
||||
# pyfile = re.sub(r'^(\.\./)*', '', pyfile)
|
||||
# pyfile = re.sub('/', '.', pyfile)
|
||||
# #print(' Searching: %s (%d, %s)' % (pyfile, level, path))
|
||||
# try:
|
||||
# tmp = importlib.__import__(pyfile, globals(), locals(), [], level=level+1)
|
||||
# except Exception as e:
|
||||
# print('Caught exception while attempting to search for classes')
|
||||
# print(' cls: %s' % str(cls))
|
||||
# print(' pyfile: %s' % pyfile)
|
||||
# print(' %s' % str(e))
|
||||
# #print(' Could not import')
|
||||
# continue
|
||||
# for tk in dir(tmp):
|
||||
# m = getattr(tmp, tk)
|
||||
# if not inspect.ismodule(m): continue
|
||||
# for k in dir(m):
|
||||
# v = getattr(m, k)
|
||||
# if not inspect.isclass(v): continue
|
||||
# if v is cls: continue
|
||||
# if not issubclass(v, cls): continue
|
||||
# # v is a subclass of cls, so add it to the global namespace
|
||||
# #print(' Found %s in %s' % (str(v), pyfile))
|
||||
# globals()[k] = v
|
||||
# found_subclasses.add(v)
|
||||
# except Exception as e:
|
||||
# print('Exception occurred while searching %s' % path)
|
||||
# debugger.print_exception()
|
||||
|
||||
# #print('Searching for class %s' % str(cls))
|
||||
# #print(' cwd: %s' % os.getcwd())
|
||||
# #print(' Root: %s' % root_path)
|
||||
# search(root_path)
|
||||
# return found_subclasses
|
||||
|
||||
|
||||
#########################################################
|
||||
|
||||
def delay_exec(action, f_globals=None, f_locals=None, ordered_parameters=None, precall=None):
|
||||
if f_globals is None or f_locals is None:
|
||||
frame = inspect.currentframe().f_back # get frame of calling function
|
||||
if f_globals is None: f_globals = frame.f_globals # get globals of calling function
|
||||
if f_locals is None: f_locals = frame.f_locals # get locals of calling function
|
||||
def run_it(*args, **kwargs):
|
||||
# args are ignored!?
|
||||
nf_locals = dict(f_locals)
|
||||
if ordered_parameters:
|
||||
for k,v in zip(ordered_parameters, args):
|
||||
nf_locals[k] = v
|
||||
nf_locals.update(kwargs)
|
||||
try:
|
||||
if precall: precall(nf_locals)
|
||||
return exec(action, f_globals, nf_locals)
|
||||
except Exception as e:
|
||||
print('Caught exception while trying to run a delay_exec')
|
||||
print(' action:', action)
|
||||
print(' except:', e)
|
||||
raise e
|
||||
return run_it
|
||||
|
||||
#########################################################
|
||||
|
||||
|
||||
# def git_info(start_at_caller=True):
|
||||
# if start_at_caller:
|
||||
# path_root = os.path.abspath(inspect.stack()[1][1])
|
||||
# else:
|
||||
# path_root = os.path.abspath(os.path.dirname(__file__))
|
||||
# try:
|
||||
# path_git_head = None
|
||||
# while path_root:
|
||||
# path_test = os.path.join(path_root, '.git', 'HEAD')
|
||||
# if os.path.exists(path_test):
|
||||
# # found it!
|
||||
# path_git_head = path_test
|
||||
# break
|
||||
# if os.path.split(path_root)[1] in {'addons', 'addons_contrib'}:
|
||||
# break
|
||||
# path_root = os.path.dirname(path_root) # try next level up
|
||||
# if not path_git_head:
|
||||
# # could not find .git folder
|
||||
# return None
|
||||
# path_git_ref = open(path_git_head).read().split()[1]
|
||||
# if not path_git_ref.startswith('refs/heads/'):
|
||||
# print('git detected, but HEAD uses unexpected format')
|
||||
# return None
|
||||
# path_git_ref = path_git_ref[len('refs/heads/'):]
|
||||
# git_ref_fullpath = os.path.join(path_root, '.git', 'logs', 'refs', 'heads', path_git_ref)
|
||||
# if not os.path.exists(git_ref_fullpath):
|
||||
# print('git detected, but could not find ref file %s' % git_ref_fullpath)
|
||||
# return None
|
||||
# log = open(git_ref_fullpath).read().splitlines()
|
||||
# commit = log[-1].split()[1]
|
||||
# return ('%s %s' % (path_git_ref, commit))
|
||||
# except Exception as e:
|
||||
# print('An exception occurred while checking git info')
|
||||
# print(e)
|
||||
# return None
|
||||
|
||||
|
||||
|
||||
|
||||
#########################################################
|
||||
|
||||
|
||||
|
||||
|
||||
def kwargopts(kwargs, defvals=None, **mykwargs):
|
||||
opts = defvals.copy() if defvals else {}
|
||||
opts.update(mykwargs)
|
||||
opts.update(kwargs)
|
||||
if 'opts' in kwargs: opts.update(opts['opts'])
|
||||
def factory():
|
||||
class Opts():
|
||||
''' pretend to be a dictionary, but also add . access fns '''
|
||||
def __init__(self):
|
||||
self.touched = set()
|
||||
def __getattr__(self, opt):
|
||||
self.touched.add(opt)
|
||||
return opts[opt]
|
||||
def __getitem__(self, opt):
|
||||
self.touched.add(opt)
|
||||
return opts[opt]
|
||||
def __len__(self): return len(opts)
|
||||
def has_key(self, opt): return opt in opts
|
||||
def keys(self): return opts.keys()
|
||||
def values(self): return opts.values()
|
||||
def items(self): return opts.items()
|
||||
def __contains__(self, opt): return opt in opts
|
||||
def __iter__(self): return iter(opts)
|
||||
def print_untouched(self):
|
||||
print('untouched: %s' % str(set(opts.keys()) - self.touched))
|
||||
def pass_through(self, *args):
|
||||
return {key:self[key] for key in args}
|
||||
return Opts()
|
||||
return factory()
|
||||
|
||||
|
||||
|
||||
def kwargs_translate(key_from, key_to, kwargs):
|
||||
if key_from in kwargs:
|
||||
kwargs[key_to] = kwargs[key_from]
|
||||
del kwargs[key_from]
|
||||
|
||||
def kwargs_splitter(kwargs, *, keys=None, fn=None):
|
||||
if keys is not None:
|
||||
if type(keys) is str: keys = [keys]
|
||||
kw = {k:v for (k,v) in kwargs.items() if k in keys}
|
||||
elif fn is not None:
|
||||
kw = {k:v for (k,v) in kwargs.items() if fn(k, v)}
|
||||
else:
|
||||
assert False, f'Must specify either keys or fn'
|
||||
for k in kw.keys():
|
||||
del kwargs[k]
|
||||
return kw
|
||||
|
||||
|
||||
def any_args(*args):
|
||||
return any(bool(a) for a in args)
|
||||
|
||||
def get_and_discard(d, k, default=None):
|
||||
if k not in d: return default
|
||||
v = d[k]
|
||||
del d[k]
|
||||
return v
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
|
||||
|
||||
def abspath(*args, frame_depth=1, **kwargs):
|
||||
frame = inspect.currentframe()
|
||||
for i in range(frame_depth): frame = frame.f_back
|
||||
module = inspect.getmodule(frame)
|
||||
path = os.path.dirname(module.__file__)
|
||||
return os.path.abspath(os.path.join(path, *args, **kwargs))
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
|
||||
def strshort(s, l=50):
|
||||
s = str(s)
|
||||
return s[:l] + ('...' if len(s) > l else '')
|
||||
|
||||
|
||||
def join(sep, iterable, preSep='', postSep='', toStr=str):
|
||||
'''
|
||||
this function adds features on to sep.join(iterable)
|
||||
if iterable is not empty, preSep is prepended and postSep is appended
|
||||
also, all items of iterable are turned to strings using toStr, which can be customized
|
||||
ex: join(', ', [1,2,3]) => '1, 2, 3'
|
||||
ex: join('.', ['foo', 'bar'], preSep='.') => '.foo.bar'
|
||||
'''
|
||||
s = sep.join(map(toStr, iterable))
|
||||
if not s: return ''
|
||||
return f'{preSep}{s}{postSep}'
|
||||
|
||||
def accumulate_last(iterable, *args, **kwargs):
|
||||
# returns last result when accumulating
|
||||
# https://docs.python.org/3.7/library/itertools.html#itertools.accumulate
|
||||
final = None
|
||||
for step in itertools.accumulate(iterable, *args, **kwargs):
|
||||
final = step
|
||||
return final
|
||||
|
||||
def selection_mouse():
|
||||
select_type = get_preferences().inputs.select_mouse
|
||||
return ['%sMOUSE' % select_type, 'SHIFT+%sMOUSE' % select_type]
|
||||
|
||||
def get_settings():
|
||||
if not hasattr(get_settings, 'cache'):
|
||||
addons = get_preferences().addons
|
||||
folderpath = os.path.dirname(os.path.abspath(__file__))
|
||||
while folderpath:
|
||||
folderpath,foldername = os.path.split(folderpath)
|
||||
if foldername in {'lib','addons', 'addons_contrib'}: continue
|
||||
if foldername in addons: break
|
||||
else:
|
||||
assert False, 'Could not find non-"lib" folder'
|
||||
if not addons[foldername].preferences: return None
|
||||
get_settings.cache = addons[foldername].preferences
|
||||
return get_settings.cache
|
||||
|
||||
def get_dpi():
|
||||
system_preferences = get_preferences().system
|
||||
factor = getattr(system_preferences, "pixel_size", 1)
|
||||
return int(system_preferences.dpi * factor)
|
||||
|
||||
def get_dpi_factor():
|
||||
return get_dpi() / 72
|
||||
|
||||
def blender_version():
|
||||
major,minor,rev = bpy.app.version
|
||||
# '%03d.%03d.%03d' % (major, minor, rev)
|
||||
return '%d.%02d' % (major,minor)
|
||||
|
||||
|
||||
def iter_head(iterable, *, default=None):
|
||||
return next(iter(iterable), default)
|
||||
try:
|
||||
return next(iter(iterable))
|
||||
except StopIteration:
|
||||
return default
|
||||
|
||||
def iter_running_sum(lw):
|
||||
s = 0
|
||||
for w in lw:
|
||||
s += w
|
||||
yield (w,s)
|
||||
|
||||
def iter_pairs(items, wrap, repeat=False):
|
||||
if not items: return
|
||||
while True:
|
||||
for i0,i1 in zip(items[:-1],items[1:]): yield i0,i1
|
||||
if wrap: yield items[-1],items[0]
|
||||
if not repeat: return
|
||||
|
||||
def rotate_cycle(cycle, offset):
|
||||
l = len(cycle)
|
||||
return [cycle[(l + ((i - offset) % l)) % l] for i in range(l)]
|
||||
|
||||
def max_index(vals, key=None):
|
||||
if not key: return max(enumerate(vals), key=lambda ival:ival[1])[0]
|
||||
return max(enumerate(vals), key=lambda ival:key(ival[1]))[0]
|
||||
|
||||
def min_index(vals, key=None):
|
||||
if not key: return min(enumerate(vals), key=lambda ival:ival[1])[0]
|
||||
return min(enumerate(vals), key=lambda ival:key(ival[1]))[0]
|
||||
|
||||
|
||||
def shorten_floats(s):
|
||||
# reduces number of digits (for float) found in a string
|
||||
# useful for reducing noise of printing out a Vector, Buffer, Matrix, etc.
|
||||
s = re.sub(r'(?P<neg>-?)(?P<d0>\d)\.(?P<d1>\d)\d\d+e-02', r'\g<neg>0.0\g<d0>\g<d1>', s)
|
||||
s = re.sub(r'(?P<neg>-?)(?P<d0>\d)\.\d\d\d+e-03', r'\g<neg>0.00\g<d0>', s)
|
||||
s = re.sub(r'-?\d\.\d\d\d+e-0[4-9]', r'0.000', s)
|
||||
s = re.sub(r'-?\d\.\d\d\d+e-[1-9]\d', r'0.000', s)
|
||||
s = re.sub(r'(?P<digs>\d\.\d\d\d)\d+', r'\g<digs>', s)
|
||||
return s
|
||||
|
||||
|
||||
def get_matrices(ob):
|
||||
''' obtain blender object matrices '''
|
||||
mx = ob.matrix_world
|
||||
imx = mx.inverted_safe()
|
||||
return (mx, imx)
|
||||
|
||||
|
||||
class AddonLocator(object):
|
||||
def __init__(self, f=None):
|
||||
self.fullInitPath = f if f else __file__
|
||||
self.FolderPath = os.path.dirname(self.fullInitPath)
|
||||
self.FolderName = os.path.basename(self.FolderPath)
|
||||
|
||||
def AppendPath(self):
|
||||
sys.path.append(self.FolderPath)
|
||||
print("Addon path has been registered into system path for this session")
|
||||
|
||||
|
||||
|
||||
class UniqueCounter():
|
||||
__counter = 0
|
||||
@staticmethod
|
||||
def next():
|
||||
UniqueCounter.__counter += 1
|
||||
return UniqueCounter.__counter
|
||||
|
||||
|
||||
class Dict():
|
||||
'''
|
||||
a fancy dictionary object
|
||||
'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['__d'] = {}
|
||||
if 'get_default' in kwargs:
|
||||
v = kwargs.pop('get_default')
|
||||
self.set_get_default_fn(lambda: v)
|
||||
if 'get_default_fn' in kwargs:
|
||||
self.set_get_default_fn(kwargs.pop('get_default_fn'))
|
||||
self.set(*args, **kwargs)
|
||||
def set_get_default_fn(self, fn):
|
||||
self.__dict__['__default_fn'] = fn
|
||||
def __getitem__(self, k):
|
||||
d = self.__dict__['__d']
|
||||
if '__default_fn' in self.__dict__: # get_default_fn set
|
||||
return d[k] if k in d else self.__dict__['__default_fn']()
|
||||
return self.__dict__['__d'][k]
|
||||
def get(self, k, *args):
|
||||
d = self.__dict__['__d']
|
||||
if args: # default specified
|
||||
return d.get(k, *args)
|
||||
if '__default_fn' in self.__dict__: # get_default_fn set
|
||||
return d[k] if k in d else self.__dict__['__default_fn']()
|
||||
return d.get(k)
|
||||
def __setitem__(self, k, v):
|
||||
self.__dict__['__d'][k] = v
|
||||
return v
|
||||
def __delitem__(self, k):
|
||||
del self.__dict__['__d'][k]
|
||||
def __getattr__(self, k):
|
||||
return self.get(k)
|
||||
# return self.__dict__['__d'][k]
|
||||
def __setattr__(self, k, v):
|
||||
self.__dict__['__d'][k] = v
|
||||
return v
|
||||
def __delattr__(self, k):
|
||||
del self.__dict__['__d'][k]
|
||||
def set(self, kvs=None, **kwargs):
|
||||
kvs = kvs or {}
|
||||
for k,v in itertools.chain(kvs.items(), kwargs.items()): self[k] = v
|
||||
def __str__(self): return str(self.__dict__['__d'])
|
||||
def __repr__(self): return repr(self.__dict__['__d'])
|
||||
def values(self): return self.__dict__['__d'].values()
|
||||
def __iter__(self): return iter(self.__dict__['__d'])
|
||||
|
||||
def has_duplicates(lst):
|
||||
l = len(lst)
|
||||
if l == 0: return False
|
||||
if l < 20 or not hasattr(lst[0], '__hash__'):
|
||||
# runs in O(n^2) time (perfectly fine if n is small, assuming [:index] uses iter)
|
||||
# does not require items in list to hash
|
||||
# requires O(1) memory
|
||||
return any(item in lst[:index] for (index,item) in enumerate(lst))
|
||||
else:
|
||||
# runs in either O(n) time (assuming hash-set)
|
||||
# requires items to hash
|
||||
# requires O(N) memory
|
||||
seen = set()
|
||||
for i in lst:
|
||||
if i in seen: return True
|
||||
seen.add(i)
|
||||
return False
|
||||
|
||||
def deduplicate_list(l):
|
||||
nl = []
|
||||
for i in l:
|
||||
if i in nl: continue
|
||||
nl.append(i)
|
||||
return nl
|
||||
|
||||
class StopWatch:
|
||||
def __init__(self):
|
||||
self._start = time.time()
|
||||
self._last = time.time()
|
||||
def elapsed(self):
|
||||
self._last, prev = time.time(), self._last
|
||||
return self._last - prev
|
||||
def total_elapsed(self):
|
||||
return time.time() - self._start
|
||||
@@ -0,0 +1,23 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
__all__ = ['cookiecutter', 'test']
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..common.blender import perform_redraw_all
|
||||
from ..common.debug import debugger, tprint
|
||||
from ..common.profiler import profiler
|
||||
|
||||
from .cookiecutter_actions import CookieCutter_Actions
|
||||
from .cookiecutter_blender import CookieCutter_Blender
|
||||
from .cookiecutter_debug import CookieCutter_Debug
|
||||
from .cookiecutter_exceptions import CookieCutter_Exceptions
|
||||
from .cookiecutter_fsm import CookieCutter_FSM
|
||||
from .cookiecutter_modal import CookieCutter_Modal
|
||||
from .cookiecutter_ui import CookieCutter_UI
|
||||
|
||||
|
||||
is_broken = False
|
||||
|
||||
class CookieCutter(
|
||||
Operator,
|
||||
CookieCutter_UI,
|
||||
CookieCutter_Actions,
|
||||
CookieCutter_FSM,
|
||||
CookieCutter_Blender,
|
||||
CookieCutter_Exceptions,
|
||||
CookieCutter_Debug,
|
||||
CookieCutter_Modal,
|
||||
):
|
||||
'''
|
||||
CookieCutter is used to create advanced operators very quickly!
|
||||
|
||||
To use:
|
||||
|
||||
- specify CookieCutter as a subclass
|
||||
- provide appropriate values for Blender class attributes: bl_idname, bl_label, etc.
|
||||
- provide appropriate dictionary that maps user action labels to keyboard and mouse actions
|
||||
- override the start function
|
||||
- register finite state machine state callbacks with the FSM.on_state(state) function decorator
|
||||
- state can be any string that is a state in your FSM
|
||||
- Must provide at least a 'main' state
|
||||
- return values of each on_state decorated function tell FSM which state to switch into
|
||||
- None, '', or no return: stay in same state
|
||||
- register drawing callbacks with the CookieCutter.Draw(mode) function decorator
|
||||
- mode: 'pre3d', 'post3d', 'post2d'
|
||||
|
||||
'''
|
||||
|
||||
# registry = []
|
||||
# def __init_subclass__(cls, *args, **kwargs):
|
||||
# super().__init_subclass__(*args, **kwargs)
|
||||
# if not hasattr(cls, '_cookiecutter_index'):
|
||||
# # add cls to registry (might get updated later) and add FSM,Draw
|
||||
# cls._rfwidget_index = len(CookieCutter.registry)
|
||||
# CookieCutter.registry.append(cls)
|
||||
# cls.fsm = FSM()
|
||||
# cls.drawcallbacks = DrawCallbacks()
|
||||
# else:
|
||||
# # update registry, but do not add new FSM
|
||||
# CookieCutter.registry[cls._cookiecutter_index] = cls
|
||||
|
||||
|
||||
############################################################################
|
||||
# override the following values and functions
|
||||
|
||||
bl_idname = "view3d.cookiecutter_unnamed"
|
||||
bl_label = "CookieCutter Unnamed"
|
||||
|
||||
is_running = False
|
||||
|
||||
@classmethod
|
||||
def can_start(cls, context): return True
|
||||
|
||||
def prestart(self): pass
|
||||
def is_ready_to_start(self): return True
|
||||
def start(self): pass
|
||||
def update(self): pass
|
||||
def end_commit(self): pass
|
||||
def end_cancel(self): pass
|
||||
def end(self): pass
|
||||
def should_pass_through(self, context, event): return False
|
||||
|
||||
############################################################################
|
||||
|
||||
@staticmethod
|
||||
def cc_break():
|
||||
global is_broken
|
||||
is_broken = True
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
global is_broken
|
||||
if is_broken: return False
|
||||
with cls.try_exception('call can_start()'):
|
||||
return cls.can_start(context)
|
||||
print('BREAKING COOKIECUTTER')
|
||||
print(f'{cls.bl_idname}')
|
||||
cls.cc_break()
|
||||
return False
|
||||
|
||||
def invoke(self, context, event):
|
||||
CookieCutter.is_running = True
|
||||
self._cc_stage = 'prestart'
|
||||
self.context = context
|
||||
self.event = event
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def stop_running(self):
|
||||
CookieCutter.is_running = False
|
||||
|
||||
def done(self, *, cancel=False, emergency_bail=False):
|
||||
if emergency_bail:
|
||||
self._done = 'bail'
|
||||
else:
|
||||
self._done = 'commit' if not cancel else 'cancel'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.useractions import ActionHandler
|
||||
|
||||
|
||||
class CookieCutter_Actions:
|
||||
def _cc_actions_init(self):
|
||||
self._cc_actions = ActionHandler(self.context)
|
||||
self._timer = self._cc_actions.start_timer(10)
|
||||
|
||||
def _cc_actions_update(self):
|
||||
self._cc_actions.update(self.context, self.event, fn_debug=self.debug_print_actions)
|
||||
|
||||
def _cc_actions_end(self):
|
||||
self._timer.done()
|
||||
self._cc_actions.done()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import math
|
||||
from inspect import ismethod, isfunction, signature
|
||||
from contextlib import contextmanager
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.blender import region_label_to_data, create_simple_context, StoreRestore, BlenderSettings
|
||||
from ..common.decorators import blender_version_wrapper, ignore_exceptions
|
||||
from ..common.functools import find_fns, self_wrapper
|
||||
from ..common.debug import debugger
|
||||
from ..common.blender_cursors import Cursors
|
||||
from ..common.utils import iter_head
|
||||
|
||||
|
||||
|
||||
class CookieCutter_Blender(BlenderSettings):
|
||||
def _cc_blenderui_init(self):
|
||||
self.storerestore_init()
|
||||
for _,fn in find_fns(self, '_blender_change_callback'):
|
||||
self.register_blender_change_callback(self_wrapper(self, fn))
|
||||
self._storerestore.store_all()
|
||||
|
||||
@staticmethod
|
||||
def blender_change_callback(fn):
|
||||
fn._blender_change_callback = True
|
||||
return fn
|
||||
def register_blender_change_callback(self, fn):
|
||||
self._storerestore.register_storage_change_callback(fn)
|
||||
def blender_change_init(self, storage):
|
||||
self._storerestore.init_storage(storage)
|
||||
|
||||
def _cc_blenderui_end(self, ignore=None):
|
||||
self._storerestore.restore_all(ignore=ignore)
|
||||
|
||||
self.header_text_restore()
|
||||
self.statusbar_text_restore()
|
||||
self.cursor_restore()
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from ..common.blender import get_text_block
|
||||
|
||||
|
||||
class CookieCutter_Debug:
|
||||
cc_debug_print_to = 'CookieCutter_Debug'
|
||||
cc_debug_all_enabled = False
|
||||
cc_debug_actions_enabled = False
|
||||
|
||||
def debug_print(self, label, *args, override_enabled=False, **kwargs):
|
||||
if not override_enabled and not self.cc_debug_all_enabled: return
|
||||
|
||||
text_block = get_text_block(self.cc_debug_print_to)
|
||||
assert text_block
|
||||
text_block.cursor_set(0x7fffffff) # move cursor to last line
|
||||
text_block.write(f'{label}: {", ".join(args)}\n')
|
||||
for k,v in kwargs.items():
|
||||
text_block.write(f' {k} = {v}\n')
|
||||
text_block.write('\n')
|
||||
|
||||
def debug_print_actions(self, *args, **kwargs):
|
||||
self.debug_print(
|
||||
'Action',
|
||||
*args,
|
||||
override_enabled=self.cc_debug_actions_enabled,
|
||||
**kwargs
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
https://github.com/CGCookie/retopoflow
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import contextlib
|
||||
|
||||
from ..common.fsm import FSM
|
||||
from ..common.debug import debugger, ExceptionHandler
|
||||
from ..common.functools import find_fns
|
||||
|
||||
class CookieCutter_Exceptions:
|
||||
@staticmethod
|
||||
def _handle_exception(e, action, fatal=False):
|
||||
print(f'CookieCutter_Exceptions: handling caught exception')
|
||||
print(f' action: {action}')
|
||||
debugger.print_exception()
|
||||
if fatal: assert False
|
||||
if hasattr(CookieCutter_Exceptions, '_instance'):
|
||||
CookieCutter_Exceptions._instance._callback_exception_callbacks(e)
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def try_exception(action, *, fatal=False, fn_succeed=None, fn_exception=None, fn_finally=None):
|
||||
try:
|
||||
yield
|
||||
if fn_succeed: fn_succeed()
|
||||
except Exception as e:
|
||||
CookieCutter_Exceptions._handle_exception(e, action, fatal=fatal)
|
||||
if fn_exception: fn_exception(e)
|
||||
finally:
|
||||
if fn_finally: fn_finally()
|
||||
|
||||
@staticmethod
|
||||
def _exception_callback_wrapper(fn):
|
||||
fn._cc_exception_callback = True
|
||||
return fn
|
||||
Exception_Callback = _exception_callback_wrapper
|
||||
|
||||
@ExceptionHandler.on_exception
|
||||
def _callback_exception_callbacks(self, e):
|
||||
print(f'CookieCutter_Exceptions._callback_exception_callbacks: {e}')
|
||||
# debugger.dcallstack(0)
|
||||
for fn_name in self._exception_callbacks:
|
||||
try:
|
||||
fn = getattr(self, fn_name)
|
||||
fn(e)
|
||||
except Exception as e2:
|
||||
print(f'CookieCutter caught exception while calling back exception callbacks: {fn.__name__}')
|
||||
debugger.print_exception()
|
||||
|
||||
def _cc_exception_init(self):
|
||||
self._exception_callbacks = [fn.__name__ for (_,fn) in find_fns(self, '_cc_exception_callback')]
|
||||
self._exceptionhandler = ExceptionHandler(self)
|
||||
#self._exceptionhandler.add_callback(self._callback_exception_callbacks, universal=True)
|
||||
CookieCutter_Exceptions._instance = self
|
||||
|
||||
def _cc_exception_done(self):
|
||||
del self._exceptionhandler
|
||||
del CookieCutter_Exceptions._instance
|
||||
self._exceptionhandler = None
|
||||
ExceptionHandler.clear_universal_callbacks()
|
||||
@@ -0,0 +1,55 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
https://github.com/CGCookie/retopoflow
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.debug import debugger
|
||||
from ..common.fsm import FSM
|
||||
from ..common.timerhandler import TimerHandler
|
||||
|
||||
class CookieCutter_FSM:
|
||||
def _cc_fsm_init(self):
|
||||
self.fsm = FSM(self, start='main')
|
||||
self.fsm.add_exception_callback(lambda e: self._handle_exception(e, 'handle exception caught by FSM'))
|
||||
# def callback(e): self._handle_exception(e, 'handle exception caught by FSM')
|
||||
# self.fsm.add_exception_callback(callback)
|
||||
|
||||
def _cc_fsm_update(self):
|
||||
self.fsm.update()
|
||||
|
||||
def _cc_fsm_force_event(self):
|
||||
# add some NOP event to event queue to force modal operator to be called again right away
|
||||
|
||||
# # warp cursor to same spot
|
||||
# # DOES NOT WORK: (event.mouse_x, event.mouse_y) might be incorrect!!!
|
||||
# self.context.window.cursor_warp(self.event.mouse_x, self.event.mouse_y)
|
||||
|
||||
# # simulate an event
|
||||
# # DOES NOT WORK: only works with `--enable-event-simulate`, but then Blender cannot accept any input!!!
|
||||
# self.context.window.event_simulate(type='NONE', value='NOTHING')
|
||||
|
||||
# # register a short-lived timer (only returns `None`)
|
||||
# # DOES NOT WORK: these timers do NOT cause modal operator to be called for some reason :(
|
||||
# bpy.app.timers.register(lambda:None, first_interval=0.01)
|
||||
|
||||
# create a short-lived WindowManager timer
|
||||
# self.actions might not yet be created!
|
||||
if not hasattr(self, '_cc_force_event_handler'):
|
||||
self._cc_force_event_handler = TimerHandler(120, context=self.context, enabled=False)
|
||||
self._cc_force_event_handler.start()
|
||||
|
||||
def _cc_fsm_stop_force_event(self):
|
||||
if hasattr(self, '_cc_force_event_handler'):
|
||||
self._cc_force_event_handler.stop()
|
||||
@@ -0,0 +1,166 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..common.blender import perform_redraw_all, get_view3d_area
|
||||
from ..common.debug import debugger, tprint
|
||||
from ..common.profiler import profiler
|
||||
|
||||
|
||||
class CookieCutter_Modal:
|
||||
def modal(self, context, event):
|
||||
self.context = context
|
||||
self.event = event
|
||||
|
||||
if self._cc_stage == 'quit': return {'FINISHED'}
|
||||
|
||||
# if we're not yet in the main loop, create a NOP event so that we can
|
||||
# work our way through the initialization stuff as quickly as possible!
|
||||
if self._cc_stage != 'main loop': self._cc_fsm_force_event()
|
||||
else: self._cc_fsm_stop_force_event()
|
||||
|
||||
# get the method corresponding to the current stage
|
||||
fn_modal = {
|
||||
'prestart': self.modal_prestart,
|
||||
'start when ready': self.modal_start_when_ready,
|
||||
'init CC internals': self.modal_init_cc_internals,
|
||||
'init CC start': self.modal_init_cc_start,
|
||||
'init CC UI': self.modal_init_cc_ui,
|
||||
'main loop': self.modal_mainloop,
|
||||
}.get(self._cc_stage, None)
|
||||
assert fn_modal, f"Unhandled CC stage: '{self._cc_stage}'"
|
||||
|
||||
ret = fn_modal()
|
||||
# if ret == {'PASS_THROUGH'}:
|
||||
# print(f'passing through {random.random()}')
|
||||
return ret
|
||||
|
||||
def modal_prestart(self):
|
||||
with self.try_exception('prestarting'):
|
||||
self.prestart()
|
||||
self._cc_stage = 'start when ready'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_start_when_ready(self):
|
||||
with self.try_exception('waiting for start readiness'):
|
||||
if self.is_ready_to_start():
|
||||
self._cc_stage = 'init CC internals'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_internals(self):
|
||||
with self.try_exception('initialize internals (Exception Callbacks, FSM, UI, Actions)'):
|
||||
self._nav = False
|
||||
self._nav_time = 0
|
||||
self._done = False
|
||||
self._start_time = time.time()
|
||||
self._tmp_time = self._start_time
|
||||
self._cc_exception_init()
|
||||
self._cc_fsm_init()
|
||||
self._cc_ui_init()
|
||||
self._cc_actions_init()
|
||||
self._cc_stage = 'init CC start'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_start(self):
|
||||
with self.try_exception('initialize start'):
|
||||
self.start()
|
||||
self._cc_stage = 'init CC UI'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_ui(self):
|
||||
with self.try_exception('initialize ui'):
|
||||
self._cc_ui_start()
|
||||
self._cc_stage = 'main loop'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_mainloop(self):
|
||||
self.drawcallbacks.reset_pre()
|
||||
|
||||
if time.time() - self._tmp_time >= 1:
|
||||
self._tmp_time = time.time()
|
||||
# print('--- %d ---' % int(self._tmp_time - self._start_time))
|
||||
profiler.printfile()
|
||||
|
||||
if self._done:
|
||||
self.modal_maindone()
|
||||
self._cc_ui_end()
|
||||
self._cc_actions_end()
|
||||
self._cc_exception_done()
|
||||
return {'FINISHED'} if self._done=='finish' else {'CANCELLED'}
|
||||
|
||||
ret = None
|
||||
|
||||
if self._nav:
|
||||
self._nav = False
|
||||
self._nav_time = time.time()
|
||||
self._cc_actions_update()
|
||||
|
||||
if self._cc_ui_update():
|
||||
# UI handled the action
|
||||
ret = {'RUNNING_MODAL'}
|
||||
elif self._cc_actions.using('blender window action'):
|
||||
# allow window actions to pass through to Blender
|
||||
ret = {'PASS_THROUGH'}
|
||||
elif self._cc_actions.is_navigating or (self._cc_actions.timer and self._nav):
|
||||
self._nav = True
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
with self.try_exception('call update'):
|
||||
self.update()
|
||||
if self.should_pass_through(self.context, self.event):
|
||||
ret = {'PASS_THROUGH'}
|
||||
|
||||
if not ret:
|
||||
self._cc_fsm_update()
|
||||
ret = {'RUNNING_MODAL'}
|
||||
|
||||
perform_redraw_all(only_area=get_view3d_area(self.context))
|
||||
return ret
|
||||
|
||||
def modal_maindone(self):
|
||||
if self._done == 'bail':
|
||||
return
|
||||
|
||||
try:
|
||||
fn_end = self.end_commit if self._done == 'commit' else self.end_cancel
|
||||
fn_end()
|
||||
self.end()
|
||||
self.stop_running()
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'call end() with %s' % self._done)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user