First Commit
parent
d302e8d14b
commit
e4ae2be1e8
|
@ -0,0 +1,6 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
0.3.0
|
||||||
|
|
||||||
|
- initial commit
|
|
@ -0,0 +1,339 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) 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
|
||||||
|
this service 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 make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. 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.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
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
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the 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 a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
Background Plane Manager
|
||||||
|
Copyright (C) 2023 autour-de-minuit / blender
|
||||||
|
|
||||||
|
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 2 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, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision 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, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This 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.
|
|
@ -0,0 +1,76 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "Background plane manager",
|
||||||
|
"description": "Manage the background image planes and grease pencil object relative to a camera",
|
||||||
|
"author": "Samuel Bernou",
|
||||||
|
"version": (0, 4, 2),
|
||||||
|
"blender": (3, 5, 0),
|
||||||
|
"location": "View3D",
|
||||||
|
"warning": "",
|
||||||
|
"doc_url": "https://gitlab.com/autour-de-minuit/blender/background_plane_manager",
|
||||||
|
"category": "Object"
|
||||||
|
}
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from . import operators
|
||||||
|
from . import export_psd_layers
|
||||||
|
from . import ui
|
||||||
|
from . import preferences
|
||||||
|
|
||||||
|
from . import fn
|
||||||
|
|
||||||
|
from . file_utils import install_module
|
||||||
|
install_module('psd_tools', 'psd-tools')
|
||||||
|
|
||||||
|
"""
|
||||||
|
from . import auto_modules
|
||||||
|
|
||||||
|
## module auto-install
|
||||||
|
## module_name, package_name
|
||||||
|
DEPENDENCIES = {
|
||||||
|
('psd_tools', 'psd-tools'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# modules_loc = bpy.utils.user_resource('SCRIPTS', path='modules')
|
||||||
|
modules_loc = Path(__file__).parents[1] / 'modules'
|
||||||
|
error_message = f'''--- Cannot import modules (see console).
|
||||||
|
Try enabling addon after restarting blender as admin
|
||||||
|
---
|
||||||
|
'''
|
||||||
|
|
||||||
|
error = auto_modules.pip_install_and_import(DEPENDENCIES)
|
||||||
|
# note: an internet connexion is needed to auto-install needed modules)
|
||||||
|
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise Exception(error_message) from error
|
||||||
|
|
||||||
|
has_psd_tools = True
|
||||||
|
try:
|
||||||
|
import psd_tools
|
||||||
|
except Exception:
|
||||||
|
has_psd_tools = False
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Add an enum in prefs to choose default type to use
|
||||||
|
# Override this enum if there is an environement variable in project
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
operators,
|
||||||
|
export_psd_layers,
|
||||||
|
preferences,
|
||||||
|
ui,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for m in modules:
|
||||||
|
m.register()
|
||||||
|
|
||||||
|
preferences.ui_in_sidebar_update(fn.get_addon_prefs(), bpy.context)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for m in reversed(modules):
|
||||||
|
m.unregister()
|
|
@ -0,0 +1,145 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os, sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# THIRD_PARTY = Path(__file__).resolve().parent / "libs"# os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs")
|
||||||
|
# DEFAULT_CACHE_DIR = Path(__file__).resolve().parent / "cache"# os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
|
||||||
|
# SUBPROCESS_DIR = PYTHON_PATH.parent
|
||||||
|
|
||||||
|
PYTHON_PATH = Path(sys.executable)
|
||||||
|
BLENDER_SITE_PACKAGE = PYTHON_PATH.parents[1] / 'lib' / 'site-packages'
|
||||||
|
# or: BLENDER_SITE_PACKAGE = Path(bpy.utils.resource_path('LOCAL')) / 'python' / 'lib' / 'site-packages'
|
||||||
|
|
||||||
|
|
||||||
|
def module_can_be_imported(name):
|
||||||
|
try:
|
||||||
|
__import__(name)
|
||||||
|
return True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def install_pip():
|
||||||
|
# pip can not necessarily be imported into Blender after this
|
||||||
|
subprocess.run([str(PYTHON_PATH), "-m", "ensurepip"])
|
||||||
|
|
||||||
|
def install_package(name):
|
||||||
|
# import logging
|
||||||
|
# logging.debug(f"Using {PYTHON_PATH} for installation")
|
||||||
|
## !!! try with , '--user' ?
|
||||||
|
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", name])
|
||||||
|
|
||||||
|
def setup(dependencies):
|
||||||
|
'''
|
||||||
|
Get a set containing multiple tuple of (module, package) names pair (str)
|
||||||
|
install pip with ensurepip if needed, try to import, install, retry
|
||||||
|
'''
|
||||||
|
|
||||||
|
os.environ['PYTHONUSERBASE'] = str(BLENDER_SITE_PACKAGE)
|
||||||
|
if not module_can_be_imported("pip"):
|
||||||
|
install_pip()
|
||||||
|
|
||||||
|
for module_name, package_name in dependencies:
|
||||||
|
if not module_can_be_imported(module_name):
|
||||||
|
print(f'Installing module: {module_name} - package: {package_name}')
|
||||||
|
install_package(package_name)
|
||||||
|
module_can_be_imported(package_name)
|
||||||
|
|
||||||
|
|
||||||
|
### --- classic install (not used)
|
||||||
|
|
||||||
|
def pip_install(package_name):
|
||||||
|
'''Get a package name (str) and try to install and print in console'''
|
||||||
|
print(f'---Installing {package_name}---')
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output([bpy.app.binary_path_python, '-m', 'pip', 'install', package_name])
|
||||||
|
print(output)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(e.output)
|
||||||
|
return e.output
|
||||||
|
|
||||||
|
|
||||||
|
def pip_install_and_import(dependencies):
|
||||||
|
'''
|
||||||
|
Get a set containing multiple tuple of (module, package) names pair (str)
|
||||||
|
try to import, if import fail, try to install then try to reimport
|
||||||
|
'''
|
||||||
|
# os.environ['PYTHONUSERBASE'] = str(BLENDER_SITE_PACKAGE)
|
||||||
|
for module_name, package_name in dependencies:
|
||||||
|
# '--user'# install for all version of blender (suposely in app data roaming or config files...)
|
||||||
|
# '--no-deps'# dont update dependancy (in this case, avoid installing downloading a duplication of numpy)
|
||||||
|
|
||||||
|
try:
|
||||||
|
__import__(module_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
## auto install dependancy (need to run as admin)
|
||||||
|
print(f'Installing module: {module_name} - package: {package_name}') #"--user" ?
|
||||||
|
|
||||||
|
#### Using target
|
||||||
|
## within built-in modules (need run as admin in most case)
|
||||||
|
# subprocess.check_call([str(PYTHON_PATH), "-m", "pip", "install", f'--target={BLENDER_SITE_PACKAGE}', package_name])
|
||||||
|
|
||||||
|
done = False
|
||||||
|
|
||||||
|
## within external modules (if script files are there)
|
||||||
|
external_scripts = bpy.context.preferences.filepaths.script_directory
|
||||||
|
print('external_scripts: ', external_scripts)
|
||||||
|
print('__file__: ', __file__)
|
||||||
|
in_external = str(Path(__file__)).startswith(str(Path(external_scripts)))
|
||||||
|
|
||||||
|
if external_scripts and len(external_scripts) > 2 and in_external:
|
||||||
|
external_scripts = Path(external_scripts)
|
||||||
|
if external_scripts.exists():
|
||||||
|
external_modules = external_scripts / 'modules'
|
||||||
|
print(f'using external scripts modules: {external_modules}')
|
||||||
|
external_modules.mkdir(exist_ok=True)# dont raise error if already exists
|
||||||
|
|
||||||
|
# cmd = [str(PYTHON_PATH), "-m", "pip", "install", f'--target={external_modules}', package_name]
|
||||||
|
cmd = [str(PYTHON_PATH), "-m", "pip", '--no-cache-dir', "install", f'--target={external_modules}', package_name, '--no-deps']
|
||||||
|
print('Run', ' '.join(cmd))
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
done=True
|
||||||
|
|
||||||
|
## within user local modules (if not in external scripts)
|
||||||
|
if not done:
|
||||||
|
user_module = Path(bpy.utils.user_resource('SCRIPTS', path='modules', create=True)) # create the folder if not exists
|
||||||
|
print(f'Using users modules: {user_module}')
|
||||||
|
|
||||||
|
# cmd = [str(PYTHON_PATH), "-m", "pip", "install", f'--target={user_module}', package_name]
|
||||||
|
cmd = [str(PYTHON_PATH), "-m", "pip", '--no-cache-dir', "install", f'--target={user_module}', package_name, '--no-deps']
|
||||||
|
print('Run', ' '.join(cmd))
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'{package_name} install error: {e}')
|
||||||
|
print('Maybe try restarting blender as Admin')
|
||||||
|
return e
|
||||||
|
|
||||||
|
try:
|
||||||
|
__import__(module_name)
|
||||||
|
except ImportError as e:
|
||||||
|
print(f'!!! module {module_name} still cannot be imported')
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
## addons Paths
|
||||||
|
|
||||||
|
# natives
|
||||||
|
built_in_addons = os.path.join(bpy.utils.resource_path('LOCAL') , path='scripts', 'addons')
|
||||||
|
|
||||||
|
# users
|
||||||
|
users_addons = bpy.utils.user_resource('SCRIPTS', path='addons')
|
||||||
|
|
||||||
|
#external
|
||||||
|
external_addons = None
|
||||||
|
external_script_dir = bpy.context.preferences.filepaths.script_directory
|
||||||
|
if external_script_dir and len(external_script_dir) > 2:
|
||||||
|
external_addons = os.path.join(external_script_dir, 'addons')
|
||||||
|
"""
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PREFIX = 'BG_'
|
||||||
|
INIT_POS = 9.0
|
||||||
|
MODULE_DIR = Path(__file__).parent
|
||||||
|
BGCOL = 'Background'
|
|
@ -0,0 +1,278 @@
|
||||||
|
import bpy
|
||||||
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psd_tools
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
from . import fn
|
||||||
|
|
||||||
|
|
||||||
|
def print_progress(progress, min=0, max=100, barlen=50, prefix='', suffix='', line_width=80):
|
||||||
|
total_len = max - min
|
||||||
|
progress_float = (progress - min) / total_len
|
||||||
|
bar_progress = int(progress_float * barlen) * '='
|
||||||
|
bar_empty = (barlen - len(bar_progress)) * ' '
|
||||||
|
percentage = ''.join((str(int(progress_float * 100)), '%'))
|
||||||
|
progress_string = ''.join((prefix, '[', bar_progress, bar_empty, ']', ' ', percentage, suffix))[:line_width]
|
||||||
|
print_string = ''.join((progress_string, ' ' * (line_width - len(progress_string))))
|
||||||
|
print(print_string, end='\r')
|
||||||
|
|
||||||
|
|
||||||
|
def export_psd(psd_file, output=None, scale=0.5):
|
||||||
|
'''
|
||||||
|
export_psd(string psd_file) -> list layers, list bboxes, tuple image_size, string png_dir
|
||||||
|
|
||||||
|
Reads psd_file and exports all top level layers to png's.
|
||||||
|
Returns a list of all the layer objects, the image size and
|
||||||
|
the png export directory.
|
||||||
|
|
||||||
|
string psd_file - the filepath of the psd file
|
||||||
|
'''
|
||||||
|
|
||||||
|
# def get_layers(layer, all_layers=[]):
|
||||||
|
# if not layer.is_group():
|
||||||
|
# return
|
||||||
|
# for sub_layer in reversed(layer): # reversed() since psd_tools 1.8
|
||||||
|
# all_layers.append(sub_layer)
|
||||||
|
# get_layers(sub_layer, all_layers=all_layers)
|
||||||
|
# return all_layers
|
||||||
|
|
||||||
|
def get_dimensions(layer, bbox):
|
||||||
|
if bbox is not None:
|
||||||
|
# print('with bbox')
|
||||||
|
# pp(bbox)
|
||||||
|
|
||||||
|
x = layer.bbox[0] + bbox[0]
|
||||||
|
y = layer.bbox[1] + bbox[1]
|
||||||
|
width = bbox[2] - bbox[0]
|
||||||
|
height = bbox[3] - bbox[1]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# print('layer bbox')
|
||||||
|
# pp(layer.bbox[:])
|
||||||
|
x = layer.bbox[0]
|
||||||
|
y = layer.bbox[1]
|
||||||
|
width = layer.bbox[2] - x
|
||||||
|
height = layer.bbox[3] - y
|
||||||
|
# print('x', x)
|
||||||
|
# print('y', y)
|
||||||
|
# print('width', width)
|
||||||
|
# print('height', height)
|
||||||
|
|
||||||
|
return x, y, width, height
|
||||||
|
|
||||||
|
def export_layers_as_png(layers, output, crop=False, scale=0.5):
|
||||||
|
output = Path(output)
|
||||||
|
box = layers.viewbox
|
||||||
|
all_layers = []
|
||||||
|
|
||||||
|
for i, layer in enumerate(layers):
|
||||||
|
# if (layer.is_group() or (not self.hidden_layers and not layer.is_visible())):
|
||||||
|
# continue
|
||||||
|
layer.visible = True
|
||||||
|
if layer.is_group() and 'GUIDE' in layer.name:
|
||||||
|
for l in layer:
|
||||||
|
#print(l.name, l.visible)
|
||||||
|
l.visible = True
|
||||||
|
|
||||||
|
name = layer.name
|
||||||
|
norm_name = fn.norm_str(name, padding=2) # Gadget normstr
|
||||||
|
print('name: ', name)
|
||||||
|
png_output = (output/norm_name).with_suffix('.png')
|
||||||
|
|
||||||
|
print('Layer Output', png_output)
|
||||||
|
|
||||||
|
prefix = ' - exporting: '
|
||||||
|
suffix = ' - {}'.format(layer.name)
|
||||||
|
print_progress(i+1, max=(len(layers)), barlen=40, prefix=prefix, suffix=suffix, line_width=120)
|
||||||
|
|
||||||
|
|
||||||
|
# if self.clean_name:
|
||||||
|
# name = bpy.path.clean_name(layer.name).rstrip('_')
|
||||||
|
# else:
|
||||||
|
# name = layer.name.replace('\x00', '')
|
||||||
|
|
||||||
|
# name = name.rstrip('_')
|
||||||
|
# if self.layer_index_name:
|
||||||
|
# name = name + '_' + str(i)
|
||||||
|
|
||||||
|
# composite return a PIL object
|
||||||
|
|
||||||
|
if crop:
|
||||||
|
# get pre-crop size
|
||||||
|
|
||||||
|
# layer_image = layer.topil()
|
||||||
|
layer_image = layer.composite(viewport=box)
|
||||||
|
bbox = layer_image.getbbox()
|
||||||
|
## TODO layer bbox might be completely off (when it's a group)
|
||||||
|
|
||||||
|
image = layer.composite()# This crop the image
|
||||||
|
|
||||||
|
else:
|
||||||
|
image = layer.composite(viewport=box, force=True)# This crop to canvas size before getting bbox ??
|
||||||
|
bbox = None
|
||||||
|
|
||||||
|
if not image:
|
||||||
|
continue
|
||||||
|
|
||||||
|
## optimisation reduce size
|
||||||
|
#if scale != 1 :
|
||||||
|
image = image.resize((image.width // int(1/scale), image.height // int(1/scale)))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.save(str(png_output))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# if crop:
|
||||||
|
# layer_box_tuple = get_dimensions(layer, bbox)
|
||||||
|
# else:
|
||||||
|
# layer_box_tuple = None
|
||||||
|
lbbox = get_dimensions(layer, bbox)
|
||||||
|
|
||||||
|
all_layers.append({'name': name, 'layer_bbox': lbbox, 'bbox': bbox, 'index': i, 'path': f'./{png_output.name}'})
|
||||||
|
|
||||||
|
## No crop for now
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# layer_image = layer.topil()
|
||||||
|
# except ValueError:
|
||||||
|
# print("Could not process layer " + layer.name)
|
||||||
|
# bboxes.append(None)
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# if layer_image is None:
|
||||||
|
# bboxes.append(None)
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# ## AUTOCROP
|
||||||
|
# if self.crop_layers:
|
||||||
|
# bbox = layer_image.getbbox()
|
||||||
|
# bboxes.append(bbox)
|
||||||
|
#
|
||||||
|
# layer_image = layer_image.crop(bbox)
|
||||||
|
# else:
|
||||||
|
# bboxes.append(None)
|
||||||
|
# layer_image.save(png_file)
|
||||||
|
|
||||||
|
return all_layers
|
||||||
|
|
||||||
|
|
||||||
|
print(f'Exporting: {psd_file}')
|
||||||
|
|
||||||
|
psd_file = Path(psd_file)
|
||||||
|
if not output:
|
||||||
|
# export relative to psd location
|
||||||
|
output = psd_file.parent / 'render' # f'{psd_file.stem}_pngs'
|
||||||
|
|
||||||
|
output.mkdir(exist_ok = True)
|
||||||
|
psd = psd_tools.PSDImage.open(psd_file)
|
||||||
|
|
||||||
|
## get all layer separately
|
||||||
|
# layers = get_layers(psd)
|
||||||
|
# bboxes = export_layers_as_png(layers, png_dir)
|
||||||
|
|
||||||
|
## export the main image (PSD, MAIN, COMPOSITE ?)
|
||||||
|
image = psd.composite()
|
||||||
|
org_image_size = [image.width, image.height]
|
||||||
|
|
||||||
|
image = image.resize((image.width // 2, image.height // 2))
|
||||||
|
image.save(str((output / 'main.png')))
|
||||||
|
|
||||||
|
## export top level layer passing directly psd
|
||||||
|
all_layers = export_layers_as_png(psd, output, crop=False, scale=scale)
|
||||||
|
bb = psd.bbox
|
||||||
|
image_size = (bb[2] - bb[0], bb[3] - bb[1])
|
||||||
|
|
||||||
|
# return ([l.name for l in psd], bboxes, image_size, output)
|
||||||
|
return (all_layers, image_size, org_image_size, output)
|
||||||
|
|
||||||
|
|
||||||
|
def export_psd_bg(psd_fp, scale=0.5):
|
||||||
|
'''Export layers of the psd, create and return a json file
|
||||||
|
psd_fp :: filepath to the psd file
|
||||||
|
scale :: output resolution of the layers (ex: 0.5 means divided by 2)
|
||||||
|
'''
|
||||||
|
|
||||||
|
t0 = time()
|
||||||
|
psd_fp = Path(psd_fp)
|
||||||
|
|
||||||
|
## FIXME: Choose how to handle this nomenclature
|
||||||
|
output = psd_fp.parent / 'render'
|
||||||
|
if scale >= 1:
|
||||||
|
output = psd_fp.parent / 'render_hd'
|
||||||
|
|
||||||
|
# print('Folder Output', output)
|
||||||
|
|
||||||
|
## note: No output passed create 'render' folder aside psd automatically
|
||||||
|
layers, image_size, org_image_size, png_dir = export_psd(psd_fp, output, scale=scale)
|
||||||
|
json_fp = png_dir/'setup.json'
|
||||||
|
|
||||||
|
setup_dic = {
|
||||||
|
'psd': f'../{psd_fp.name}' ,'layers': layers, 'image_size': image_size,
|
||||||
|
'org_image_size' : org_image_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
## Dump data to json file
|
||||||
|
|
||||||
|
json_file = png_dir / 'setup.json'
|
||||||
|
|
||||||
|
# json.dump(str(json_file), setup_dic, ensure_ascii=False)
|
||||||
|
json_file.write_text(json.dumps(setup_dic, indent=4, ensure_ascii=False), encoding='utf8')
|
||||||
|
|
||||||
|
print(f'Exported in : {time() - t0:.2f}s')
|
||||||
|
return json_fp
|
||||||
|
|
||||||
|
class BPM_OT_export_psd_layers(bpy.types.Operator, ImportHelper):
|
||||||
|
bl_idname = "bpm.export_psd_layers"
|
||||||
|
bl_label = "Export Psd Layers"
|
||||||
|
bl_description = "Export psd layers from a psd file in a render folder"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'GPENCIL'
|
||||||
|
|
||||||
|
filename_ext = '.psd'
|
||||||
|
|
||||||
|
filter_glob: bpy.props.StringProperty(default='*.psd', options={'HIDDEN'} )
|
||||||
|
|
||||||
|
filepath : bpy.props.StringProperty(
|
||||||
|
name="File Path",
|
||||||
|
description="File path used for import",
|
||||||
|
maxlen= 1024)
|
||||||
|
|
||||||
|
resolution_scale: bpy.props.FloatProperty(
|
||||||
|
name='Resolution Scale',
|
||||||
|
default=1.0,
|
||||||
|
description='Export resolution ratio. Ex: 0.5 export at half resolution\
|
||||||
|
\nusefull for non-def lightweight backgrounds in Blender',
|
||||||
|
options={'HIDDEN'})
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
|
||||||
|
json_fp = export_psd_bg(self.filepath, scale=self.resolution_scale)
|
||||||
|
if not json_fp:
|
||||||
|
self.report({'ERROR'}, f'Problem when exporting image for: {self.filepath}')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'BG exported, Json at: {json_fp}')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
BPM_OT_export_psd_layers,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,134 @@
|
||||||
|
import importlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def install_module(module_name, package_name=None):
|
||||||
|
'''Install a python module with pip or return it if already installed'''
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
print(f'Installing Module {module_name} ....')
|
||||||
|
|
||||||
|
subprocess.call([sys.executable, '-m', 'ensurepip'])
|
||||||
|
subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name])
|
||||||
|
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
def import_module_from_path(path):
|
||||||
|
from importlib import util
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = Path(path)
|
||||||
|
spec = util.spec_from_file_location(path.stem, str(path))
|
||||||
|
mod = util.module_from_spec(spec)
|
||||||
|
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
|
||||||
|
return mod
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Cannot import file {path}')
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def norm_str(string, separator='_', format=str.lower, padding=0):
|
||||||
|
string = str(string)
|
||||||
|
string = string.replace('_', ' ')
|
||||||
|
string = string.replace('-', ' ')
|
||||||
|
string = re.sub('[ ]+', ' ', string)
|
||||||
|
string = re.sub('[ ]+\/[ ]+', '/', string)
|
||||||
|
string = string.strip()
|
||||||
|
|
||||||
|
if format:
|
||||||
|
string = format(string)
|
||||||
|
|
||||||
|
# Padd rightest number
|
||||||
|
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
||||||
|
|
||||||
|
string = string.replace(' ', separator)
|
||||||
|
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def norm_name(string, separator='_', format=str.lower, padding=0):
|
||||||
|
string = str(string)
|
||||||
|
string = string.split('/')[-1] #used to remove first slash -> albert / albert_casque -> albert_casque
|
||||||
|
string = string.replace('_', ' ')
|
||||||
|
string = string.replace('-', ' ')
|
||||||
|
string = re.sub('[ ]+', ' ', string)
|
||||||
|
string = re.sub('[ ]+\/[ ]+', '/', string)
|
||||||
|
string = string.strip()
|
||||||
|
|
||||||
|
if format:
|
||||||
|
string = format(string)
|
||||||
|
|
||||||
|
# Padd rightest number
|
||||||
|
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
||||||
|
|
||||||
|
string = string.replace(' ', separator)
|
||||||
|
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def read_file(path):
|
||||||
|
'''Read a file with an extension in (json, yaml, yml, txt)'''
|
||||||
|
|
||||||
|
exts = ('.json', '.yaml', '.yml', '.txt')
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
print('Try to read empty file')
|
||||||
|
|
||||||
|
path = Path(path)
|
||||||
|
if not path.exists():
|
||||||
|
print('File not exist', path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if path.suffix not in exts:
|
||||||
|
print(f'Cannot read file {path}, extension must be in {exts}')
|
||||||
|
return
|
||||||
|
|
||||||
|
txt = path.read_text()
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if path.suffix.lower() in ('.yaml', '.yml'):
|
||||||
|
yaml = install_module('yaml')
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(txt)
|
||||||
|
except Exception:
|
||||||
|
print(f'Could not load yaml file {path}')
|
||||||
|
return
|
||||||
|
elif path.suffix.lower() == '.json':
|
||||||
|
try:
|
||||||
|
data = json.loads(txt)
|
||||||
|
except Exception:
|
||||||
|
print(f'Could not load json file {path}')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
data = txt
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def open_file(filepath, env=None, select=False):
|
||||||
|
if platform.system() == 'Darwin': # macOS
|
||||||
|
cmd = ['open']
|
||||||
|
if select:
|
||||||
|
cmd += ['-R']
|
||||||
|
|
||||||
|
elif platform.system() == 'Windows': # Windows
|
||||||
|
cmd = ['explorer']
|
||||||
|
if select:
|
||||||
|
cmd += ['/select,']
|
||||||
|
else: # linux variants
|
||||||
|
cmd = ['xdg-open']
|
||||||
|
if select:
|
||||||
|
cmd = ['nemo']
|
||||||
|
|
||||||
|
cmd += [str(filepath)]
|
||||||
|
|
||||||
|
subprocess.Popen(cmd, env=env)
|
|
@ -0,0 +1,809 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
## Standalone based on SPA code
|
||||||
|
## Code from create_gp_texture_ref.py (Copyright (C) 2023, The SPA Studios. All rights reserved.)
|
||||||
|
## adapted to create as a single 'GP as image' and fit in camera with driver
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from mathutils import Vector, geometry
|
||||||
|
from pprint import pprint as pp
|
||||||
|
from .constants import PREFIX, BGCOL
|
||||||
|
|
||||||
|
def get_addon_prefs():
|
||||||
|
return bpy.context.preferences.addons[__package__].preferences
|
||||||
|
|
||||||
|
def norm_str(string, separator='_', format=str.lower, padding=0):
|
||||||
|
import unicodedata
|
||||||
|
string = str(string)
|
||||||
|
string = string.replace('_', ' ')
|
||||||
|
string = string.replace('-', ' ')
|
||||||
|
string = re.sub('[ ]+', ' ', string)
|
||||||
|
string = re.sub('[ ]+\/[ ]+', '/', string)
|
||||||
|
string = string.strip()
|
||||||
|
|
||||||
|
if format:
|
||||||
|
string = format(string)
|
||||||
|
|
||||||
|
# Padd rightest number
|
||||||
|
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
||||||
|
|
||||||
|
string = string.replace(' ', separator)
|
||||||
|
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def clean_image_name(name):
|
||||||
|
'''Strip blender numbering then strip extension'''
|
||||||
|
name = re.sub(r'\.\d{3}$', '', name.strip())
|
||||||
|
return os.path.splitext(name)[0].strip()
|
||||||
|
|
||||||
|
def get_image_infos_from_object(ob):
|
||||||
|
'''Get image filepath of a background whatever the type
|
||||||
|
return image_data, transparency'''
|
||||||
|
|
||||||
|
transparency = None
|
||||||
|
if ob.type == 'EMPTY':
|
||||||
|
image_data = ob.data
|
||||||
|
transparency = ob.color[3]
|
||||||
|
|
||||||
|
elif ob.type == 'MESH':
|
||||||
|
texture_plane_ng = bpy.data.node_groups.get('texture_plane')
|
||||||
|
if not texture_plane_ng:
|
||||||
|
print(f'{ob.name} : No texture plane nodegroup found !')
|
||||||
|
return
|
||||||
|
|
||||||
|
mat = ob.active_material
|
||||||
|
node = next((n for n in mat.node_tree.nodes
|
||||||
|
if n.type=='GROUP' and n.node_tree is texture_plane_ng), None)
|
||||||
|
if not node:
|
||||||
|
print(f'{ob.name} : No texture plane nodegroup in material {mat.name}')
|
||||||
|
return
|
||||||
|
|
||||||
|
## Get input texture
|
||||||
|
tex_node = next(n for n in mat.node_tree.nodes
|
||||||
|
if n.type=='TEX_IMAGE')
|
||||||
|
|
||||||
|
if not tex_node:
|
||||||
|
print(f'{ob.name} : No image texture node in material {mat.name}')
|
||||||
|
return
|
||||||
|
|
||||||
|
image_data = tex_node.image
|
||||||
|
transparency = node.inputs['Transparency'].default_value
|
||||||
|
|
||||||
|
elif ob.type == 'GPENCIL':
|
||||||
|
image_data = ob.data.materials[0].grease_pencil.fill_image
|
||||||
|
transparency = ob.data.layers[0].opacity
|
||||||
|
|
||||||
|
if not image_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
return (image_data, transparency)
|
||||||
|
|
||||||
|
def get_image(ob):
|
||||||
|
'''Get image data from image object'''
|
||||||
|
infos = get_image_infos_from_object(ob)
|
||||||
|
if infos is None:
|
||||||
|
return
|
||||||
|
img, _opacity = infos
|
||||||
|
return img
|
||||||
|
|
||||||
|
def get_opacity(ob):
|
||||||
|
'''Get opacity from image object'''
|
||||||
|
infos = get_image_infos_from_object(ob)
|
||||||
|
if infos is None:
|
||||||
|
return
|
||||||
|
_img, opacity = infos
|
||||||
|
return opacity
|
||||||
|
|
||||||
|
def set_opacity(ob, opacity):
|
||||||
|
'''Set opacity of BG image object whatever the type'''
|
||||||
|
if ob.type == 'EMPTY':
|
||||||
|
ob.color[3] = opacity
|
||||||
|
|
||||||
|
elif ob.type == 'MESH':
|
||||||
|
texture_plane_ng = bpy.data.node_groups.get('texture_plane')
|
||||||
|
if texture_plane_ng:
|
||||||
|
node = next(n for n in ob.active_material.node_tree.nodes
|
||||||
|
if n.type == 'GROUP' and n.node_tree is texture_plane_ng)
|
||||||
|
node.inputs['Transparency'].default_value = opacity
|
||||||
|
|
||||||
|
elif ob.type == 'GPENCIL':
|
||||||
|
ob.data.layers[0].opacity = opacity
|
||||||
|
|
||||||
|
def create_plane_holder(img, name=None, parent=None, link_in_col=None):
|
||||||
|
'''
|
||||||
|
img :: blender image or path to an image
|
||||||
|
prefix :: name holder with image name with given prefix
|
||||||
|
parent :: object to parent to (only if passed)
|
||||||
|
link_in_col :: collection to link after creation
|
||||||
|
|
||||||
|
Return created object mesh without face
|
||||||
|
None if creation failed
|
||||||
|
'''
|
||||||
|
## get image if a path is passed in
|
||||||
|
if not isinstance(img, bpy.types.Image):
|
||||||
|
try:
|
||||||
|
# load from path (str)
|
||||||
|
img = bpy.data.images.load(str(img), check_existing=True)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
# name = img.name.split('.')[0]
|
||||||
|
name = name or clean_image_name(img.name)
|
||||||
|
obj_name = name # PREFIX + name # <- always set prefix ?
|
||||||
|
x,y = img.size[:]
|
||||||
|
|
||||||
|
# create and place object plane
|
||||||
|
# find height from width (as 1)
|
||||||
|
|
||||||
|
if y < x:
|
||||||
|
h = y/x
|
||||||
|
w = 1
|
||||||
|
else:
|
||||||
|
h = 1
|
||||||
|
w = x/y
|
||||||
|
# size/2 to get center image (half width | height)
|
||||||
|
h = h/2
|
||||||
|
w = w/2
|
||||||
|
|
||||||
|
verts = [
|
||||||
|
(-w, h, 0),#2
|
||||||
|
(w, h, 0),#3
|
||||||
|
(w, -h, 0),#4
|
||||||
|
(-w, -h, 0),#1
|
||||||
|
]
|
||||||
|
|
||||||
|
mesh = bpy.data.meshes.new(obj_name)
|
||||||
|
mesh.from_pydata(verts, [(3,2), (2,1), (1,0), (0,3)], [])
|
||||||
|
holder = bpy.data.objects.new(obj_name, mesh)
|
||||||
|
holder.lock_location = (True, True, False)
|
||||||
|
holder.hide_render = True
|
||||||
|
|
||||||
|
if link_in_col is not None:
|
||||||
|
link_in_col.objects.link(holder)
|
||||||
|
# bpy.context.scene.collection.objects.link(holder)
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
holder.parent = parent
|
||||||
|
return holder
|
||||||
|
|
||||||
|
## Remove func is no intermediate collection created
|
||||||
|
def get_col(name, parent=None, create=True):
|
||||||
|
parent = parent or bpy.context.scene.collection
|
||||||
|
col = bpy.data.collections.get(name)
|
||||||
|
if not col and create:
|
||||||
|
col = bpy.data.collections.new(name)
|
||||||
|
|
||||||
|
if col not in parent.children[:]:
|
||||||
|
parent.children.link(col)
|
||||||
|
|
||||||
|
return col
|
||||||
|
|
||||||
|
def create_cam(name, type='ORTHO', size=6, focale=50):
|
||||||
|
'''Create an orthographic camera with given scale
|
||||||
|
size :: othographic scale of the camera
|
||||||
|
Return camera object
|
||||||
|
'''
|
||||||
|
|
||||||
|
cam_data = bpy.data.cameras.new(name)
|
||||||
|
cam_data.type = type
|
||||||
|
cam_data.clip_start = 0.02
|
||||||
|
cam_data.clip_end = 2000
|
||||||
|
cam_data.ortho_scale = size
|
||||||
|
cam_data.lens = focale
|
||||||
|
cam = bpy.data.objects.new(name, cam_data)
|
||||||
|
cam.rotation_euler.x = 1.5707963705062866
|
||||||
|
cam.location = (0, -8, 0)
|
||||||
|
bpy.context.scene.collection.objects.link(cam)
|
||||||
|
return cam
|
||||||
|
|
||||||
|
def link_nodegroup(filepath, group_name, link=True, keep_existing=False):
|
||||||
|
ng = bpy.data.node_groups.get(group_name)
|
||||||
|
if ng:
|
||||||
|
if keep_existing:
|
||||||
|
return ng
|
||||||
|
## risk deleting nodegroup from other object !
|
||||||
|
# else:
|
||||||
|
# bpy.data.node_groups.remove(ng)
|
||||||
|
|
||||||
|
lib_path = bpy.path.relpath(str(filepath))
|
||||||
|
with bpy.data.libraries.load(lib_path, link=link) as (data_from, data_to):
|
||||||
|
data_to.node_groups = [n for n in data_from.node_groups if n.startswith(group_name)]
|
||||||
|
|
||||||
|
group = bpy.data.node_groups.get(group_name)
|
||||||
|
if group:
|
||||||
|
return group
|
||||||
|
|
||||||
|
def create_plane_driver(plane, cam, distance=None):
|
||||||
|
|
||||||
|
# Multiple planes spacing
|
||||||
|
plane.lock_location = (True, True, False)
|
||||||
|
plane.lock_rotation = (True,)*3
|
||||||
|
plane.lock_scale = (True,)*3
|
||||||
|
|
||||||
|
plane['scale'] = 1.0
|
||||||
|
plane['distance'] = distance if distance is not None else 9.0
|
||||||
|
|
||||||
|
# DRIVERS
|
||||||
|
## LOC X AND Y (shift) ##
|
||||||
|
for axis in range(2):
|
||||||
|
driver = plane.driver_add('scale', axis)
|
||||||
|
|
||||||
|
# Driver type
|
||||||
|
driver.driver.type = 'SCRIPTED'
|
||||||
|
|
||||||
|
# Variable DISTANCE
|
||||||
|
var = driver.driver.variables.new()
|
||||||
|
var.name = "distance"
|
||||||
|
#-# use a propertie to drive distance to camera
|
||||||
|
var.type = 'SINGLE_PROP'
|
||||||
|
var.targets[0].id = plane
|
||||||
|
var.targets[0].data_path = '["distance"]'
|
||||||
|
#-# Can use directly object location (but give refresh problem)
|
||||||
|
# var.type = 'TRANSFORMS'
|
||||||
|
# var.targets[0].id = plane
|
||||||
|
# var.targets[0].transform_type = 'LOC_Z'
|
||||||
|
# var.targets[0].transform_space = 'LOCAL_SPACE'
|
||||||
|
|
||||||
|
# Variable FOV
|
||||||
|
var = driver.driver.variables.new()
|
||||||
|
var.name = "FOV"
|
||||||
|
var.type = 'SINGLE_PROP'
|
||||||
|
var.targets[0].id_type = "OBJECT"
|
||||||
|
var.targets[0].id = cam
|
||||||
|
var.targets[0].data_path = 'data.angle'
|
||||||
|
|
||||||
|
# Variable scale
|
||||||
|
var = driver.driver.variables.new()
|
||||||
|
var.name = "scale"
|
||||||
|
var.type = 'SINGLE_PROP'
|
||||||
|
var.targets[0].id = plane
|
||||||
|
var.targets[0].data_path = '["scale"]'
|
||||||
|
|
||||||
|
# Expression
|
||||||
|
# -distance*tan(FOV/2)
|
||||||
|
driver.driver.expression = \
|
||||||
|
"tan(FOV/2) * distance * scale * 2"
|
||||||
|
# "tan(FOV/2) * -distance * scale * 2" # inverse dist if based on locZ
|
||||||
|
|
||||||
|
# Driver for location
|
||||||
|
dist_drv = plane.driver_add('location', 2)
|
||||||
|
dist_drv.driver.type = 'SCRIPTED' #'AVERAGE' #need to be inverted
|
||||||
|
var = dist_drv.driver.variables.new()
|
||||||
|
var.name = "distance"
|
||||||
|
var.type = 'SINGLE_PROP'
|
||||||
|
var.targets[0].id = plane
|
||||||
|
var.targets[0].data_path = '["distance"]'
|
||||||
|
dist_drv.driver.expression = '-distance'
|
||||||
|
|
||||||
|
return driver, dist_drv
|
||||||
|
|
||||||
|
def set_collection(ob, collection, unlink=True) :
|
||||||
|
''' link an object in a collection and create it if necessary
|
||||||
|
if unlink object is removed from other collections
|
||||||
|
|
||||||
|
return collection (get/created)
|
||||||
|
'''
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
col = None
|
||||||
|
visible = False
|
||||||
|
linked = False
|
||||||
|
|
||||||
|
# check if collection exist or create it
|
||||||
|
for c in bpy.data.collections :
|
||||||
|
if c.name == collection : col = c
|
||||||
|
if not col :
|
||||||
|
col = bpy.data.collections.new(name=collection)
|
||||||
|
|
||||||
|
# link the collection to the scene's collection if necessary
|
||||||
|
# for c in scn.collection.children :
|
||||||
|
# if c.name == col.name : visible = True
|
||||||
|
# if not visible : scn.collection.children.link(col)
|
||||||
|
|
||||||
|
# check if the object is already in the collection and link it if necessary
|
||||||
|
for o in col.objects :
|
||||||
|
if o == ob : linked = True
|
||||||
|
if not linked : col.objects.link(ob)
|
||||||
|
|
||||||
|
# remove object from scene's collection
|
||||||
|
for o in scn.collection.objects :
|
||||||
|
if o == ob : scn.collection.objects.unlink(ob)
|
||||||
|
|
||||||
|
# if unlink flag we remove the object from other collections
|
||||||
|
if unlink :
|
||||||
|
for c in ob.users_collection :
|
||||||
|
if c.name != collection : c.objects.unlink(ob)
|
||||||
|
|
||||||
|
return col
|
||||||
|
|
||||||
|
def placeholder_name(name='', context=None) -> str:
|
||||||
|
|
||||||
|
# def increment(match):
|
||||||
|
# return str(int(match.group(1))+1).zfill(len(match.group(1)))
|
||||||
|
|
||||||
|
context = context or bpy.context
|
||||||
|
name = name.strip()
|
||||||
|
if not name:
|
||||||
|
# Create a default name (fing last 3 number, before blender increment if exists, default increment)
|
||||||
|
numbers = [int(match.group(1)) for o in context.scene.objects\
|
||||||
|
if (match := re.search(r'^drawing.*_(\d+)(?:\.\d{3})?$', o.name, flags=re.I))\
|
||||||
|
# and o.type == 'GPENCIL'
|
||||||
|
]
|
||||||
|
if numbers:
|
||||||
|
numbers.sort()
|
||||||
|
name = f'drawing_{numbers[-1] + 1:03d}'
|
||||||
|
else:
|
||||||
|
name = 'drawing_001'
|
||||||
|
|
||||||
|
# elif context.scene.objects.get(name):
|
||||||
|
# name = re.sub(r'(\d+)(?!.*\d)', increment, name) # find rightmost number
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
def reset_gp_uv(ob):
|
||||||
|
uvs = [(0.5, 0.5), (-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5)]
|
||||||
|
try:
|
||||||
|
for p, uv in zip(ob.data.layers[0].frames[0].strokes[0].points, uvs):
|
||||||
|
p.uv_fill = uv
|
||||||
|
except:
|
||||||
|
print('Could not set Gp points UV')
|
||||||
|
|
||||||
|
## Get empty coords
|
||||||
|
def get_ref_object_space_coord(o):
|
||||||
|
size = o.empty_display_size
|
||||||
|
x,y = o.empty_image_offset
|
||||||
|
img = o.data
|
||||||
|
|
||||||
|
res_x, res_y = img.size
|
||||||
|
scaling = 1 / max(res_x, res_y)
|
||||||
|
|
||||||
|
# 3----2
|
||||||
|
# | |
|
||||||
|
# 0----1
|
||||||
|
|
||||||
|
corners = [
|
||||||
|
Vector((0,0)),
|
||||||
|
Vector((res_x, 0)),
|
||||||
|
Vector((0, res_y)),
|
||||||
|
Vector((res_x, res_y)),
|
||||||
|
]
|
||||||
|
|
||||||
|
obj_space_corners = []
|
||||||
|
for co in corners:
|
||||||
|
nco_x = ((co.x + (x * res_x)) * size) * scaling
|
||||||
|
nco_y = ((co.y + (y * res_y)) * size) * scaling
|
||||||
|
obj_space_corners.append(Vector((nco_x, nco_y, 0)))
|
||||||
|
return obj_space_corners
|
||||||
|
|
||||||
|
|
||||||
|
## Generate GP object
|
||||||
|
def create_gpencil_reference(
|
||||||
|
gpd: bpy.types.GreasePencil,
|
||||||
|
gpf: bpy.types.GPencilFrame,
|
||||||
|
image: bpy.types.Image,
|
||||||
|
) -> bpy.types.GPencilStroke:
|
||||||
|
"""
|
||||||
|
Add a rectangular stroke textured with `image` to the given grease pencil fame.
|
||||||
|
:param gpd: The grease pencil data.
|
||||||
|
:param gpf: The grease pencil frame.
|
||||||
|
:param image: The image to use as texture.
|
||||||
|
:return: The created grease pencil stroke.
|
||||||
|
"""
|
||||||
|
name = clean_image_name(image.name) + '_texgp'
|
||||||
|
|
||||||
|
# Create new material
|
||||||
|
mat = bpy.data.materials.new(f".ref/{name}")
|
||||||
|
bpy.data.materials.create_gpencil_data(mat)
|
||||||
|
gpd.materials.append(mat)
|
||||||
|
idx = gpd.materials.find(mat.name)
|
||||||
|
|
||||||
|
# Setup material settings
|
||||||
|
mat.grease_pencil.show_stroke = False
|
||||||
|
mat.grease_pencil.show_fill = True
|
||||||
|
mat.grease_pencil.fill_image = image
|
||||||
|
mat.grease_pencil.fill_style = "TEXTURE"
|
||||||
|
mat.grease_pencil.mix_factor = 0.0
|
||||||
|
mat.grease_pencil.texture_offset = (0.0, 0.0)
|
||||||
|
mat.grease_pencil.texture_angle = 0.0
|
||||||
|
mat.grease_pencil.texture_scale = (1.0, 1.0)
|
||||||
|
mat.grease_pencil.texture_clamp = True
|
||||||
|
|
||||||
|
# Create the stroke
|
||||||
|
gps_new = gpf.strokes.new()
|
||||||
|
gps_new.points.add(4, pressure=0, strength=0)
|
||||||
|
|
||||||
|
## This will make sure that the uv's always remain the same
|
||||||
|
# gps_new.use_automatic_uvs = False # <<- /!\ exists only in SPA core changes
|
||||||
|
|
||||||
|
gps_new.use_cyclic = True
|
||||||
|
gps_new.material_index = idx
|
||||||
|
|
||||||
|
x,y = image.size[:]
|
||||||
|
|
||||||
|
# create and place object plane
|
||||||
|
# find height from width (as 1)
|
||||||
|
if y < x:
|
||||||
|
h = y/x
|
||||||
|
w = 1
|
||||||
|
else:
|
||||||
|
h = 1
|
||||||
|
w = x/y
|
||||||
|
# size/2 to get center image (half width | height)
|
||||||
|
h = h/2
|
||||||
|
w = w/2
|
||||||
|
|
||||||
|
coords = [
|
||||||
|
(w, h, 0),
|
||||||
|
(-w, h, 0),
|
||||||
|
(-w, -h, 0),
|
||||||
|
(w, -h, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
uvs = [(0.5, 0.5), (-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5)]
|
||||||
|
|
||||||
|
for i, (co, uv) in enumerate(zip(coords, uvs)):
|
||||||
|
gps_new.points[i].co = co
|
||||||
|
gps_new.points[i].uv_fill = uv
|
||||||
|
|
||||||
|
return gps_new
|
||||||
|
|
||||||
|
def import_image_as_gp_reference(
|
||||||
|
context: bpy.types.Context,
|
||||||
|
image,
|
||||||
|
pack_image: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Import image from `image` as a textured rectangle in the given
|
||||||
|
grease pencil object.
|
||||||
|
:param context: The active context.
|
||||||
|
:param image: The image filepath or image datablock
|
||||||
|
:param pack_image: Whether to pack the image into the Blender file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not image:
|
||||||
|
return
|
||||||
|
|
||||||
|
scene = context.scene
|
||||||
|
if not isinstance(image, bpy.types.Image):
|
||||||
|
image = bpy.data.images.load(str(image), check_existing=True)
|
||||||
|
|
||||||
|
## Create grease pencil object
|
||||||
|
gpd = bpy.data.grease_pencils.new(image.name)
|
||||||
|
ob = bpy.data.objects.new(image.name, gpd)
|
||||||
|
gpl = gpd.layers.new(image.name)
|
||||||
|
gpf = gpl.frames.new(0) # TDOO: test negative
|
||||||
|
|
||||||
|
if pack_image:
|
||||||
|
image.pack()
|
||||||
|
|
||||||
|
gps: bpy.types.GPencilStroke = create_gpencil_reference(
|
||||||
|
gpd,
|
||||||
|
gpf,
|
||||||
|
image,
|
||||||
|
)
|
||||||
|
|
||||||
|
gps.select = True # Not needed
|
||||||
|
|
||||||
|
cam = scene.camera # should be bg_cam
|
||||||
|
|
||||||
|
# Create the driver and parent
|
||||||
|
ob.use_grease_pencil_lights = False
|
||||||
|
ob['is_background'] = True
|
||||||
|
print(f'Created {ob.name} GP background object')
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def gp_transfer_mode(ob, context=None):
|
||||||
|
context= context or bpy.context
|
||||||
|
if ob.type != 'GPENCIL' or context.object is ob:
|
||||||
|
return
|
||||||
|
|
||||||
|
prev_mode = context.mode
|
||||||
|
possible_gp_mods = ('OBJECT',
|
||||||
|
'EDIT_GPENCIL', 'SCULPT_GPENCIL', 'PAINT_GPENCIL',
|
||||||
|
'WEIGHT_GPENCIL', 'VERTEX_GPENCIL')
|
||||||
|
|
||||||
|
if prev_mode not in possible_gp_mods:
|
||||||
|
prev_mode = None
|
||||||
|
|
||||||
|
mode_swap = False
|
||||||
|
|
||||||
|
## TODO optional: Option to stop mode sync ?
|
||||||
|
## Set in same mode as previous object
|
||||||
|
if context.scene.tool_settings.lock_object_mode:
|
||||||
|
if context.mode != 'OBJECT':
|
||||||
|
mode_swap = True
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# set active
|
||||||
|
context.view_layer.objects.active = ob
|
||||||
|
|
||||||
|
## keep same mode accross objects
|
||||||
|
if mode_swap and prev_mode is not None:
|
||||||
|
bpy.ops.object.mode_set(mode=prev_mode)
|
||||||
|
|
||||||
|
else:
|
||||||
|
## keep same mode accross objects
|
||||||
|
context.view_layer.objects.active = ob
|
||||||
|
if context.mode != prev_mode is not None:
|
||||||
|
bpy.ops.object.mode_set(mode=prev_mode)
|
||||||
|
|
||||||
|
for o in [o for o in context.scene.objects if o.type == 'GPENCIL']:
|
||||||
|
o.select_set(o == ob) # select only active (when not in object mode)
|
||||||
|
|
||||||
|
## generate tex plane
|
||||||
|
# def create_image_plane(coords, name):
|
||||||
|
# '''Create an a mesh plane with a defaut UVmap from passed coordinate
|
||||||
|
# object and mesh get passed name
|
||||||
|
|
||||||
|
# return plane object
|
||||||
|
# '''
|
||||||
|
# fac = [(0, 1, 3, 2)]
|
||||||
|
# me = bpy.data.meshes.new(name)
|
||||||
|
# me.from_pydata(coords, [], fac)
|
||||||
|
# plane = bpy.data.objects.new(name, me)
|
||||||
|
# # col = bpy.context.collection
|
||||||
|
# # col.objects.link(plane)
|
||||||
|
|
||||||
|
# me.uv_layers.new(name='UVMap')
|
||||||
|
# return plane
|
||||||
|
|
||||||
|
def create_material(name, img, node_group, keep_existing=False):
|
||||||
|
if keep_existing:
|
||||||
|
## Reuse if exist and seem clean
|
||||||
|
m = bpy.data.materials.get(name)
|
||||||
|
if m:
|
||||||
|
if name in m.name and m.use_nodes:
|
||||||
|
valid_image = next((n for n in m.node_tree.nodes if n.type == 'TEX_IMAGE' and n.image == img), None)
|
||||||
|
valid_nodegroup = next((n for n in m.node_tree.nodes if n.type == 'GROUP' and n.node_tree), None)
|
||||||
|
if valid_image and valid_nodegroup:
|
||||||
|
# Replace node_tree if different
|
||||||
|
print('node_group: ', node_group)
|
||||||
|
print('valid_nodegroup.node_tree: ', valid_nodegroup.node_tree)
|
||||||
|
if node_group != valid_nodegroup.node_tree:
|
||||||
|
valid_nodegroup = node_group
|
||||||
|
return m
|
||||||
|
|
||||||
|
# create mat
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.blend_method = 'BLEND' # 'CLIP' (clip is able to respect space)
|
||||||
|
|
||||||
|
mat.use_nodes = True
|
||||||
|
|
||||||
|
node_tree = mat.node_tree
|
||||||
|
nodes = node_tree.nodes
|
||||||
|
links = node_tree.links
|
||||||
|
|
||||||
|
# clear default BSDG
|
||||||
|
principled = nodes.get('Principled BSDF')
|
||||||
|
if principled: nodes.remove(principled)
|
||||||
|
mat_output = nodes.get('Material Output')
|
||||||
|
|
||||||
|
group = nodes.new("ShaderNodeGroup")
|
||||||
|
group.node_tree = node_group
|
||||||
|
group.location = (0, 276)
|
||||||
|
|
||||||
|
img_tex = nodes.new('ShaderNodeTexImage')
|
||||||
|
img_tex.image = img
|
||||||
|
img_tex.interpolation = 'Linear' # 'Closest','Cubic','Smart'
|
||||||
|
img_tex.extension = 'CLIP' # or EXTEND
|
||||||
|
img_tex.location = (-400, 218)
|
||||||
|
|
||||||
|
# links.new(img_tex.outputs['Color'], group.inputs['Color'])
|
||||||
|
# links.new(img_tex.outputs['Alpha'], group.inputs['Alpha'])
|
||||||
|
links.new(img_tex.outputs[0], group.inputs[0])
|
||||||
|
links.new(img_tex.outputs[1], group.inputs[1])
|
||||||
|
links.new(group.outputs[0], mat_output.inputs[0])
|
||||||
|
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def create_image_plane(img, node_group, parent=None):
|
||||||
|
'''
|
||||||
|
img :: blender image or path to an image
|
||||||
|
parent :: object to parent to (only if passed)
|
||||||
|
|
||||||
|
Return created object or None if creation failed
|
||||||
|
'''
|
||||||
|
|
||||||
|
## get image if a path is passed in
|
||||||
|
if not isinstance(img, bpy.types.Image):
|
||||||
|
try:
|
||||||
|
img = bpy.data.images.load(str(img), check_existing=True)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
name = img.name.split('.')[0]
|
||||||
|
obj_name = f'{name}_texplane'
|
||||||
|
x,y = img.size[:]
|
||||||
|
|
||||||
|
# create and place object plane
|
||||||
|
# find height from width (as 1)
|
||||||
|
if y < x:
|
||||||
|
h = y/x
|
||||||
|
w = 1
|
||||||
|
else:
|
||||||
|
h = 1
|
||||||
|
w = x/y
|
||||||
|
# size/2 to get center image (half width | height)
|
||||||
|
h = h/2
|
||||||
|
w = w/2
|
||||||
|
|
||||||
|
verts = [
|
||||||
|
(-w, h, 0),#2
|
||||||
|
(w, h, 0),#3
|
||||||
|
(w, -h, 0),#4
|
||||||
|
(-w, -h, 0),#1
|
||||||
|
]
|
||||||
|
|
||||||
|
faces = [(3, 2, 1, 0)]
|
||||||
|
mesh = bpy.data.meshes.new(obj_name)
|
||||||
|
mesh.from_pydata(verts, [], faces)
|
||||||
|
plane = bpy.data.objects.new(obj_name, mesh)
|
||||||
|
|
||||||
|
# setup material (link nodegroup)
|
||||||
|
|
||||||
|
plane.data.uv_layers.new()
|
||||||
|
|
||||||
|
# Create and assign material
|
||||||
|
|
||||||
|
mat = create_material(name, img, node_group)
|
||||||
|
plane.data.materials.append(mat)
|
||||||
|
bpy.context.scene.collection.objects.link(plane)
|
||||||
|
# full lock (or just kill selectability...)
|
||||||
|
plane.lock_location = [True]*3
|
||||||
|
plane.hide_select = True
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
plane.parent = parent
|
||||||
|
|
||||||
|
return plane
|
||||||
|
|
||||||
|
def create_empty_image(image):
|
||||||
|
if not isinstance(image, bpy.types.Image):
|
||||||
|
try:
|
||||||
|
image = bpy.data.images.load(str(image), check_existing=True)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ob = bpy.data.objects.new(f'{image_name}_texempty', None)
|
||||||
|
ob = bpy.data.objects.new(image.name, None)
|
||||||
|
# ob.hide_viewport = (cam_type == 'ORTHO') # hide if in persp mode
|
||||||
|
|
||||||
|
# load image on the empty
|
||||||
|
ob.empty_display_type = 'IMAGE'
|
||||||
|
ob.data = image
|
||||||
|
# ob.empty_display_size = bg_cam.data.ortho_scale # set display size
|
||||||
|
ob.use_empty_image_alpha = True
|
||||||
|
ob.color[3] = 0.5 # transparency
|
||||||
|
return ob
|
||||||
|
|
||||||
|
## Scan background objects and reload
|
||||||
|
|
||||||
|
def scan_backgrounds(context=None, scene=None, all_bg=False):
|
||||||
|
'''return a list (sorted by negative locZ)
|
||||||
|
of all holder object (parent of texempty and texplane)
|
||||||
|
'''
|
||||||
|
|
||||||
|
context = context or bpy.context
|
||||||
|
scene = scene or context.scene
|
||||||
|
|
||||||
|
oblist = []
|
||||||
|
if all_bg:
|
||||||
|
# oblist = [o for o in scene.objects if o.get('is_background_holder')]
|
||||||
|
oblist = [o for o in scene.objects if o.name.startswith(PREFIX) and o.type == 'MESH']
|
||||||
|
else:
|
||||||
|
col = bpy.data.collections.get(BGCOL)
|
||||||
|
if not col:
|
||||||
|
return oblist
|
||||||
|
# oblist = [o for o in col.all_objects if o.get('is_background_holder')]
|
||||||
|
oblist = [o for o in col.all_objects if o.name.startswith(PREFIX) and o.type == 'MESH']
|
||||||
|
|
||||||
|
oblist.sort(key=lambda x : x.location.z, reverse=True)
|
||||||
|
return oblist
|
||||||
|
|
||||||
|
def add_loc_z_driver(plane):
|
||||||
|
# Create properties
|
||||||
|
# plane['distance'] = abs(plane.location.z)
|
||||||
|
plane['distance'] = -plane.location.z
|
||||||
|
# Driver for location
|
||||||
|
dist_drv = plane.driver_add('location', 2)
|
||||||
|
dist_drv.driver.type = 'SCRIPTED' #'AVERAGE' #need to be inverted
|
||||||
|
var = dist_drv.driver.variables.new()
|
||||||
|
var.name = "distance"
|
||||||
|
var.type = 'SINGLE_PROP'
|
||||||
|
var.targets[0].id = plane
|
||||||
|
var.targets[0].data_path = '["distance"]'
|
||||||
|
dist_drv.driver.expression = '-distance'
|
||||||
|
|
||||||
|
def reload_bg_list(scene=None):
|
||||||
|
'''List holder plane object and add them to bg collection'''
|
||||||
|
start = time.time()
|
||||||
|
scn = scene or bpy.context.scene
|
||||||
|
uilist = scn.bg_props.planes
|
||||||
|
# current_index = scn.bg_props.index
|
||||||
|
|
||||||
|
oblist = scan_backgrounds()
|
||||||
|
# print('reload_bg_list > oblist:')
|
||||||
|
# pp(oblist)
|
||||||
|
warning = []
|
||||||
|
if not oblist:
|
||||||
|
return f'No BG found, need to be in collection "{BGCOL}" !'
|
||||||
|
|
||||||
|
## Clean
|
||||||
|
for ob in oblist:
|
||||||
|
bg_viewlayer = bpy.context.view_layer.layer_collection.children.get(BGCOL)
|
||||||
|
if not bg_viewlayer:
|
||||||
|
return "No 'Background' collection in scene master collection !"
|
||||||
|
|
||||||
|
ob_viewcol = bg_viewlayer.children.get(ob.name)
|
||||||
|
if not ob_viewcol:
|
||||||
|
warning.append(f'{ob.name} not found in "Background" collection')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if vl_col is hided or object is hided
|
||||||
|
# visible = any(o for o in ob.children if o.visible_get()) # ob.visible_get()
|
||||||
|
# for o in ob.children: o.hide_set(False)
|
||||||
|
|
||||||
|
ob_viewcol.hide_viewport = False
|
||||||
|
ob_viewcol.collection.hide_viewport = False
|
||||||
|
# ob_viewcol.exclude = not visible
|
||||||
|
|
||||||
|
## Driver check (don't erase if exists, else will delete persp mode drivers...)
|
||||||
|
if not ob.get('distance'):
|
||||||
|
if ob.animation_data:
|
||||||
|
for dr in ob.animation_data.drivers:
|
||||||
|
if dr.driver.variables.get('distance'):
|
||||||
|
# dist = ob.get('distance')
|
||||||
|
ob.animation_data.drivers.remove(dr)
|
||||||
|
add_loc_z_driver(ob)
|
||||||
|
|
||||||
|
uilist.clear()
|
||||||
|
|
||||||
|
## Populate the list
|
||||||
|
for ob in oblist: # populate list
|
||||||
|
item = uilist.add()
|
||||||
|
item.plane = ob
|
||||||
|
item.type = "bg"
|
||||||
|
scn.bg_props.index = len(uilist) - 1 # id (len of list in the ad loop)
|
||||||
|
if ob.children and ob.children[0].type == 'GPENCIL':
|
||||||
|
reset_gp_uv(ob.children[0]) # Force reset UV
|
||||||
|
|
||||||
|
## Add Grease pencil objects
|
||||||
|
gp_list = [o for o in bpy.context.scene.objects if o.type == 'GPENCIL' \
|
||||||
|
and not o.get('is_background') and o not in oblist]
|
||||||
|
for ob in gp_list:
|
||||||
|
item = uilist.add()
|
||||||
|
item.plane = ob
|
||||||
|
item.type = "obj"
|
||||||
|
scn.bg_props.index = len(uilist) - 1 # id (len of list in the ad loop)
|
||||||
|
|
||||||
|
print(f"{len(uilist)} loaded in {time.time()-start:.2f}")
|
||||||
|
return warning
|
||||||
|
|
||||||
|
|
||||||
|
def coord_distance_from_cam_straight(coord=None, context=None, camera=None):
|
||||||
|
context = context or bpy.context
|
||||||
|
coord = coord or context.scene.cursor.location
|
||||||
|
camera = camera or context.scene.camera
|
||||||
|
if not camera:
|
||||||
|
return
|
||||||
|
|
||||||
|
view_mat = camera.matrix_world
|
||||||
|
view_point = view_mat @ Vector((0, 0, -1000))
|
||||||
|
co = geometry.intersect_line_plane(view_mat.translation, view_point, coord, view_point)
|
||||||
|
if co is None:
|
||||||
|
return None
|
||||||
|
return (co - view_mat.translation).length
|
||||||
|
|
||||||
|
def coord_distance_from_cam(coord, context=None, camera=None):
|
||||||
|
context = context or bpy.context
|
||||||
|
coord = coord or context.scene.cursor.location
|
||||||
|
camera = camera or context.scene.camera
|
||||||
|
if not camera:
|
||||||
|
return
|
||||||
|
return (coord - camera.matrix_world.to_translation()).length
|
|
@ -0,0 +1,25 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from . import (manage_objects, manage_planes, import_planes, convert_planes)
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
manage_objects,
|
||||||
|
manage_planes,
|
||||||
|
import_planes,
|
||||||
|
convert_planes
|
||||||
|
)
|
||||||
|
|
||||||
|
# if 'bpy' in locals():
|
||||||
|
# import importlib
|
||||||
|
# for mod in modules:
|
||||||
|
# importlib.reload(mod)
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for mod in modules:
|
||||||
|
mod.register()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for mod in reversed(modules):
|
||||||
|
mod.unregister()
|
|
@ -0,0 +1,165 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
# import bpy_extras
|
||||||
|
# from bpy_extras.io_utils import ImportHelper
|
||||||
|
from pathlib import Path
|
||||||
|
from bpy.types import Operator
|
||||||
|
from .. import fn
|
||||||
|
from .. constants import BGCOL, MODULE_DIR
|
||||||
|
|
||||||
|
## Plane conversion between multiple types: GP plane / image plane / empty references
|
||||||
|
|
||||||
|
def convert_background_image(ob, target = 'GPENCIL'):
|
||||||
|
'''Target in ['GPENCIL', 'EMPTY', 'MESH']'''
|
||||||
|
|
||||||
|
if target == ob.type:
|
||||||
|
print('WARNING', f'{ob.name} Already of type {target}')
|
||||||
|
return
|
||||||
|
|
||||||
|
scn = bpy.context.scene
|
||||||
|
cam = scn.objects.get('bg_cam')
|
||||||
|
if not cam:
|
||||||
|
cam = ob.parent
|
||||||
|
if not cam:
|
||||||
|
cam = scn.camera
|
||||||
|
if not cam:
|
||||||
|
print('No "bg_cam", no parent, source object is not parented !')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get info from Source
|
||||||
|
img_infos = fn.get_image_infos_from_object(ob)
|
||||||
|
if not img_infos:
|
||||||
|
print(f'No could not retrieve image source from {ob}')
|
||||||
|
return
|
||||||
|
|
||||||
|
image_data, opacity = img_infos
|
||||||
|
|
||||||
|
|
||||||
|
### Create new plane type
|
||||||
|
|
||||||
|
## RENAME to avoid conflict (always keep same name for list scan ?)
|
||||||
|
## rename source temporarily
|
||||||
|
## write type in name ?
|
||||||
|
## name only holder (if there is a holder)
|
||||||
|
|
||||||
|
ob.name = f'{ob.name}_temp'
|
||||||
|
|
||||||
|
## TODO: ensure image filepath is ok ?
|
||||||
|
|
||||||
|
suffix = ''
|
||||||
|
if target == 'GPENCIL':
|
||||||
|
## Create GP plane from Texture source
|
||||||
|
new = fn.import_image_as_gp_reference(
|
||||||
|
bpy.context,
|
||||||
|
image_data,
|
||||||
|
pack_image=False,
|
||||||
|
)
|
||||||
|
suffix = '_texgp'
|
||||||
|
|
||||||
|
if target == 'MESH':
|
||||||
|
## HARDCODED text blend path:
|
||||||
|
# blend = '/s/blender/blender-2.9_scripts/addons/bg_plane_manager/texture_plane.blend'
|
||||||
|
blend = str(MODULE_DIR / 'texture_plane.blend')
|
||||||
|
node_group = fn.link_nodegroup(blend, 'texture_plane', link=False)
|
||||||
|
new = fn.create_image_plane(image_data, node_group)
|
||||||
|
suffix = '_texplane'
|
||||||
|
|
||||||
|
if target == 'EMPTY':
|
||||||
|
new = fn.create_empty_image(image_data)
|
||||||
|
suffix = '_texempty'
|
||||||
|
|
||||||
|
|
||||||
|
# Strip after '.' or get original name ?
|
||||||
|
print('source name: ', new.name)
|
||||||
|
new.name = fn.clean_image_name(image_data.name) + suffix
|
||||||
|
print('final name:', new.name)
|
||||||
|
new['is_background'] = True
|
||||||
|
|
||||||
|
## Without holder
|
||||||
|
## Transfer attributes
|
||||||
|
# fn.create_plane_driver(new, cam)
|
||||||
|
# new.parent = cam
|
||||||
|
|
||||||
|
# new['distance'] = ob['distance']
|
||||||
|
# new['scale'] = ob['scale']
|
||||||
|
|
||||||
|
|
||||||
|
# new.lock_location = ob.lock_location # lock only X,Y ?
|
||||||
|
# new.hide_select = ob.hide_select
|
||||||
|
|
||||||
|
# new.rotation_euler = ob.rotation_euler
|
||||||
|
|
||||||
|
## With holder
|
||||||
|
new.parent = ob.parent
|
||||||
|
|
||||||
|
|
||||||
|
## Loc/scale overrided by driver
|
||||||
|
# new.location = ob.location
|
||||||
|
# new.scale = ob.scale
|
||||||
|
|
||||||
|
## Transfer current transparency
|
||||||
|
|
||||||
|
# Destination
|
||||||
|
fn.set_opacity(new, opacity)
|
||||||
|
|
||||||
|
## Set in collection
|
||||||
|
if not len(ob.users_collection):
|
||||||
|
fn.set_collection(new, BGCOL)
|
||||||
|
else:
|
||||||
|
fn.set_collection(new, ob.users_collection[0].name)
|
||||||
|
|
||||||
|
## Remove old object
|
||||||
|
bpy.data.objects.remove(ob)
|
||||||
|
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_convert_planes(Operator):
|
||||||
|
bl_idname = "bpm.convert_planes"
|
||||||
|
bl_label = "Convert Planes"
|
||||||
|
bl_description = "Convert as plane all images inside a folder"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
target_type : bpy.props.EnumProperty(
|
||||||
|
name="Convert To", description="", default='GPENCIL', options={'ANIMATABLE'},
|
||||||
|
items=(
|
||||||
|
('GPENCIL', 'Gpencil Object', 'Convert bg planes to gpencil objects', 0),
|
||||||
|
('EMPTY', 'Empty Reference', 'Convert bg planes to empty objects', 1),
|
||||||
|
('MESH', 'Texture Plane', 'Convert bg planes to mesh objects', 2),
|
||||||
|
))
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||||
|
# return self.execute(context)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
print('BPM_OT_convert_planes')
|
||||||
|
holder_list = fn.scan_backgrounds(all_bg=True)
|
||||||
|
if not holder_list:
|
||||||
|
self.report({'ERROR'}, 'No Background found to convert, Structure must have a parent holder')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
for holder in holder_list:
|
||||||
|
if not holder.children:
|
||||||
|
print(f'{holder.name} has not child')
|
||||||
|
continue
|
||||||
|
ob = holder.children[0] # take first child FIXME: what if multiple childrens ?
|
||||||
|
new_bg = convert_background_image(ob, target=self.target_type)
|
||||||
|
|
||||||
|
|
||||||
|
# if isinstance(new_bg, str):
|
||||||
|
# self.report({'ERROR'}, new_bg)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
BPM_OT_convert_planes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,263 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
## Standalone based on SPA code
|
||||||
|
## Code from create_gp_texture_ref.py (Copyright (C) 2023, The SPA Studios. All rights reserved.)
|
||||||
|
## adapted to create as a single 'GP as image' and fit in camera with driver
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
from bpy.props import (StringProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
BoolProperty,
|
||||||
|
EnumProperty)
|
||||||
|
import json
|
||||||
|
import bpy_extras
|
||||||
|
from mathutils import Vector
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from .. import fn
|
||||||
|
from .. constants import *
|
||||||
|
from .. export_psd_layers import export_psd_bg
|
||||||
|
|
||||||
|
|
||||||
|
def get_json_infos(json_path) -> tuple((list, tuple)):
|
||||||
|
'''return a tuple with (image paths list, [x, y] original psd resolution'''
|
||||||
|
|
||||||
|
import json
|
||||||
|
setup_json_path = Path(json_path)
|
||||||
|
setup_json = json.loads(setup_json_path.read_text())
|
||||||
|
|
||||||
|
# psd = Path(setup_json['psd']) # path to psd
|
||||||
|
# image_size = setup_json['image_size'] # image size (reduced)
|
||||||
|
# png_dir = Path(setup_json['png_dir']) # path to png directory
|
||||||
|
layers = setup_json['layers'] # dic : name, lbbox, bbox, index, path
|
||||||
|
org_image_size = setup_json['org_image_size'] # original PSD dimensions
|
||||||
|
img_list = [Path(l['path']) for l in layers] # (png_dir /l['name']).with_suffix('.png')
|
||||||
|
|
||||||
|
return img_list, org_image_size
|
||||||
|
|
||||||
|
def setup_bg_camera(image_size, context=None, scn=None, cam_type=None):
|
||||||
|
'''Create background camera from scene active camera according to background image resolution
|
||||||
|
image_size ::
|
||||||
|
cam_type :: wether the BG camera should be set to perspective or orthographic
|
||||||
|
'''
|
||||||
|
context = context or bpy.context
|
||||||
|
scn=scn or context.scene
|
||||||
|
|
||||||
|
movie_resolution = scn.render.resolution_x, scn.render.resolution_y
|
||||||
|
anim_cam = scn.camera
|
||||||
|
anim_cam_scale = 6 # anim_cam.data.ortho_scale
|
||||||
|
anim_cam_focale = 50 # anim_cam.data.lens
|
||||||
|
cam_type = cam_type or anim_cam.data.type
|
||||||
|
|
||||||
|
## scale up plane scale and bg_cam ortho_scale according to film res
|
||||||
|
bg_factor = (anim_cam_scale * image_size[0]) / movie_resolution[0]
|
||||||
|
#print(f'({anim_cam_scale} * {image_size[0]}) / {movie_resolution[0]} = {bg_factor}')
|
||||||
|
|
||||||
|
## scale down the focal according to film res
|
||||||
|
bg_focale = (anim_cam_focale * movie_resolution[0]) / image_size[0]
|
||||||
|
#print(f'({anim_cam_focale} * {movie_resolution[0]}) / {image_size[0]} = {bg_focale}')
|
||||||
|
|
||||||
|
bg_cam = scn.objects.get('bg_cam')
|
||||||
|
if not bg_cam:
|
||||||
|
bg_cam = fn.create_cam(name='bg_cam', type=cam_type, size=bg_factor, focale=bg_focale)
|
||||||
|
bg_cam['resolution'] = image_size
|
||||||
|
|
||||||
|
bg_cam.matrix_world = anim_cam.matrix_world
|
||||||
|
|
||||||
|
bg_cam.hide_select = True
|
||||||
|
bg_cam.lock_location = bg_cam.lock_rotation = bg_cam.lock_scale = [True]*3
|
||||||
|
|
||||||
|
fn.set_collection(bg_cam, 'Camera')
|
||||||
|
fn.set_collection(anim_cam, 'Camera')
|
||||||
|
return bg_cam
|
||||||
|
|
||||||
|
class BPM_OT_import_bg_images(bpy.types.Operator, ImportHelper):
|
||||||
|
bl_idname = "bpm.import_bg_images"
|
||||||
|
bl_label = "Import Background images"
|
||||||
|
bl_description = "Import either a Json, a PSD, multiple files"
|
||||||
|
bl_options = {"REGISTER"} # , "INTERNAL"
|
||||||
|
|
||||||
|
# filename_ext = '.json'
|
||||||
|
|
||||||
|
filter_glob: StringProperty(
|
||||||
|
default=';'.join([f'*{i}' for i in bpy.path.extensions_image]) + ';*.json',
|
||||||
|
options={'HIDDEN'} )
|
||||||
|
|
||||||
|
## Active selection
|
||||||
|
filepath : StringProperty(
|
||||||
|
name="File Path",
|
||||||
|
description="File path used for import",
|
||||||
|
maxlen= 1024) # the active file
|
||||||
|
|
||||||
|
## Handle multi-selection
|
||||||
|
files: CollectionProperty(
|
||||||
|
name="File Path",
|
||||||
|
type=bpy.types.OperatorFileListElement,
|
||||||
|
) # The filelist collection
|
||||||
|
|
||||||
|
## Choice to place before or after ?
|
||||||
|
|
||||||
|
import_type : EnumProperty(
|
||||||
|
name="Import As", description="Type of import to ", default='GPENCIL', options={'ANIMATABLE'},
|
||||||
|
items=(
|
||||||
|
('GPENCIL', 'Gpencil Object', 'Import bg planes as gpencil objects', 0),
|
||||||
|
('EMPTY', 'Empty Reference', 'Import bg planes as empty objects', 1),
|
||||||
|
('MESH', 'Texture Plane', 'Import bg planes as mesh objects', 2),
|
||||||
|
))
|
||||||
|
|
||||||
|
mode : EnumProperty(
|
||||||
|
name="Mode", description="", default='REPLACE', options={'ANIMATABLE'},
|
||||||
|
items=(
|
||||||
|
('REPLACE', 'Replace Existing', 'Replace the image if already exists', 0),
|
||||||
|
('SKIP', 'Skip Existing', 'Skip the import if the image alreaady exists in planes', 1),
|
||||||
|
# ('PURGE', 'Purge', 'When object exists, fully delete it before linking, even associated collection and holder', 2),
|
||||||
|
))
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
org_image_size = None
|
||||||
|
scn = context.scene
|
||||||
|
active_file = Path(self.filepath)
|
||||||
|
|
||||||
|
if len(self.files) == 1 and active_file.suffix.lower() in ('.json', '.psd'):
|
||||||
|
json_path = None
|
||||||
|
print('active_file.suffix.lower(): ', active_file.suffix.lower())
|
||||||
|
if active_file.suffix.lower() == '.psd':
|
||||||
|
print('Is a PSD')
|
||||||
|
## Export layers and create json setup file
|
||||||
|
# Export passes in a 'render' or 'render_hd' folder aside psd
|
||||||
|
json_path = export_psd_bg(str(active_file))
|
||||||
|
|
||||||
|
elif active_file.suffix.lower() == '.json':
|
||||||
|
print('Is a json')
|
||||||
|
# Use json data to batch import
|
||||||
|
json_path = active_file
|
||||||
|
|
||||||
|
if not json_path:
|
||||||
|
self.report({'ERROR'}, 'No json path to load, you can try loading from psd or selecting image file directly')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
file_list, org_image_size = get_json_infos(json_path)
|
||||||
|
folder = Path(json_path).parent # render folder
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = active_file.parent
|
||||||
|
# Filter out json (we may want ot import the PSD as imagelisted in bpy.path.extensions_image)
|
||||||
|
file_list = [folder / f.name for f in self.files if Path(f.name).suffix in bpy.path.extensions_image]
|
||||||
|
|
||||||
|
if not file_list:
|
||||||
|
self.report({'ERROR'}, 'Image file list is empty')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
|
||||||
|
## simple_list
|
||||||
|
# existing_backgrounds = [o for o in context.scene.objects if o.get('is_background')]
|
||||||
|
# existing_holders = [o for o in context.scene.objects if o.name.startswith(PREFIX) and o.type == 'MESH' and o.children]
|
||||||
|
|
||||||
|
## Has dict
|
||||||
|
# existing_backgrounds = {o : img_info for o in context.scene.objects if o.get('is_background') and (img_info := o.get('is_background'))}
|
||||||
|
|
||||||
|
# FIXME: Use existing background custom prop? : o.get('is_background')
|
||||||
|
## list of Tuples : [(plane_object, img_data, transparency_value), ...]
|
||||||
|
|
||||||
|
existing_backgrounds = [(o, *img_info) for o in context.scene.objects \
|
||||||
|
if o.get('is_background') and (img_info := fn.get_image_infos_from_object(o)) and o.get('is_background') and o.parent]
|
||||||
|
|
||||||
|
existing_backgrounds.sort(key=lambda x: x[0].parent.location.z, reverse=True) # sort by parent (holder) location Z
|
||||||
|
|
||||||
|
far_plane = INIT_POS
|
||||||
|
if existing_backgrounds:
|
||||||
|
far_plane = existing_backgrounds[-1][0].parent.location.z
|
||||||
|
|
||||||
|
## Ensure bg_cam setup (option to use active camera ?)
|
||||||
|
bg_cam = scn.objects.get('bg_cam')
|
||||||
|
if not bg_cam:
|
||||||
|
if not org_image_size:
|
||||||
|
## Get image size from first file
|
||||||
|
img = bpy.data.images.load(file_list[0], check_existing=True)
|
||||||
|
org_image_size = (img.size[0], img.size[1])
|
||||||
|
bg_cam = setup_bg_camera(
|
||||||
|
org_image_size, context=context, scn=scn, cam_type=scn.camera.data.type)
|
||||||
|
|
||||||
|
## Ensure Background collection
|
||||||
|
backgrounds = fn.get_col(BGCOL) # Create if needed
|
||||||
|
|
||||||
|
for fp in file_list:
|
||||||
|
print(f'Importing {fp.name}')
|
||||||
|
current_holder = None
|
||||||
|
|
||||||
|
file_stem = Path(fp).stem
|
||||||
|
# current_bg = next((o for o if o.parent and fn.get_image_infos_from_object(o)), None)
|
||||||
|
current_bg = next((o for o in existing_backgrounds if o[1].filepath == fp), None)
|
||||||
|
if current_bg:
|
||||||
|
current_holder = current_bg.parent
|
||||||
|
# TODO store opacity or delete existing objects only after loop
|
||||||
|
|
||||||
|
if current_bg and self.mode == 'SKIP':
|
||||||
|
print(f' - SKIP: existing {current_bg.name}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_bg and self.mode == 'REPLACE':
|
||||||
|
print(f' - DEL: existing {current_bg.name}')
|
||||||
|
bpy.data.objects.remove(current_bg)
|
||||||
|
|
||||||
|
# Import image
|
||||||
|
if self.import_type == 'GPENCIL':
|
||||||
|
bg_img = fn.import_image_as_gp_reference(
|
||||||
|
context=context,
|
||||||
|
image=fp,
|
||||||
|
pack_image=False,
|
||||||
|
)
|
||||||
|
elif self.import_type == 'MESH':
|
||||||
|
bg_img = fn.create_image_plane(fp)
|
||||||
|
|
||||||
|
elif self.import_type == 'EMPTY':
|
||||||
|
bg_img = fn.create_empty_image(fp)
|
||||||
|
|
||||||
|
|
||||||
|
bg_img.name = file_stem + '_texgp'
|
||||||
|
bg_img.hide_select = True
|
||||||
|
|
||||||
|
if current_holder:
|
||||||
|
bg_img.parent = current_holder
|
||||||
|
fn.set_collection(bg_img, current_holder.users_collection[0].name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# img, opacity = fn.get_image_infos_from_object(bg_img)
|
||||||
|
print('bg_img: ', bg_img)
|
||||||
|
img = fn.get_image(bg_img)
|
||||||
|
# bg_name = fn.clean_image_name(img.name)
|
||||||
|
file_stem = file_stem
|
||||||
|
|
||||||
|
# Get create collection from image clean name
|
||||||
|
bg_col = fn.get_col(file_stem, parent=backgrounds)
|
||||||
|
|
||||||
|
## Set in collection
|
||||||
|
bg_col.objects.link(bg_img)
|
||||||
|
|
||||||
|
print('img: ', img.name, bg_img.name, bg_img.users_collection)
|
||||||
|
|
||||||
|
## create the holder, parent to camera, set driver and set collection. Could also pass 'fp'
|
||||||
|
holder = fn.create_plane_holder(img, name=PREFIX + file_stem, parent=bg_cam, link_in_col=bg_col)
|
||||||
|
print('holder: ', holder.name)
|
||||||
|
|
||||||
|
fn.create_plane_driver(holder, bg_cam, distance=far_plane)
|
||||||
|
bg_img.parent = holder
|
||||||
|
|
||||||
|
far_plane += 2
|
||||||
|
|
||||||
|
fn.reload_bg_list(scene=scn)
|
||||||
|
self.report({'INFO'}, f'Settings loaded from: {os.path.basename(self.filepath)}')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
BPM_OT_import_bg_images,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,397 @@
|
||||||
|
import re
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
from bpy.types import Operator
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
from .. import fn
|
||||||
|
|
||||||
|
|
||||||
|
def set_resolution_from_cam_prop(cam=None):
|
||||||
|
if not cam:
|
||||||
|
cam = bpy.context.scene.camera
|
||||||
|
if not cam:
|
||||||
|
return ('ERROR', 'No active camera')
|
||||||
|
|
||||||
|
res = cam.get('resolution')
|
||||||
|
if not res:
|
||||||
|
return ('ERROR', 'Cam has no resolution attribute')
|
||||||
|
|
||||||
|
rd = bpy.context.scene.render
|
||||||
|
if rd.resolution_x == res[0] and rd.resolution_y == res[1]:
|
||||||
|
return ('INFO', f'Resolution already at {res[0]}x{res[1]}')
|
||||||
|
else:
|
||||||
|
rd.resolution_x, rd.resolution_y = res[0], res[1]
|
||||||
|
return ('INFO', f'Resolution to {res[0]}x{res[1]}')
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_swap_cams(Operator):
|
||||||
|
bl_idname = "bpm.swap_cams"
|
||||||
|
bl_label = "Swap Cameras"
|
||||||
|
bl_description = "Toggle between anim and bg cam"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
anim_cam = bpy.context.scene.objects.get('anim_cam')
|
||||||
|
bg_cam = bpy.context.scene.objects.get('bg_cam')
|
||||||
|
|
||||||
|
if not anim_cam or not bg_cam:
|
||||||
|
self.report({'ERROR'}, 'anim_cam or bg_cam is missing')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
cam = context.scene.camera
|
||||||
|
if not cam:
|
||||||
|
context.scene.camera = anim_cam
|
||||||
|
set_resolution_from_cam_prop()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
in_draw = False
|
||||||
|
if cam.parent and cam.name in ('draw_cam', 'action_cam'):
|
||||||
|
if cam.name == 'draw_cam':
|
||||||
|
draw_cam = cam
|
||||||
|
in_draw = True
|
||||||
|
cam = cam.parent
|
||||||
|
|
||||||
|
## swap
|
||||||
|
# context.scene.camera = bg_cam if cam is anim_cam else anim_cam
|
||||||
|
if cam is anim_cam:
|
||||||
|
main = context.scene.camera = bg_cam
|
||||||
|
anim_cam.hide_viewport = True
|
||||||
|
bg_cam.hide_viewport = False
|
||||||
|
else:
|
||||||
|
main = context.scene.camera = anim_cam
|
||||||
|
anim_cam.hide_viewport = False
|
||||||
|
bg_cam.hide_viewport = True
|
||||||
|
|
||||||
|
if in_draw:
|
||||||
|
draw_cam.parent = main
|
||||||
|
draw_cam.data = main.data
|
||||||
|
# back in draw_cam
|
||||||
|
context.scene.camera = draw_cam
|
||||||
|
bg_cam.hide_viewport = anim_cam.hide_viewport = True
|
||||||
|
|
||||||
|
# set res
|
||||||
|
ret = set_resolution_from_cam_prop(main)
|
||||||
|
if ret:
|
||||||
|
self.report({ret[0]}, ret[1])
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_send_gp_to_plane(Operator):
|
||||||
|
bl_idname = "bpm.send_gp_to_plane"
|
||||||
|
bl_label = "Send To Plane"
|
||||||
|
bl_description = "Send the selected GPs to current active layer, adjusting scale to keep size in camera"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type != 'CAMERA'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
offset = 0.005 # 0.001
|
||||||
|
ob = context.object
|
||||||
|
|
||||||
|
cam = context.scene.camera
|
||||||
|
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
plane = settings.planes[settings.index].plane
|
||||||
|
plane_mat = plane.matrix_world
|
||||||
|
|
||||||
|
plane_co = plane_mat.translation
|
||||||
|
plane_no = Vector((0,0,1))
|
||||||
|
plane_no.rotate(plane_mat)
|
||||||
|
|
||||||
|
cam_co = cam.matrix_world.translation
|
||||||
|
ob_co = ob.matrix_world.translation
|
||||||
|
|
||||||
|
if cam.data.type == 'ORTHO':
|
||||||
|
forward = Vector((0,0,-1))
|
||||||
|
backward = Vector((0,0,1))
|
||||||
|
forward.rotate(cam.matrix_world)
|
||||||
|
backward.rotate(cam.matrix_world)
|
||||||
|
new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+forward, plane_co, plane_no)
|
||||||
|
if not new_co:
|
||||||
|
new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+backward, plane_co, plane_no)
|
||||||
|
if not new_co:
|
||||||
|
self.report({'ERROR'}, 'Could not hit background surface by tracing looking in cam direction from obj\nCheck if BG plane is parallel to camera view')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
new_vec = new_co - ob_co
|
||||||
|
new_vec += backward * offset
|
||||||
|
ob.matrix_world.translation += new_vec
|
||||||
|
self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_vec.length:.3f}m)')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# PERSP mode
|
||||||
|
init_dist = (ob_co - cam_co).length
|
||||||
|
new_co = mathutils.geometry.intersect_line_plane(cam_co, ob_co, plane_co, plane_no)
|
||||||
|
print('new_co: ', new_co)
|
||||||
|
if not new_co:
|
||||||
|
self.report({'ERROR'}, 'Grease pencil object might be behind camera\nCould not hit background surface by tracing from cam to BG')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
new_vec = new_co - cam_co
|
||||||
|
new_vec -= new_vec.normalized() * offset # substract offset from cam to ob vector
|
||||||
|
new_dist = new_vec.length # check distance after offset applied for right scaling
|
||||||
|
dist_percentage = new_dist / init_dist
|
||||||
|
ob.matrix_world.translation = cam_co + new_vec # replace from cam to ob
|
||||||
|
|
||||||
|
ob.scale = ob.matrix_world.to_scale() * dist_percentage # adjust scale
|
||||||
|
self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_dist:.3f}m)')
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_parent_to_bg(Operator):
|
||||||
|
bl_idname = "bpm.parent_to_bg"
|
||||||
|
bl_label = "Parent To Selected Background"
|
||||||
|
bl_description = "Parent selected active object to active Background in list"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object# and context.object.type != 'CAMERA'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
plane = settings.planes[settings.index].plane
|
||||||
|
plane_list = [i.plane for i in settings.planes]
|
||||||
|
# plane_mat = plane.matrix_world
|
||||||
|
o = bpy.context.object
|
||||||
|
|
||||||
|
if o in plane_list:
|
||||||
|
self.report({'ERROR'}, 'Selected object must not be a plane')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
mat = o.matrix_world.copy()
|
||||||
|
if o.parent:
|
||||||
|
parent = o.parent
|
||||||
|
o.parent = None
|
||||||
|
self.report({'INFO'}, f'Object "{o.name}" unparented from {parent.name}')
|
||||||
|
else:
|
||||||
|
o.parent = plane
|
||||||
|
self.report({'INFO'}, f'Object "{o.name}" parented to {plane.name}')
|
||||||
|
|
||||||
|
o.matrix_world = mat
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO make the align to plane orientation (change object rotation without affecting points)
|
||||||
|
# need to make a loop with frame_set on each frame (and change only relevant layers...)
|
||||||
|
|
||||||
|
class BPM_OT_align_to_plane(Operator):
|
||||||
|
bl_idname = "bpm.align_to_plane"
|
||||||
|
bl_label = "Align to GP plane"
|
||||||
|
bl_description = "Align the current GP object to plane\n(change object orientation while keeping points in place)"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
plane = settings.planes[settings.index].plane
|
||||||
|
plane_mat = plane.matrix_world
|
||||||
|
|
||||||
|
o = bpy.context.object
|
||||||
|
old_mat = o.matrix_world.copy()
|
||||||
|
|
||||||
|
# reset or align to needed plane
|
||||||
|
|
||||||
|
# decompose old matrix
|
||||||
|
loc, rot, scale = old_mat.decompose()
|
||||||
|
|
||||||
|
matloc = Matrix.Translation(loc)
|
||||||
|
|
||||||
|
#matrot = Matrix()
|
||||||
|
matrot = rot.to_matrix().to_4x4()
|
||||||
|
|
||||||
|
# recreate a neutral mat scale
|
||||||
|
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
||||||
|
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
||||||
|
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
||||||
|
matscale = matscale_x @ matscale_y @ matscale_z
|
||||||
|
|
||||||
|
#C.object.rotation_euler = (0,0,0)
|
||||||
|
|
||||||
|
# mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||||
|
|
||||||
|
print("old_mat", old_mat)#Dbg
|
||||||
|
#new_mat = o.matrix_world.copy()
|
||||||
|
|
||||||
|
new_mat = matloc @ matscale
|
||||||
|
context.object.matrix_world = new_mat
|
||||||
|
|
||||||
|
|
||||||
|
for l in o.data.layers:
|
||||||
|
for f in l.frames:
|
||||||
|
for s in f.strokes:
|
||||||
|
for p in s.points:
|
||||||
|
p.co = matrot @ p.co
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def place_object_from_facing_cam(ob=None, cam=None, distance=8):
|
||||||
|
ob = ob or bpy.context.object
|
||||||
|
cam = cam or bpy.context.scene.camera
|
||||||
|
if not cam:
|
||||||
|
return
|
||||||
|
scale = ob.matrix_world.to_scale()
|
||||||
|
mat_scale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
||||||
|
mat_scale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
||||||
|
mat_scale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
||||||
|
mat_scale = mat_scale_x @ mat_scale_y @ mat_scale_z
|
||||||
|
|
||||||
|
mat = cam.matrix_world.copy()
|
||||||
|
cam_mat_inv = mat.inverted()
|
||||||
|
|
||||||
|
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
||||||
|
|
||||||
|
# Offset in object local Y
|
||||||
|
mat.translation -= Vector((0, 0, distance)) @ cam_mat_inv
|
||||||
|
mat = mat @ mat_90 @ mat_scale
|
||||||
|
ob.matrix_world = mat
|
||||||
|
|
||||||
|
class BPM_OT_create_and_place_in_camera(Operator):
|
||||||
|
bl_idname = "bpm.create_and_place_in_camera"
|
||||||
|
bl_label = "Create and Place Gpencil In Camera"
|
||||||
|
bl_description = "Create GP object\
|
||||||
|
\nCentered and Rotated so X-Z front axis is facing cam\
|
||||||
|
\nCtrl + Click to place selected object intead of creating"
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if not context.scene.camera:
|
||||||
|
cls.poll_message_set("Need a scene camera, object is created facing cam")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
create : bpy.props.BoolProperty(name='Create', default=False, options={'SKIP_SAVE'})
|
||||||
|
name : bpy.props.StringProperty(name='Name', default='', options={'SKIP_SAVE'})
|
||||||
|
distance : bpy.props.FloatProperty(name='Distance', default=8, subtype='DISTANCE')
|
||||||
|
use_light : bpy.props.BoolProperty(name='Use Light', default=False, options={'SKIP_SAVE'})
|
||||||
|
edit_line_opacity : bpy.props.FloatProperty(name='Edit Line Opacity',
|
||||||
|
description="Edit line opacity for newly created objects\
|
||||||
|
\nAdvanced users generally like it at 0 (show only selected line in edit mode)\
|
||||||
|
\nBlender default is 0.5",
|
||||||
|
default=0.0, min=0.0, max=1.0)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if event.ctrl:
|
||||||
|
self.create = False
|
||||||
|
|
||||||
|
## Set placeholder name (Comment to let an empty string)
|
||||||
|
self.name = fn.placeholder_name(self.name, context)
|
||||||
|
prefs = fn.get_addon_prefs()
|
||||||
|
self.use_light = prefs.use_light
|
||||||
|
self.edit_line_opacity = prefs.edit_line_opacity
|
||||||
|
|
||||||
|
if not bpy.context.scene.objects.get('bg_cam'):
|
||||||
|
self.report({'ERROR'}, 'No bg_cam')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
## match current plane distance
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
|
||||||
|
if settings.planes and settings.planes[settings.index].plane:
|
||||||
|
plane = settings.planes[settings.index].plane
|
||||||
|
self.distance = fn.coord_distance_from_cam_straight(plane.matrix_world.to_translation()) - 0.005
|
||||||
|
else:
|
||||||
|
self.distance = fn.coord_distance_from_cam_straight(context.scene.cursor.location)
|
||||||
|
self.distance = max([1.0, self.distance]) # minimum one meter away from cam
|
||||||
|
|
||||||
|
if self.create:
|
||||||
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||||
|
else:
|
||||||
|
if not context.object:
|
||||||
|
self.report({'ERROR'}, 'No active object')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.prop(self, 'name', icon='OUTLINER_OB_GREASEPENCIL')
|
||||||
|
layout.prop(self, 'distance', icon='DRIVER_DISTANCE')
|
||||||
|
layout.separator()
|
||||||
|
layout.prop(self, 'use_light')
|
||||||
|
layout.prop(self, 'edit_line_opacity')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if self.create:
|
||||||
|
ob_name = fn.placeholder_name(self.name, context)
|
||||||
|
|
||||||
|
## Create Object
|
||||||
|
prefs = fn.get_addon_prefs()
|
||||||
|
gp_data = bpy.data.grease_pencils.new(ob_name)
|
||||||
|
ob = bpy.data.objects.new(ob_name, gp_data)
|
||||||
|
ob.use_grease_pencil_lights = prefs.use_light
|
||||||
|
gp_data.edit_line_color[3] = prefs.edit_line_opacity
|
||||||
|
l = gp_data.layers.new('GP_Layer')
|
||||||
|
l.frames.new(context.scene.frame_current)
|
||||||
|
|
||||||
|
fn.set_collection(ob, 'GP') # Gpencils
|
||||||
|
|
||||||
|
# Add to bg_plane collection
|
||||||
|
new_item = context.scene.bg_props.planes.add()
|
||||||
|
new_item.plane = ob
|
||||||
|
new_item.type = 'obj'
|
||||||
|
|
||||||
|
# Set active on last
|
||||||
|
context.scene.bg_props.index = len(context.scene.bg_props.planes) - 1
|
||||||
|
fn.gp_transfer_mode(ob)
|
||||||
|
|
||||||
|
loaded_palette = False
|
||||||
|
if hasattr(bpy.types, "GPTB_OT_load_default_palette"):
|
||||||
|
res = bpy.ops.gp.load_default_palette()
|
||||||
|
if res == {"FINISHED"}:
|
||||||
|
loaded_palette = True
|
||||||
|
|
||||||
|
if not loaded_palette:
|
||||||
|
# Append at least line material
|
||||||
|
mat = bpy.data.materials.get('line')
|
||||||
|
if not mat:
|
||||||
|
## Create basic GP mat
|
||||||
|
mat = bpy.data.materials.new(name='line')
|
||||||
|
bpy.data.materials.create_gpencil_data(mat)
|
||||||
|
gp_data.materials.append(mat)
|
||||||
|
|
||||||
|
else:
|
||||||
|
ob = context.object
|
||||||
|
|
||||||
|
## Place in centered and front facing camera at given distance
|
||||||
|
cam = context.scene.camera
|
||||||
|
place_object_from_facing_cam(ob, cam, self.distance)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
## Scene
|
||||||
|
BPM_OT_swap_cams,
|
||||||
|
|
||||||
|
## GP related
|
||||||
|
BPM_OT_send_gp_to_plane,
|
||||||
|
BPM_OT_parent_to_bg,
|
||||||
|
BPM_OT_create_and_place_in_camera,
|
||||||
|
# BPM_OT_align_to_plane # << TODO
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,319 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from mathutils import Vector
|
||||||
|
from os.path import abspath
|
||||||
|
|
||||||
|
from .. import fn
|
||||||
|
from .. constants import BGCOL
|
||||||
|
|
||||||
|
## Open image folder
|
||||||
|
## Open change material alpha mode when using texture
|
||||||
|
## Selection ops (select toggle / all)
|
||||||
|
## Set distance modal
|
||||||
|
## Move plane by index
|
||||||
|
## Rebuild collection (Scan Background collection)
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_open_bg_folder(Operator):
|
||||||
|
bl_idname = "bpm.open_bg_folder"
|
||||||
|
bl_label = "Open bg folder"
|
||||||
|
bl_description = "Open folder of active element"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if context.scene.bg_props.planes and context.scene.bg_props.planes[context.scene.bg_props.index].type == 'bg':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
cls.poll_message_set("Only available when a background image planes is selected")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
item = context.scene.bg_props.planes[context.scene.bg_props.index]
|
||||||
|
ob = item.plane
|
||||||
|
# name = f'{item.plane.name}_texempty'
|
||||||
|
tex_obj = next((o for o in ob.children), None)
|
||||||
|
if not tex_obj:
|
||||||
|
self.report({'ERROR'}, f'Could not found child for holder: {ob.name}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
img = fn.get_image(tex_obj)
|
||||||
|
fp = Path(abspath(bpy.path.abspath(img.filepath)))
|
||||||
|
if not fp.exists():
|
||||||
|
self.report({'ERROR'}, f'Not found: {fp}')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bpy.ops.wm.path_open(filepath=str(fp.parent))
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class BPM_OT_change_material_alpha_mode(Operator):
|
||||||
|
bl_idname = "bpm.change_material_alpha_mode"
|
||||||
|
bl_label = "Swap Material Aplha"
|
||||||
|
bl_description = "Swap alpha : blend <-> Clip"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
print('----')
|
||||||
|
# maybe add a shift to act on selection only
|
||||||
|
on_selection = False
|
||||||
|
ct = 0
|
||||||
|
mode = None
|
||||||
|
for col in bpy.context.scene.collection.children:
|
||||||
|
if col.name != BGCOL:
|
||||||
|
continue
|
||||||
|
for bgcol in col.children:
|
||||||
|
for o in bgcol.all_objects:
|
||||||
|
if o.type == 'MESH' and len(o.data.materials):
|
||||||
|
|
||||||
|
if on_selection and o.parent and not o.parent.select_get():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mode is None:
|
||||||
|
mode = 'BLEND' if o.data.materials[0].blend_method == 'CLIP' else 'CLIP'
|
||||||
|
print(o.name, '>>', mode)
|
||||||
|
ct += 1
|
||||||
|
o.data.materials[0].blend_method = mode
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'{ct} swapped to alpha {mode}')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_OT_select_swap(Operator):
|
||||||
|
bl_idname = "bpm.select_swap_active_bg"
|
||||||
|
bl_label = "Select / Deselect"
|
||||||
|
bl_description = "Select/Deselect Active Background"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
plane = settings.planes[settings.index].plane
|
||||||
|
if not plane:
|
||||||
|
self.report({'ERROR'}, 'could not found current plane\ntry a refresh')
|
||||||
|
return{'CANCELLED'}
|
||||||
|
plane.select_set(not plane.select_get())
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class BPM_OT_select_all(Operator):
|
||||||
|
bl_idname = "bpm.select_all"
|
||||||
|
bl_label = "Select All Background"
|
||||||
|
bl_description = "Select all Background"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
bg_col = bpy.data.collections.get('Background')
|
||||||
|
if bg_col:
|
||||||
|
bg_col.hide_select = False
|
||||||
|
settings = context.scene.bg_props
|
||||||
|
for p in settings.planes:
|
||||||
|
if p and p.type == 'bg':
|
||||||
|
if p.plane.visible_get():
|
||||||
|
p.plane.select_set(True)
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# class BPM_OT_frame_backgrounds(Operator):
|
||||||
|
# bl_idname = "bpm.frame_backgrounds"
|
||||||
|
# bl_label = "Frame Backgrounds"
|
||||||
|
# bl_description = "Get out of camera and frame backgrounds from a quarter point or view"
|
||||||
|
# bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def poll(cls, context):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# def execute(self, context):
|
||||||
|
# if context.space_data.region_3d.view_perspective != 'CAMERA':
|
||||||
|
# context.space_data.region_3d.view_perspective = 'CAMERA'
|
||||||
|
# return {"FINISHED"}
|
||||||
|
|
||||||
|
# ## go out of camera, frame all bg in view at wauter angle
|
||||||
|
# settings = context.scene.bg_props
|
||||||
|
|
||||||
|
# visible_planes = [p.plane for p in settings.planes if p and p.type == 'bg' and p.plane.visible_get()]
|
||||||
|
# # TODO get out of camera and view frame planes
|
||||||
|
# context.space_data.region_3d.view_perspective = 'PERSPECTIVE'
|
||||||
|
# return {"FINISHED"}
|
||||||
|
|
||||||
|
class BPM_OT_set_distance(Operator):
|
||||||
|
bl_idname = "bpm.set_distance"
|
||||||
|
bl_label = "Distance"
|
||||||
|
bl_description = "Set distance of the active plane object\
|
||||||
|
\nShift move all further plane\
|
||||||
|
\nCtrl move all closer planes\
|
||||||
|
\nAlt move All"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.init_mouse_x = event.mouse_x
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
self.active = context.scene.bg_props.planes[context.scene.bg_props.index].plane
|
||||||
|
|
||||||
|
if context.scene.bg_props.move_hided:
|
||||||
|
self.plane_list = [i.plane for i in context.scene.bg_props.planes if i.type == 'bg']
|
||||||
|
else:
|
||||||
|
# o.visible_get() << if not get_view_layer_col(o.users_collection[0]).exclude
|
||||||
|
self.plane_list = [i.plane for i in context.scene.bg_props.planes if i.type == 'bg' and i.plane.visible_get()]
|
||||||
|
|
||||||
|
if not self.plane_list:
|
||||||
|
self.report({'ERROR'}, f'No plane found in list with current filter')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
self.plane_list.sort(key=lambda o: o.get('distance'), reverse=True)
|
||||||
|
self.active_index = self.plane_list.index(self.active)
|
||||||
|
self.offset = 0.0
|
||||||
|
self.further = self.plane_list[:self.active_index+1] # need +1 to include active
|
||||||
|
self.closer = self.plane_list[self.active_index:]
|
||||||
|
|
||||||
|
self.init_dist = [o.get('distance') for o in self.plane_list]
|
||||||
|
|
||||||
|
# context.area.header_text_set(f'{self.mode}')
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
|
||||||
|
context.area.header_text_set(f'Shift : move active and all further planes |\
|
||||||
|
Ctrl : move active and all closer planes | Shift + Ctrl :move all| Offset : {self.offset:.3f}m')
|
||||||
|
|
||||||
|
if event.type in {'LEFTMOUSE'} and event.value == 'PRESS':
|
||||||
|
# VALID
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
if event.type in {'ESC', 'RIGHTMOUSE'} and event.value == 'PRESS':
|
||||||
|
# CANCEL
|
||||||
|
context.area.header_text_set(None)
|
||||||
|
for plane, init_dist in zip(self.plane_list, self.init_dist):
|
||||||
|
plane['distance'] = init_dist
|
||||||
|
plane.location = plane.location
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
if event.type in {'MOUSEMOVE'}:
|
||||||
|
self.mouse = Vector((event.mouse_x, event.mouse_y))
|
||||||
|
self.offset = (event.mouse_x - self.init_mouse_x) * 0.1
|
||||||
|
|
||||||
|
for plane, init_dist in zip(self.plane_list, self.init_dist):
|
||||||
|
# reset to init distance dist
|
||||||
|
plane['distance'] = init_dist
|
||||||
|
|
||||||
|
if event.ctrl and event.shift or event.alt:
|
||||||
|
# all
|
||||||
|
plane['distance'] = init_dist + self.offset
|
||||||
|
|
||||||
|
elif event.shift:
|
||||||
|
if plane in self.further:
|
||||||
|
plane['distance'] = init_dist + self.offset
|
||||||
|
elif event.ctrl:
|
||||||
|
if plane in self.closer:
|
||||||
|
plane['distance'] = init_dist + self.offset
|
||||||
|
else:
|
||||||
|
if plane == self.active:
|
||||||
|
plane['distance'] = init_dist + self.offset
|
||||||
|
|
||||||
|
## needed esle no update...
|
||||||
|
plane.location = plane.location
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
class BPM_OT_move_plane(Operator):
|
||||||
|
bl_idname = "bpm.move_plane"
|
||||||
|
bl_label = "Move Plane"
|
||||||
|
bl_description = "Move active plane up or down"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
direction : bpy.props.StringProperty()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
props = context.scene.bg_props
|
||||||
|
return props \
|
||||||
|
and props.planes \
|
||||||
|
and props.index >= 0 \
|
||||||
|
and props.index < len(props.planes) \
|
||||||
|
and props.planes[props.index].type == 'bg'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
props = context.scene.bg_props
|
||||||
|
planes = props.planes
|
||||||
|
plane_ob = planes[props.index].plane
|
||||||
|
|
||||||
|
plane_objects = sorted([p.plane for p in planes if p.type == 'bg'], key=lambda x :x['distance'])
|
||||||
|
index = plane_objects.index(plane_ob)
|
||||||
|
|
||||||
|
if self.direction == 'UP':
|
||||||
|
if index == 0:
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
other_plane = plane_objects[index - 1]
|
||||||
|
|
||||||
|
elif self.direction == 'DOWN':
|
||||||
|
# If index == len(planes)-1: # Invalid when there are GP as well
|
||||||
|
if index == len(planes) - 1 or planes[index + 1].type != 'bg':
|
||||||
|
# End of list or avoid going below a GP object item.
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
other_plane = plane_objects[index + 1]
|
||||||
|
|
||||||
|
other_plane['distance'], plane_ob['distance'] = plane_ob['distance'], other_plane['distance']
|
||||||
|
|
||||||
|
plane_ob.location = plane_ob.location
|
||||||
|
other_plane.location = other_plane.location
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
class BPM_OT_reload(Operator):
|
||||||
|
bl_idname = "bpm.reload_list"
|
||||||
|
bl_label = "Refresh Bg List"
|
||||||
|
bl_description = "Refresh the background list (scan for objects using a distance prop)"
|
||||||
|
bl_options = {"REGISTER"} # , "UNDO"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
error = fn.reload_bg_list()
|
||||||
|
if error:
|
||||||
|
if isinstance(error, list):
|
||||||
|
self.report({'WARNING'}, 'Wrong name for some object, see console:' + '\n'.join(error))
|
||||||
|
else:
|
||||||
|
self.report({'ERROR'}, error)
|
||||||
|
return{'CANCELLED'}
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
# BPM_OT_change_background_type,
|
||||||
|
BPM_OT_select_swap,
|
||||||
|
BPM_OT_change_material_alpha_mode,
|
||||||
|
BPM_OT_select_all,
|
||||||
|
BPM_OT_reload,
|
||||||
|
BPM_OT_open_bg_folder,
|
||||||
|
BPM_OT_set_distance,
|
||||||
|
BPM_OT_move_plane
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
|
@ -0,0 +1,66 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import which
|
||||||
|
from bpy.props import (FloatProperty,
|
||||||
|
BoolProperty,
|
||||||
|
EnumProperty,
|
||||||
|
StringProperty,
|
||||||
|
IntProperty,
|
||||||
|
PointerProperty)
|
||||||
|
|
||||||
|
|
||||||
|
def ui_in_sidebar_update(self, _):
|
||||||
|
from .ui.panels import BPM_PT_bg_manager_panel
|
||||||
|
|
||||||
|
if hasattr(bpy.types, BPM_PT_bg_manager_panel.bl_idname):
|
||||||
|
try:
|
||||||
|
bpy.utils.unregister_class(BPM_PT_bg_manager_panel)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
BPM_PT_bg_manager_panel.bl_category = self.category.strip()
|
||||||
|
bpy.utils.register_class(BPM_PT_bg_manager_panel)
|
||||||
|
|
||||||
|
class BPM_prefs(bpy.types.AddonPreferences):
|
||||||
|
bl_idname = __name__.split('.')[0] # or __package__
|
||||||
|
|
||||||
|
category : StringProperty(
|
||||||
|
name="Category",
|
||||||
|
description="Choose a name for the sidebar category tab",
|
||||||
|
default="View",
|
||||||
|
update=ui_in_sidebar_update)
|
||||||
|
|
||||||
|
## Object settings
|
||||||
|
edit_line_opacity : FloatProperty(
|
||||||
|
name='Default Edit Line Opacity',
|
||||||
|
description="Edit line opacity for newly created objects\
|
||||||
|
\nAdvanced users generally like it at 0 (show only selected line in edit mode)\
|
||||||
|
\nBlender default is 0.5",
|
||||||
|
default=0.0, min=0.0, max=1.0)
|
||||||
|
|
||||||
|
use_light : BoolProperty(
|
||||||
|
name='Use light',
|
||||||
|
description="Use light or not for newly created objects",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
|
||||||
|
layout.prop(self, 'category')
|
||||||
|
|
||||||
|
layout.label(text='Object Settings:', icon='GREASEPENCIL')
|
||||||
|
layout.label(text='Properties of newly created Grease pencil object:')
|
||||||
|
layout.prop(self, 'edit_line_opacity')
|
||||||
|
layout.prop(self, 'use_light')
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(BPM_prefs)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(BPM_prefs)
|
Binary file not shown.
|
@ -0,0 +1,23 @@
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from . import (ui_list, panels)
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
ui_list,
|
||||||
|
panels
|
||||||
|
)
|
||||||
|
|
||||||
|
# if 'bpy' in locals():
|
||||||
|
# import importlib
|
||||||
|
# for mod in modules:
|
||||||
|
# importlib.reload(mod)
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for mod in modules:
|
||||||
|
mod.register()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for mod in reversed(modules):
|
||||||
|
mod.unregister()
|
|
@ -0,0 +1,172 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Panel
|
||||||
|
from .. constants import BGCOL, PREFIX
|
||||||
|
|
||||||
|
class BPM_PT_bg_manager_panel(Panel):
|
||||||
|
bl_idname = 'BPM_PT_bg_manager_panel'
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "View"
|
||||||
|
bl_label = "BG Plane Manager"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
scn = bpy.context.scene
|
||||||
|
|
||||||
|
# layout.label(text='Manual tests:')
|
||||||
|
# layout.operator('bpm.import_bg_images')
|
||||||
|
# layout.operator('bpm.convert_planes')
|
||||||
|
|
||||||
|
## Camera swapping
|
||||||
|
row = layout.row(align=True)
|
||||||
|
cam = context.scene.camera
|
||||||
|
|
||||||
|
# cam_name = cam.name if cam else 'None'
|
||||||
|
# if cam and cam_name == 'draw_cam':
|
||||||
|
# cam_name = f'{cam.parent.name} > {cam_name}'
|
||||||
|
# row.operator("bpm.swap_cams", text=cam_name, icon='OUTLINER_OB_CAMERA')
|
||||||
|
|
||||||
|
if cam:
|
||||||
|
anim_cam_name = 'anim_cam'
|
||||||
|
bg_cam_name = 'bg_cam'
|
||||||
|
in_anim = in_bg = False
|
||||||
|
|
||||||
|
if cam.name == 'anim_cam':
|
||||||
|
in_anim = True
|
||||||
|
|
||||||
|
elif cam.name == 'bg_cam':
|
||||||
|
in_bg = True
|
||||||
|
|
||||||
|
elif cam.name == 'draw_cam' and cam.parent:
|
||||||
|
if cam.parent.name == 'anim_cam':
|
||||||
|
in_anim = True
|
||||||
|
anim_cam_name = anim_cam_name + '> draw_cam'
|
||||||
|
|
||||||
|
elif cam.parent.name == 'bg_cam':
|
||||||
|
in_bg = True
|
||||||
|
bg_cam_name = bg_cam_name + '> draw_cam'
|
||||||
|
|
||||||
|
# in_bg_cam = context.scene.camera.name != 'bg_cam'
|
||||||
|
row.operator("bpm.swap_cams", text=anim_cam_name, icon='OUTLINER_OB_CAMERA', depress=in_anim)
|
||||||
|
row.operator("bpm.swap_cams", text=bg_cam_name, icon='OUTLINER_OB_CAMERA', depress=in_bg)
|
||||||
|
|
||||||
|
planes = [i.plane for i in scn.bg_props.planes]
|
||||||
|
|
||||||
|
if not planes:
|
||||||
|
## Show import/reload if nothing is in list
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("bpm.import_bg_images", icon="IMPORT", text="Import Planes")
|
||||||
|
row.operator("bpm.reload_list", icon="FILE_REFRESH", text="")
|
||||||
|
return
|
||||||
|
|
||||||
|
## Show settings related to active plane
|
||||||
|
active_item = scn.bg_props.planes[scn.bg_props.index]
|
||||||
|
ob = active_item.plane
|
||||||
|
if not ob:
|
||||||
|
layout.operator("bpm.reload_list", icon="FILE_REFRESH", text="Need refresh")
|
||||||
|
return
|
||||||
|
|
||||||
|
bg_main_col = context.view_layer.layer_collection.children.get(BGCOL)
|
||||||
|
if not bg_main_col:
|
||||||
|
layout.label(text="No Background collection")
|
||||||
|
layout.label(text="Import plane automatically create hierarchy")
|
||||||
|
return
|
||||||
|
|
||||||
|
if active_item.type == 'bg':
|
||||||
|
layercol = bg_main_col.children.get(ob.name[len(PREFIX):])
|
||||||
|
if not layercol:
|
||||||
|
layout.label(text=f"No {ob.name} collection")
|
||||||
|
# return
|
||||||
|
|
||||||
|
col= layout.column()
|
||||||
|
## Backgrounds and Objects UI List
|
||||||
|
col.separator()
|
||||||
|
row = col.row()
|
||||||
|
list_row = row.row()
|
||||||
|
list_row.scale_y = scn.bg_props.ui_list_scale # 1.6
|
||||||
|
minimum_rows = 8
|
||||||
|
list_row.template_list("BPM_UL_bg_list", "", scn.bg_props, "planes", scn.bg_props, "index", rows=minimum_rows)
|
||||||
|
#https://docs.blender.org/api/blender2.8/bpy.types.UILayout.html#bpy.types.UILayout.template_list
|
||||||
|
|
||||||
|
col = row.column(align=True)
|
||||||
|
|
||||||
|
col.operator('bpm.create_and_place_in_camera', icon='OUTLINER_OB_GREASEPENCIL', text='').create = True
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.menu("BPM_MT_more_option", icon='DOWNARROW_HLT', text='')
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
|
||||||
|
col.operator('bpm.move_plane', icon='TRIA_UP', text='').direction = 'UP'
|
||||||
|
col.operator('bpm.move_plane', icon='TRIA_DOWN', text='').direction = 'DOWN'
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
# select toggle
|
||||||
|
# icon = 'RESTRICT_SELECT_ON' if ob.select_get() else 'RESTRICT_SELECT_OFF'
|
||||||
|
# col.operator("bpm.select_swap_active_bg", icon=icon, text='')
|
||||||
|
|
||||||
|
# lock
|
||||||
|
bg_col = bpy.data.collections.get(BGCOL)
|
||||||
|
if bg_col:
|
||||||
|
# lock_icon = 'LOCKED' if bg_col.hide_select else 'UNLOCKED'
|
||||||
|
col.prop(bg_col, 'hide_select', icon='LOCKED', text='', invert_checkbox=True) # # emboss=False
|
||||||
|
# col.prop(bg_col, 'hide_select', text='') # origin select icon
|
||||||
|
|
||||||
|
# select all
|
||||||
|
col.operator('bpm.select_all', icon='RESTRICT_SELECT_OFF', text='')
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
col.use_property_split = True
|
||||||
|
if active_item.type == 'bg':
|
||||||
|
row = col.row()
|
||||||
|
tex_ob = next((o for o in ob.children), None)
|
||||||
|
if not tex_ob:
|
||||||
|
layout.label(text='Missing Image Child', icon='ERROR')
|
||||||
|
return
|
||||||
|
|
||||||
|
row.prop(scn.bg_props, 'opacity', slider=True)
|
||||||
|
|
||||||
|
# row.operator("bpm.change_background_type", icon="NODE_TEXTURE", text=text_bg_type)
|
||||||
|
|
||||||
|
if tex_ob.type == 'MESH' and len(tex_ob.data.materials):
|
||||||
|
text_mode = 'Clip' if tex_ob.data.materials[0].blend_method == 'CLIP' else 'Blend'
|
||||||
|
row.operator("bpm.change_material_alpha_mode", icon="MATERIAL", text=text_mode)
|
||||||
|
|
||||||
|
# located under list
|
||||||
|
row = layout.row()
|
||||||
|
row.label(text='Move:')
|
||||||
|
if active_item.type == 'bg':
|
||||||
|
row.operator("bpm.set_distance", text=f"Distance {ob['distance']:.1f}m")
|
||||||
|
row.prop(scn.bg_props, 'move_hided', text='Hided')
|
||||||
|
|
||||||
|
elif active_item.type == 'obj':
|
||||||
|
cam = context.scene.objects.get('bg_cam') or context.scene.camera
|
||||||
|
if hasattr(bpy.types, 'OBJECT_OT_depth_proportional_move'):
|
||||||
|
distance = (ob.matrix_world.to_translation() - cam.matrix_world.to_translation()).length
|
||||||
|
row.operator("object.depth_proportional_move", text=f"Distance {distance:.1f}m") # icon='TRANSFORM_ORIGINS'
|
||||||
|
else:
|
||||||
|
row.label(text=f"Distance {(ob.matrix_world.to_translation() - cam.matrix_world.to_translation()).length:.1f}m")
|
||||||
|
|
||||||
|
if active_item.type == 'bg':
|
||||||
|
row = layout.row()
|
||||||
|
# row.label(text='Actions:')
|
||||||
|
## Send object to a plane
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("bpm.send_gp_to_plane")
|
||||||
|
## parent to object
|
||||||
|
active = context.object
|
||||||
|
if active and active.parent and active not in planes:
|
||||||
|
layout.label(text=f'{active.parent.name} > {active.name}')
|
||||||
|
text='Clear Parent '
|
||||||
|
else:
|
||||||
|
text='Parent To Plane'
|
||||||
|
row.operator("bpm.parent_to_bg", text=text)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(BPM_PT_bg_manager_panel)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
if hasattr(bpy.types, BPM_PT_bg_manager_panel.bl_idname):
|
||||||
|
bpy.utils.unregister_class(BPM_PT_bg_manager_panel)
|
|
@ -0,0 +1,356 @@
|
||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
from .. import fn
|
||||||
|
from .. constants import PREFIX
|
||||||
|
from bpy.types import UIList, PropertyGroup, Menu
|
||||||
|
from bpy.props import (PointerProperty,
|
||||||
|
IntProperty,
|
||||||
|
BoolProperty,
|
||||||
|
StringProperty,
|
||||||
|
EnumProperty,
|
||||||
|
FloatProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
class BPM_UL_bg_list(UIList):
|
||||||
|
# order_by_distance : BoolProperty(default=True)
|
||||||
|
|
||||||
|
show_items : EnumProperty(
|
||||||
|
name="Show Items", description="Filter items to show, GP objects, Background or all",
|
||||||
|
default='all', options={'HIDDEN'},
|
||||||
|
items=(
|
||||||
|
('all', 'All', 'Show Background and Gp object', '', 0),
|
||||||
|
('obj', 'Gp Objects', 'Show only Gp object', '', 1),
|
||||||
|
('bg', 'Backgrounds', 'Show only backgrounds', '', 2),
|
||||||
|
))
|
||||||
|
#(key, label, descr, id[, icon])
|
||||||
|
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
# draw_item must handle the three layout types... Usually 'DEFAULT' and 'COMPACT' can share the same code.
|
||||||
|
# layout.alignment = 'CENTER'
|
||||||
|
|
||||||
|
## TODO: Find a better solution to get the collection
|
||||||
|
## Get Collection from plane name -> problem: prefix "BG_" and suffix
|
||||||
|
# if not item.plane.get('is_background_holder'):
|
||||||
|
if not item.plane.name.startswith(PREFIX): # (can check if has a parent using PREFIX)
|
||||||
|
gp_ob = item.plane
|
||||||
|
layout.prop(gp_ob, 'hide_viewport', text='', emboss=False, icon='HIDE_OFF')
|
||||||
|
|
||||||
|
icon_col = layout.row()
|
||||||
|
icon_col.label(text='', icon='OUTLINER_OB_GREASEPENCIL') # BLANK1
|
||||||
|
icon_col.ui_units_x = context.scene.bg_props.ui_list_scale # 1.6
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.label(text=gp_ob.name)
|
||||||
|
|
||||||
|
if gp_ob.data.users > 1:
|
||||||
|
row.template_ID(item, "data")
|
||||||
|
else:
|
||||||
|
row.label(text='', icon='BLANK1')
|
||||||
|
|
||||||
|
row.label(text='', icon='BLANK1')# <- add selection
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
layercol = context.view_layer.layer_collection.children['Background'].children.get(fn.clean_image_name(item.plane.name[len(PREFIX):]))
|
||||||
|
if not layercol:
|
||||||
|
layout.label(text=f'{item.plane.name} (problem with name)', icon='ERROR')
|
||||||
|
return
|
||||||
|
|
||||||
|
# icon = 'HIDE_ON' if layercol.exclude else 'HIDE_OFF'
|
||||||
|
layout.prop(layercol, 'exclude', text='', emboss=False, icon='HIDE_OFF')
|
||||||
|
|
||||||
|
if not item.plane.children:
|
||||||
|
layout.label(text=f'{item.plane.name} (No children)', icon='ERROR')
|
||||||
|
|
||||||
|
## Image preview
|
||||||
|
image = fn.get_image(item.plane.children[0])
|
||||||
|
# layout.label(icon_value=image.preview_ensure().icon_id)
|
||||||
|
# layout.template_icon(icon_value=image.preview_ensure().icon_id)
|
||||||
|
|
||||||
|
icon_col = layout.row()
|
||||||
|
icon_col.template_icon(icon_value=image.preview_ensure().icon_id, scale=1.0)
|
||||||
|
icon_col.ui_units_x = context.scene.bg_props.ui_list_scale # 1.6
|
||||||
|
|
||||||
|
## Name
|
||||||
|
row = layout.row()
|
||||||
|
row.enabled = not layercol.exclude
|
||||||
|
|
||||||
|
# row.label(text=item.plane.name) # <- Object has BG_ prefix, trim or use collection name
|
||||||
|
row.label(text=layercol.name)
|
||||||
|
|
||||||
|
if context.scene.bg_props.show_distance:# and not layercol.exclude:
|
||||||
|
row = layout.row()
|
||||||
|
# row.enabled = not layercol.exclude
|
||||||
|
row.prop(item.plane, '["distance"]', text='')
|
||||||
|
# layout.prop(item.plane, 'location', index=2, text='') # , emboss=False
|
||||||
|
|
||||||
|
ob = context.object
|
||||||
|
if ob and ob.parent == item.plane:
|
||||||
|
layout.label(text='', icon='DECORATE_LINKED') # select from prop group
|
||||||
|
else:
|
||||||
|
layout.label(text='', icon='BLANK1') # select from prop group
|
||||||
|
|
||||||
|
if not layercol.exclude:
|
||||||
|
icon = 'LAYER_ACTIVE' if item.plane.select_get() else 'LAYER_USED'
|
||||||
|
layout.prop(item, 'select', icon=icon, text='', emboss=False) # select from prop group
|
||||||
|
else:
|
||||||
|
layout.label(text='', icon='BLANK1') # select from prop group
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## note
|
||||||
|
# You should always start your row layout by a label (icon + text), or a non-embossed text field,
|
||||||
|
# this will also make the row easily selectable in the list! The later also enables ctrl-click rename.
|
||||||
|
# We use icon_value of label, as our given icon is an integer value, not an enum ID.
|
||||||
|
# Note "data" names should never be translated!
|
||||||
|
|
||||||
|
def draw_filter(self, context, layout):
|
||||||
|
row = layout.row()
|
||||||
|
|
||||||
|
subrow = row.row(align=True)
|
||||||
|
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use ‘*’ as wildcard)
|
||||||
|
|
||||||
|
# invert result
|
||||||
|
# icon = 'ZOOM_OUT' if self.use_filter_invert else 'ZOOM_IN'
|
||||||
|
# subrow.prop(self, "use_filter_invert", text="", icon=icon)
|
||||||
|
|
||||||
|
# sort by name : ALPHA SORTING NOT WORKING, MUST CHANGE IN filter_items
|
||||||
|
# subrow.prop(self, "use_filter_sort_alpha", text="", icon='SORTALPHA') # buit-in sort
|
||||||
|
|
||||||
|
subrow.prop(self, "show_items", text="") # type enum filter # icon='DOWNARROW_HLT'
|
||||||
|
|
||||||
|
# reverse order
|
||||||
|
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
|
||||||
|
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
|
||||||
|
|
||||||
|
def filter_items(self, context, data, propname):
|
||||||
|
helpers = bpy.types.UI_UL_list
|
||||||
|
items = getattr(data, propname)
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
# ordered = [items[:].index(i) for i in sorted(items[:], key=lambda o: o.plane.location.z)]
|
||||||
|
items = [(items[:].index(i), i) for i in items[:]]
|
||||||
|
|
||||||
|
# needed ?
|
||||||
|
filtered = [self.bitflag_filter_item] * len(items)
|
||||||
|
|
||||||
|
## Filter out out items thata
|
||||||
|
for i, item in items:
|
||||||
|
## GP/BG Type filter
|
||||||
|
if self.show_items != 'all':
|
||||||
|
if self.show_items != item.type:
|
||||||
|
filtered[i] &= ~self.bitflag_filter_item
|
||||||
|
## Search filter
|
||||||
|
if item.plane and self.filter_name.lower() not in item.plane.name.lower():
|
||||||
|
filtered[i] &= ~self.bitflag_filter_item
|
||||||
|
|
||||||
|
|
||||||
|
# if self.order_by_distance:
|
||||||
|
cam = context.scene.objects.get('bg_cam') or context.scene.camera
|
||||||
|
if not cam:
|
||||||
|
return filtered, ordered
|
||||||
|
|
||||||
|
## Real Distance from bg_cam or active camera
|
||||||
|
ordered = helpers.sort_items_helper(items, lambda o: (o[1].plane.matrix_world.to_translation() - cam.matrix_world.to_translation()).length)
|
||||||
|
## By distance attribute (only Backgrounds have distance attr)
|
||||||
|
# ordered = helpers.sort_items_helper(items, lambda o: o[1].plane.get('distance'))
|
||||||
|
|
||||||
|
return filtered, ordered
|
||||||
|
|
||||||
|
'''
|
||||||
|
class BPM_UL_bg_list(UIList):
|
||||||
|
# order_by_distance : BoolProperty(default=True)
|
||||||
|
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
# draw_item must handle the three layout types... Usually 'DEFAULT' and 'COMPACT' can share the same code.
|
||||||
|
# layout.alignment = 'CENTER'
|
||||||
|
|
||||||
|
## TODO: Find a better solution to get the collection
|
||||||
|
## Get Collection from plane name -> problem: prefix "BG_" and suffix
|
||||||
|
|
||||||
|
layercol = context.view_layer.layer_collection.children['Background'].children.get(fn.clean_image_name(item.plane.name[len(PREFIX):]))
|
||||||
|
if not layercol:
|
||||||
|
layout.label(text=f'{item.plane.name} (problem with name)', icon='ERROR')
|
||||||
|
return
|
||||||
|
|
||||||
|
# icon = 'HIDE_ON' if layercol.exclude else 'HIDE_OFF'
|
||||||
|
layout.prop(layercol, 'exclude', text='', emboss=False, icon='HIDE_OFF')
|
||||||
|
|
||||||
|
if not item.plane.children:
|
||||||
|
layout.label(text=f'{item.plane.name} (No children)', icon='ERROR')
|
||||||
|
|
||||||
|
## Image preview
|
||||||
|
image = fn.get_image(item.plane.children[0])
|
||||||
|
# layout.label(icon_value=image.preview_ensure().icon_id)
|
||||||
|
# layout.template_icon(icon_value=image.preview_ensure().icon_id)
|
||||||
|
|
||||||
|
icon_col = layout.row()
|
||||||
|
icon_col.template_icon(icon_value=image.preview_ensure().icon_id, scale=1.0)
|
||||||
|
icon_col.ui_units_x = context.scene.bg_props.ui_list_scale # 1.6
|
||||||
|
|
||||||
|
|
||||||
|
## Name
|
||||||
|
row = layout.row()
|
||||||
|
row.enabled = not layercol.exclude
|
||||||
|
|
||||||
|
# row.label(text=item.plane.name) # <- Object has BG_ prefix, trim or use collection name
|
||||||
|
row.label(text=layercol.name)
|
||||||
|
|
||||||
|
ob = context.object
|
||||||
|
if ob and ob.parent == item.plane:
|
||||||
|
layout.label(text='', icon='DECORATE_LINKED') # select from prop group
|
||||||
|
else:
|
||||||
|
layout.label(text='', icon='BLANK1') # select from prop group
|
||||||
|
|
||||||
|
if not layercol.exclude:
|
||||||
|
icon = 'LAYER_ACTIVE' if item.plane.select_get() else 'LAYER_USED'
|
||||||
|
layout.prop(item, 'select', icon=icon, text='', emboss=False) # select from prop group
|
||||||
|
else:
|
||||||
|
layout.label(text='', icon='BLANK1') # select from prop group
|
||||||
|
|
||||||
|
if context.scene.bg_props.show_distance:# and not layercol.exclude:
|
||||||
|
row = layout.row()
|
||||||
|
# row.enabled = not layercol.exclude
|
||||||
|
row.prop(item.plane, '["distance"]', text='')
|
||||||
|
# layout.prop(item.plane, 'location', index=2, text='') # , emboss=False
|
||||||
|
|
||||||
|
|
||||||
|
## note
|
||||||
|
# You should always start your row layout by a label (icon + text), or a non-embossed text field,
|
||||||
|
# this will also make the row easily selectable in the list! The later also enables ctrl-click rename.
|
||||||
|
# We use icon_value of label, as our given icon is an integer value, not an enum ID.
|
||||||
|
# Note "data" names should never be translated!
|
||||||
|
|
||||||
|
|
||||||
|
def filter_items(self, context, data, propname):
|
||||||
|
helpers = bpy.types.UI_UL_list
|
||||||
|
items = getattr(data, propname)
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
# ordered = [items[:].index(i) for i in sorted(items[:], key=lambda o: o.plane.location.z)]
|
||||||
|
items = [(items[:].index(i), i) for i in items[:]]
|
||||||
|
|
||||||
|
# needed ?
|
||||||
|
filtered = [self.bitflag_filter_item] * len(items)
|
||||||
|
|
||||||
|
for i, item in items:
|
||||||
|
if item.plane and self.filter_name.lower() not in item.plane.name.lower():
|
||||||
|
filtered[i] &= ~self.bitflag_filter_item
|
||||||
|
#else:
|
||||||
|
|
||||||
|
|
||||||
|
# if self.order_by_distance:
|
||||||
|
|
||||||
|
ordered = helpers.sort_items_helper(items, lambda o: o[1].plane.get('distance'))
|
||||||
|
|
||||||
|
return filtered, ordered
|
||||||
|
|
||||||
|
# def draw_filter(self, context, layout):
|
||||||
|
# """UI code for the filtering/sorting/search area."""
|
||||||
|
# col = layout.column(align=True)
|
||||||
|
# row = col.row(align=True)
|
||||||
|
|
||||||
|
# # row.prop(self, 'order_by_distance', text='', icon='DRIVER_DISTANCE')
|
||||||
|
# # row.prop(self, 'use_filter_invert', text='', icon='ARROW_LEFTRIGHT')
|
||||||
|
'''
|
||||||
|
|
||||||
|
### --- updates
|
||||||
|
|
||||||
|
def get_plane_targets(context):
|
||||||
|
selection = [i.plane for i in context.scene.bg_props.planes if i.plane.select_get()]
|
||||||
|
if selection:
|
||||||
|
return selection
|
||||||
|
## active (even not selected)
|
||||||
|
return [context.scene.bg_props.planes[context.scene.bg_props.index].plane]
|
||||||
|
|
||||||
|
def update_opacity(self, context):
|
||||||
|
pool = get_plane_targets(context)
|
||||||
|
for ob in pool:
|
||||||
|
if not ob or not ob.children:
|
||||||
|
continue
|
||||||
|
fn.set_opacity(ob.children[0], opacity=context.scene.bg_props.opacity)
|
||||||
|
|
||||||
|
def update_on_index_change(self, context):
|
||||||
|
# print('index change:', context.scene.bg_props.index)
|
||||||
|
props = context.scene.bg_props
|
||||||
|
planes_list = props.planes
|
||||||
|
item = planes_list[props.index]
|
||||||
|
if not item.plane:
|
||||||
|
## remove slot !
|
||||||
|
print(f'bg_props.planes: No plane/object at index {props.index}, removing item')
|
||||||
|
planes_list.remove(props.index)
|
||||||
|
return
|
||||||
|
|
||||||
|
if item.type == 'bg':
|
||||||
|
plane = item.plane
|
||||||
|
if not plane.children:
|
||||||
|
return
|
||||||
|
opacity = fn.get_opacity(plane.children[0])
|
||||||
|
if not opacity:
|
||||||
|
return
|
||||||
|
props['opacity'] = opacity
|
||||||
|
|
||||||
|
elif item.type == 'obj':
|
||||||
|
fn.gp_transfer_mode(item.plane, context)
|
||||||
|
|
||||||
|
|
||||||
|
def update_select(self, context):
|
||||||
|
# print('index change:', context.scene.bg_props.index)
|
||||||
|
plane = self.plane
|
||||||
|
plane.select_set(not plane.select_get())
|
||||||
|
context.scene.bg_props['index'] = context.scene.bg_props.planes[:].index(self)
|
||||||
|
|
||||||
|
class BPM_more_option(Menu):
|
||||||
|
bl_idname = "BPM_MT_more_option"
|
||||||
|
bl_label = "Options"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.operator("bpm.reload_list", text="Refresh", icon="FILE_REFRESH")
|
||||||
|
layout.operator("bpm.open_bg_folder", icon="FILE_FOLDER")
|
||||||
|
layout.operator("bpm.import_bg_images", text="Import Planes", icon="IMPORT")
|
||||||
|
layout.operator("bpm.convert_planes", text="Convert Planes", icon="OUTLINER_OB_IMAGE")
|
||||||
|
layout.separator()
|
||||||
|
layout.prop(context.scene.bg_props, 'show_distance', icon='DRIVER_DISTANCE')
|
||||||
|
layout.prop(context.scene.bg_props, 'ui_list_scale')
|
||||||
|
|
||||||
|
# Create sniptool property group
|
||||||
|
class BPM_bg_list_prop(PropertyGroup):
|
||||||
|
plane : PointerProperty(type=bpy.types.Object)
|
||||||
|
select: BoolProperty(update=update_select) # use and update to set the plane selection
|
||||||
|
type: StringProperty(default='bg')
|
||||||
|
# is_bg: BoolProperty(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BPM_bg_settings(PropertyGroup):
|
||||||
|
index : IntProperty(update=update_on_index_change)
|
||||||
|
planes : bpy.props.CollectionProperty(type=BPM_bg_list_prop)
|
||||||
|
show_distance : BoolProperty(name='Show Distance', default=False)
|
||||||
|
opacity : FloatProperty(name='Opacity', default=1.0, min=0.0, max=1.0, update=update_opacity)
|
||||||
|
move_hided : BoolProperty(name='Move hided', default=True)
|
||||||
|
ui_list_scale : FloatProperty(name='UI Item Y Scale', default=1.6, min=0.6, soft_min=1.0, max=2.0)
|
||||||
|
# distance : FloatProperty(name='Distance', default=0.0, update=update_distance)
|
||||||
|
|
||||||
|
|
||||||
|
### --- REGISTER ---
|
||||||
|
|
||||||
|
classes=(
|
||||||
|
# prop and UIlist
|
||||||
|
BPM_bg_list_prop,
|
||||||
|
BPM_bg_settings,
|
||||||
|
BPM_UL_bg_list,
|
||||||
|
BPM_more_option,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
bpy.types.Scene.bg_props = bpy.props.PointerProperty(type=BPM_bg_settings)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
del bpy.types.Scene.bg_props
|
Loading…
Reference in New Issue