From e4ae2be1e865f603c12bc85aa9701ff1dede77fa Mon Sep 17 00:00:00 2001 From: ChristopheSeux Date: Thu, 28 Sep 2023 11:34:41 +0200 Subject: [PATCH] First Commit --- CHANGELOG.md | 6 + LICENSE | 339 +++++++++++++++ __init__.py | 76 ++++ auto_modules.py | 145 +++++++ constants.py | 6 + export_psd_layers.py | 278 +++++++++++++ file_utils.py | 134 ++++++ fn.py | 809 ++++++++++++++++++++++++++++++++++++ operators/__init__.py | 25 ++ operators/convert_planes.py | 165 ++++++++ operators/import_planes.py | 263 ++++++++++++ operators/manage_objects.py | 397 ++++++++++++++++++ operators/manage_planes.py | 319 ++++++++++++++ preferences.py | 66 +++ texture_plane.blend | Bin 0 -> 615812 bytes ui/__init__.py | 23 + ui/panels.py | 172 ++++++++ ui/ui_list.py | 356 ++++++++++++++++ 18 files changed, 3579 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 __init__.py create mode 100644 auto_modules.py create mode 100644 constants.py create mode 100644 export_psd_layers.py create mode 100644 file_utils.py create mode 100644 fn.py create mode 100644 operators/__init__.py create mode 100644 operators/convert_planes.py create mode 100644 operators/import_planes.py create mode 100644 operators/manage_objects.py create mode 100644 operators/manage_planes.py create mode 100644 preferences.py create mode 100644 texture_plane.blend create mode 100644 ui/__init__.py create mode 100644 ui/panels.py create mode 100644 ui/ui_list.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6b11b4d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + + +0.3.0 + +- initial commit \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0029184 --- /dev/null +++ b/LICENSE @@ -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. + + , 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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..195e288 --- /dev/null +++ b/__init__.py @@ -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() diff --git a/auto_modules.py b/auto_modules.py new file mode 100644 index 0000000..b5c7dc1 --- /dev/null +++ b/auto_modules.py @@ -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') +""" + \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..bcdb948 --- /dev/null +++ b/constants.py @@ -0,0 +1,6 @@ +from pathlib import Path + +PREFIX = 'BG_' +INIT_POS = 9.0 +MODULE_DIR = Path(__file__).parent +BGCOL = 'Background' \ No newline at end of file diff --git a/export_psd_layers.py b/export_psd_layers.py new file mode 100644 index 0000000..2a1ddd4 --- /dev/null +++ b/export_psd_layers.py @@ -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) diff --git a/file_utils.py b/file_utils.py new file mode 100644 index 0000000..a1b998c --- /dev/null +++ b/file_utils.py @@ -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) \ No newline at end of file diff --git a/fn.py b/fn.py new file mode 100644 index 0000000..41e7fad --- /dev/null +++ b/fn.py @@ -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 \ No newline at end of file diff --git a/operators/__init__.py b/operators/__init__.py new file mode 100644 index 0000000..9118b33 --- /dev/null +++ b/operators/__init__.py @@ -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() \ No newline at end of file diff --git a/operators/convert_planes.py b/operators/convert_planes.py new file mode 100644 index 0000000..dce56d3 --- /dev/null +++ b/operators/convert_planes.py @@ -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) diff --git a/operators/import_planes.py b/operators/import_planes.py new file mode 100644 index 0000000..ea061ab --- /dev/null +++ b/operators/import_planes.py @@ -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) \ No newline at end of file diff --git a/operators/manage_objects.py b/operators/manage_objects.py new file mode 100644 index 0000000..f7e29db --- /dev/null +++ b/operators/manage_objects.py @@ -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) diff --git a/operators/manage_planes.py b/operators/manage_planes.py new file mode 100644 index 0000000..676f681 --- /dev/null +++ b/operators/manage_planes.py @@ -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) diff --git a/preferences.py b/preferences.py new file mode 100644 index 0000000..a771b39 --- /dev/null +++ b/preferences.py @@ -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) \ No newline at end of file diff --git a/texture_plane.blend b/texture_plane.blend new file mode 100644 index 0000000000000000000000000000000000000000..4a2d3bc0fdebf75b97a7026d017baebf122d8610 GIT binary patch literal 615812 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 H>)-zYvvr}m literal 0 HcmV?d00001 diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..ab89266 --- /dev/null +++ b/ui/__init__.py @@ -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() \ No newline at end of file diff --git a/ui/panels.py b/ui/panels.py new file mode 100644 index 0000000..401f946 --- /dev/null +++ b/ui/panels.py @@ -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) diff --git a/ui/ui_list.py b/ui/ui_list.py new file mode 100644 index 0000000..72f11a2 --- /dev/null +++ b/ui/ui_list.py @@ -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