From 512b99276665657856ec29a16a4aba1dd33971b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 25 Jan 2021 16:53:15 -0600 Subject: [PATCH 01/23] + Initial setup for nxt_unreal plugin. attached to release bot added readme --- README.md | 32 ++---- build/release.nxt | 61 +++++++++++- nxt_editor/__init__.py | 1 + .../unreal/Content/Python/init_unreal.py | 94 ++++++++++++++++++ nxt_editor/integration/unreal/README.md | 17 ++++ nxt_editor/integration/unreal/__init__.py | 30 ++++++ .../integration/unreal/nxt_unreal.uplugin | 17 ++++ nxt_editor/resources/icons/nxt_128.png | Bin 0 -> 1542 bytes 8 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 nxt_editor/integration/unreal/Content/Python/init_unreal.py create mode 100644 nxt_editor/integration/unreal/README.md create mode 100644 nxt_editor/integration/unreal/__init__.py create mode 100644 nxt_editor/integration/unreal/nxt_unreal.uplugin create mode 100644 nxt_editor/resources/icons/nxt_128.png diff --git a/README.md b/README.md index 4272f4e..1718755 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [Installation/Usage](#installationusage) | [Docs](https://nxt-dev.github.io/) | [Contributing](CONTRIBUTING.md) | [Licensing](LICENSE) # Installation/Usage -**To Use NXT please use the [NXT Standalone](#nxt-standalone) or [DCC plugin zip.](#maya-plugin)** +**To Use NXT please use the [NXT Standalone](#nxt-standalone) or [DCC plugin zip.](#DCC-Plugins)** Only clone this repo if you're [contributing](CONTRIBUTING.md) to the NXT codebase.
@@ -24,32 +24,14 @@ Our releases are hosted on [PyPi](https://pypi.org/project/nxt-editor/). - Update: - `pip install -U nxt-editor` -### Blender addon: -- Install: - 1. Download Blender addon (nxt_blender.zip) [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest) - 2. Extract and follow `README.md` inside [nxt_blender](nxt_editor/integration/blender/README.md) instructions (also included in the download) -- Launch: - 1. Load the `nxt_blender` Addon (Edit > Preferences > Add-ons) - 2. Navigate the newly created NXT menu and select Open Editor. -- Update: - - Automatically: NXT > Update NXT - - By Hand: `/path/to/python.exe -m pip install -U nxt-editor` - - Relaunch Blender after - +### DCC Plugins -### Maya plugin: +Each of our supported DCC's get a zip file on our [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest) -- Install: - 1. Download the maya module(`nxt_maya.zip`) from the [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest) - 2. Follow the [nxt_maya](nxt_editor/integration/maya/README.md) instructions (also included in the download) -- Launch: - 1. Load `nxt_maya` plugin in Maya - 2. Select the `nxt` menu from the menus at the top of Maya - 3. Click `Open Editor` -- Update: - 1. Download the `nxt_maya` zip from the [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest) - 2. Extract the zip and replace the existing `nxt_maya` files with the newly extracted files. - 3. Re-launch Maya +Each one contains a `README.md` inside to explain how to install/update them. +- [nxt_maya](nxt_editor/integration/maya/README.md) +- [nxt_blender](nxt_editor/integration/blender/README.md) +- [nxt_unreal](nxt_editor/integration/unreal/README.md)
diff --git a/build/release.nxt b/build/release.nxt index e21a2dc..2a0840a 100644 --- a/build/release.nxt +++ b/build/release.nxt @@ -5,6 +5,7 @@ "mute": false, "solo": false, "references": [ + "H:/Projects/nxt/nxt_editor/build/make_unreal_plugin.nxt", "make_maya_plugin.nxt", "make_blender_plugin.nxt", "../../nxt/build/release.nxt" @@ -23,8 +24,8 @@ }, "positions": { "/CreateRelease": [ - 2460.0, - 60.0 + 2861.9022362347496, + 69.87218527196421 ], "/GitClone": [ -1000.0, @@ -59,8 +60,8 @@ 420.0 ], "/ReleaseLoop": [ - 2420.0, - 0.0 + 2824.2201296032613, + 7.554291903452558 ], "/build_maya_plugin": [ 1800.0, @@ -78,6 +79,10 @@ 1820.0, 0.0 ], + "/make_plugin": [ + 2465.5358761299212, + 1.3083409543444589 + ], "/versions": [ 140.0, 0.0 @@ -85,6 +90,10 @@ "/zip_blender_addon": [ 2140.0, 80.0 + ], + "/zip_blender_addon2": [ + 2460.556716567386, + 151.71085853406586 ] }, "collapse": { @@ -128,6 +137,7 @@ "DraftRelease", "UploadBlenderAddon", "UploadMayaPlugin", + "UploadUnrealPlugin", "OpenReleaseURL" ] }, @@ -157,8 +167,21 @@ } } }, + "/CreateRelease/UploadUnrealPlugin": { + "instance": "/GitUpload", + "attrs": { + "asset_path": { + "type": "raw", + "value": "${/make_plugin/zip_unreal_plugin.zip_path}" + }, + "content_type": { + "type": "raw", + "value": "application/zip" + } + } + }, "/ReleaseLoop": { - "execute_in": "/make_addon", + "execute_in": "/make_plugin", "attrs": { "release_types": { "type": "tuple", @@ -224,6 +247,34 @@ "shutil.rmtree('${mod_folder}')" ] }, + "/make_plugin": { + "start_point": false, + "execute_in": "/make_addon", + "child_order": [ + "zip_unreal_plugin" + ], + "attrs": { + "result_dir": { + "value": "${path::${release_dir}/nxt_unreal}" + } + } + }, + "/make_plugin/zip_unreal_plugin": { + "attrs": { + "zip_name": { + "type": "raw", + "value": "${result_dir}" + }, + "zip_path": { + "type": "raw", + "value": "${zip_name}.zip" + } + }, + "code": [ + "self.zip_path = shutil.make_archive('${zip_name}', 'zip', '${result_dir}')", + "shutil.rmtree('${result_dir}')" + ] + }, "/versions": { "attrs": { "EDITOR": { diff --git a/nxt_editor/__init__.py b/nxt_editor/__init__.py index c96437e..a9d778a 100644 --- a/nxt_editor/__init__.py +++ b/nxt_editor/__init__.py @@ -78,6 +78,7 @@ def make_resources(qrc_path=None, result_path=None): from nxt_editor import qresources except ImportError: make_resources() + from nxt_editor import qresources def _new_qapp(): diff --git a/nxt_editor/integration/unreal/Content/Python/init_unreal.py b/nxt_editor/integration/unreal/Content/Python/init_unreal.py new file mode 100644 index 0000000..318c919 --- /dev/null +++ b/nxt_editor/integration/unreal/Content/Python/init_unreal.py @@ -0,0 +1,94 @@ +import os +import sys +import unreal +import subprocess + + +def is_nxt_available(): + try: + from nxt_editor.integration.unreal import launch_nxt_in_ue + return True + except: + return False + +def get_python_exc_path(): + exc_name = 'python' + if sys.platform == 'win32': + exc_name = 'python.exe' + + real_prefix = os.path.realpath(sys.prefix) + return os.path.join(real_prefix, exc_name) + +def install_nxt_to_interpreter(): + subprocess.check_call([get_python_exc_path(), '-m', 'pip', + 'install', 'nxt-editor']) + unreal.log_warning("Please restart the editor for nxt menu options.") + +def update_installed_nxt(): + subprocess.check_call([get_python_exc_path(), '-m', 'pip', + 'install', '--upgrade', 'nxt-editor']) + +def uninstall_nxt_from_interpreter(): + subprocess.check_call([get_python_exc_path(), '-m', 'pip', + 'uninstall', '-y', 'nxt-editor', 'nxt-core']) + unreal.log_warning("Nxt menu will refresh next editor launch.") + +def make_open_editor_entry(): + entry = unreal.ToolMenuEntry(name='Open Editor', + type=unreal.MultiBlockType.MENU_ENTRY) + entry.set_label('Open Editor') + launch_command = "from nxt_editor.integration.unreal import launch_nxt_in_ue; launch_nxt_in_ue()" + entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python', + string=launch_command) + return entry + +def make_install_entry(): + entry = unreal.ToolMenuEntry(name='Install Package', + type=unreal.MultiBlockType.MENU_ENTRY) + entry.set_label('Install nxt package to active python') + entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python', + string='install_nxt_to_interpreter()') + return entry + +def make_update_entry(): + entry = unreal.ToolMenuEntry(name='Update nxt package', + type=unreal.MultiBlockType.MENU_ENTRY) + entry.set_label('Update nxt python package') + entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python', + string='update_installed_nxt()') + return entry + +def make_uninstall_entry(): + entry = unreal.ToolMenuEntry(name='Uninstall Package', + type=unreal.MultiBlockType.MENU_ENTRY) + entry.set_label('Uninstall nxt package from active python') + entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python', + string='uninstall_nxt_from_interpreter()') + return entry + + +def make_or_find_nxt_menu(): + menus = unreal.ToolMenus.get() + nxt_menu = menus.find_menu("LevelEditor.MainMenu.NxtMenu") + if nxt_menu: + return nxt_menu + main_menu = menus.find_menu("LevelEditor.MainMenu") + if not main_menu: + raise ValueError("Cannot find main menu") + nxt_menu = main_menu.add_sub_menu(main_menu.get_name(), "nxt-section", + "NxtMenu", "nxt", "The nxt graph editor") + return nxt_menu + +def refresh_nxt_menu(): + nxt_menu = make_or_find_nxt_menu() + if is_nxt_available(): + nxt_menu.add_menu_entry("nxt-section", make_open_editor_entry()) + nxt_menu.add_menu_entry("nxt-section", make_update_entry()) + nxt_menu.add_menu_entry("nxt-section", make_uninstall_entry()) + else: + nxt_menu.add_menu_entry("nxt-section", make_install_entry()) + menus = unreal.ToolMenus.get() + menus.refresh_all_widgets() + +if __name__ == '__main__': + refresh_nxt_menu() diff --git a/nxt_editor/integration/unreal/README.md b/nxt_editor/integration/unreal/README.md new file mode 100644 index 0000000..5732b6f --- /dev/null +++ b/nxt_editor/integration/unreal/README.md @@ -0,0 +1,17 @@ +# Installation +**This is an experimental version of nxt_unreal. Save early, save often.** +This is an Unreal plugin to connect to the nxt python package. The nxt python package will be downloaded from the internet as part of this installation. + +1. Move this plugin either into your project or engine's plugin directory. +2. Activate the plugin in the engine plugin browser. +3. Install the nxt python package + - __automated -__ From top menu option "nxt", select "Install nxt package to active python" + - __by hand -__ Locate engine python(`sys.prefix`) in commmand prompt or terminal and run: `python -m pip install nxt-editor` (Note on windows this will be `python.exe`) +4. Restart Editor + +# Launch +From the top menu "nxt", select "Open Editor" to start the nxt editor. + +# Update +- __automated -__ From nxt menu select "Update nxt package" +- __by hand -__ From command line of engine python run `python -m pip install --upgrade nxt-editor` diff --git a/nxt_editor/integration/unreal/__init__.py b/nxt_editor/integration/unreal/__init__.py new file mode 100644 index 0000000..3d7277e --- /dev/null +++ b/nxt_editor/integration/unreal/__init__.py @@ -0,0 +1,30 @@ +# Built-in +import os + +# External +import unreal +from Qt import QtWidgets + +# Internal +from nxt.constants import NXT_DCC_ENV_VAR +import nxt_editor + + +global __NXT_WINDOW +__NXT_WINDOW = None + +def launch_nxt_in_ue(): + os.environ[NXT_DCC_ENV_VAR] = 'unreal' + existing = QtWidgets.QApplication.instance() + if existing: + unreal.log('Found existing QApp') + else: + unreal.log('Building new QApp for nxt') + nxt_editor._new_qapp() + + global __NXT_WINDOW + if __NXT_WINDOW: + __NXT_WINDOW.show() + __NXT_WINDOW.raise_() + else: + __NXT_WINDOW = nxt_editor.show_new_editor() \ No newline at end of file diff --git a/nxt_editor/integration/unreal/nxt_unreal.uplugin b/nxt_editor/integration/unreal/nxt_unreal.uplugin new file mode 100644 index 0000000..f933456 --- /dev/null +++ b/nxt_editor/integration/unreal/nxt_unreal.uplugin @@ -0,0 +1,17 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "0.1.0", + "FriendlyName": "Nxt Unreal", + "Description": "Unreal editor connections to the nxt python module.", + "Category": "Scripting", + "CreatedBy": "The nxt Authors", + "CreatedByURL": "https://nxt-dev.github.io", + "DocsURL": "https://nxt-dev.github.io/tutorials", + "MarketplaceURL": "", + "SupportURL": "https://github.com/nxt-dev/nxt_editor/discussions", + "CanContainContent": true, + "IsBetaVersion": true, + "IsExperimentalVersion": false, + "Installed": false +} \ No newline at end of file diff --git a/nxt_editor/resources/icons/nxt_128.png b/nxt_editor/resources/icons/nxt_128.png new file mode 100644 index 0000000000000000000000000000000000000000..16f80063e9dc53b324724f1ba618dc10bfa25668 GIT binary patch literal 1542 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&F%}28J2BoosZ$T+a29w(7Bet# z3xhBt!>lL zj9ykAL7?yC-4a;2`PqR|%*@OjTx@*8jLa--yn-qLIfi*tg*42hEdBJ-y16A3ID{mO zi|5$)>=#rsRtqh5o_bn9S>LR3soT73T5%1kLHSOTk25eba*4~^cJ9)Qsg-w)bDnla zSjXC`X#=mUx>?myW;RZ@xmRVKqwM<*NLl%_a`EXTx3lsHxX!+;5n0K^!YZh4YFfU? zD1SN=8;5#$nahlG7PYHw+O|1OIjQKG!pO*^9#O$1CTEZ{*`ej**O(FlQp8N)k2EQDi*tgq7@i)pn!D-g*=GiI_D~wab3IN zDo6oPeExN}IoBZJ4^-eX_X;?gfKuRinSVq7TJHs5C?6;Z@(X5QQP8!tvDR1N=8xKW z|IN=ozurGSRU&SE`2W3FzLI5<+$)YqXZ={uWSGpc`;mf8C)50kLPD2xDSX|kt_V@SoEx3|NC zg9Am{9(KekN-?J1bWMIQx+bD&S7;B1mfJy%F5RvMw>M3;W^ZEO?EU`lde7vUlP6C; z8))?IZ|~zOedF{`w&I@aHp%ey8DCdfnYsCySN6qCp|9DV-`?WuSGX;Ey-9fcl`UWT zKK=ERT(a_9tjg!u-7jbT5B)B8?eDz3F;qU zOZ*ZxM_43Q)z8m}D5?L!!IJPqgV~^phv|V#Hp8)0MTU1d%m#H%U!HbOR$HCZ=3mN^N0avRED&`~XsB0}aM;4dtDzgy%Q_*SSHdCn zX9C08uMLd1D>VhmKj?k><&ehG)_?~RP#3>z51&O5-sW&mO~C>{7GWw2m@83&t;1qWM) zbNfMthqg>?9gB}2XxMY1P{(181t(ia@bZHUf#qyG0_pX?tCvUw)bjBNJgTvjD0rwO zp`aJb#(YHdHvb~F29AY{q2WLiwhCr2c4UhtG^}u8c3H7-z0;wF*IUhVc4e|&xvw?t z`D$;03pZZnpXR=Ek>$r^37}*CFf$yj&)cufdgRaE|MzOA+8Qv*Nike#Jgm&jVD+&g zhrz&I&wrj2!gy%J$={NVt26mp4n_f9L;Ae>UyEsryY> Rj{>tAgQu&X%Q~loCIA8Ezy$yR literal 0 HcmV?d00001 From 9bdee81d0f1c2b6587d9f0847ed2de766828f849 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 26 Jan 2021 11:14:58 -0500 Subject: [PATCH 02/23] + One click Blender context. + Support for contexts that aren't strictly Python interpreters. * Centralized blender integration functions into a single class that can be called without the addon being loaded. * `launch_editor` now has a kwarg to NOT call `app.exec_()`, needed for host DCCs that aren't themselves a QApp. ... Switched to using `subprocess` as the pip approach. --- nxt_editor/__init__.py | 4 +- nxt_editor/integration/__init__.py | 98 ++++--- nxt_editor/integration/blender/__init__.py | 100 ++++++++ nxt_editor/integration/blender/nxt_blender.py | 239 +++++++++++++----- nxt_editor/main_window.py | 10 +- 5 files changed, 329 insertions(+), 122 deletions(-) create mode 100644 nxt_editor/integration/blender/__init__.py diff --git a/nxt_editor/__init__.py b/nxt_editor/__init__.py index c96437e..5682c0f 100644 --- a/nxt_editor/__init__.py +++ b/nxt_editor/__init__.py @@ -95,7 +95,7 @@ def _new_qapp(): return app -def launch_editor(paths=None, start_rpc=True): +def launch_editor(paths=None, start_rpc=True, call_exec=True): """Launch an instance of the editor. Will attach to existing QApp if found, otherwise will create and open one. """ @@ -106,7 +106,7 @@ def launch_editor(paths=None, start_rpc=True): app = _new_qapp() instance = show_new_editor(paths, start_rpc) app.setActiveWindow(instance) - if not existing: + if not existing and call_exec: app.exec_() return instance diff --git a/nxt_editor/integration/__init__.py b/nxt_editor/integration/__init__.py index 020fa48..2abb891 100644 --- a/nxt_editor/integration/__init__.py +++ b/nxt_editor/integration/__init__.py @@ -1,5 +1,6 @@ import os -import shutil +import sys +import subprocess import importlib @@ -32,29 +33,41 @@ def _safe_import_package(package_name, global_name=None): def _install_and_import_package(self, module_name, package_name=None, global_name=None): + """Calls a subprocess to pip install the given package name and then + attempts to import the new package. + + :param module_name: Desired module to import after install + :param package_name: pip package name + :param global_name: Global name to access the module if different + than the module name. + :raises: subprocess.CalledProcessError + :return: bool + """ if package_name is None: package_name = module_name if global_name is None: global_name = module_name - args = ['install', package_name] - self.ensure_pip() - import pip - if hasattr(pip, 'main'): - pip.main(['install', package_name]) - else: - pip._internal.main(['install', package_name]) + environ_copy = dict(os.environ) + environ_copy["PYTHONNOUSERSITE"] = "1" + subprocess.run([sys.executable, "-m", "pip", "install", + package_name], check=True, env=environ_copy) + success = self._safe_import_package(package_name=package_name, global_name=global_name) return success - def _update_package(self, package_name): - args = ['install', '-U', package_name] - self.ensure_pip() - import pip - if hasattr(pip, 'main'): - pip.main(args) - else: - pip._internal.main(args) + @staticmethod + def _update_package(package_name): + """Calls a subprocess to pip update the given package name. + + :param package_name: pip package name + :raises: subprocess.CalledProcessError + :return: None + """ + environ_copy = dict(os.environ) + environ_copy["PYTHONNOUSERSITE"] = "1" + subprocess.run([sys.executable, "-m", "pip", "install", "-U", + package_name], check=True, env=environ_copy) print('Please restart your DCC or Python interpreter') def check_for_nxt_core(self, install=False): @@ -81,51 +94,26 @@ def check_for_nxt_editor(self, install=False): print('Failed to import and/or install nxt-editor') return success - @staticmethod - def ensure_pip(): - try: - import pip - except ImportError: - import ensurepip - ensurepip.bootstrap() - os.environ.pop('PIP_REQ_TRACKER', None) - def update(self): if self.check_for_nxt_core(): self._update_package('nxt-core') if self.check_for_nxt_editor(): self._update_package('nxt-editor') + @ staticmethod + def uninstall(): + environ_copy = dict(os.environ) + environ_copy["PYTHONNOUSERSITE"] = "1" + subprocess.run([sys.executable, "-m", "pip", "uninstall", + "nxt-editor", "nxt-core"], check=True, env=environ_copy) + print('Please restart your DCC or Python interpreter') -class Blender(NxtIntegration): - def __init__(self): - super(Blender, self).__init__(name='blender') - import bpy - b_major, b_minor, b_patch = bpy.app.version - if b_major != 2 or b_minor < 80: - raise RuntimeError('Blender version is not compatible with this ' - 'version of nxt.') - user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' - 'Foundation/Blender/' - '{}.{}'.format(b_major, b_minor)) - self.user_dir = user_dir - nxt_modules = os.path.join(user_dir, 'scripts/addons/modules') - self.modules_dir = nxt_modules.replace(os.sep, '/') + def launch_nxt(self): + raise NotImplementedError('Your DCC needs it own nxt launch method.') - @classmethod - def setup(cls): - self = cls() - import bpy - bpy.ops.preferences.addon_disable(module='nxt_' + self.name) - addons_dir = os.path.join(self.user_dir, 'scripts/addons') - integration_filepath = self.get_integration_filepath() - shutil.copy(integration_filepath, addons_dir) - bpy.ops.preferences.addon_enable(module='nxt_' + self.name) + def quit_nxt(self): + raise NotImplementedError('Your DCC needs it own nxt quit method.') - @classmethod - def update(cls): - self = cls() - og_cwd = os.getcwd() - os.chdir(self.modules_dir) - super(Blender, self).update() - os.chdir(og_cwd) + def create_context(self): + raise NotImplementedError('Your DCC needs it own method of creating a ' + 'context.') diff --git a/nxt_editor/integration/blender/__init__.py b/nxt_editor/integration/blender/__init__.py new file mode 100644 index 0000000..a9b722f --- /dev/null +++ b/nxt_editor/integration/blender/__init__.py @@ -0,0 +1,100 @@ +# Builtin +import os +import shutil +import sys +import atexit + +# External +import bpy +from Qt import QtCore, QtWidgets + +# Internal +from nxt.constants import NXT_DCC_ENV_VAR +from nxt_editor.integration import NxtIntegration +import nxt_editor + +__NXT_INTEGRATION__ = None + + +class Blender(NxtIntegration): + def __init__(self): + super(Blender, self).__init__(name='blender') + b_major, b_minor, b_patch = bpy.app.version + if b_major != 2 or b_minor < 80: + raise RuntimeError('Blender version is not compatible with this ' + 'version of nxt.') + user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' + 'Foundation/Blender/' + '{}.{}'.format(b_major, b_minor)) + self.user_dir = user_dir + nxt_modules = os.path.join(user_dir, 'scripts/addons/modules') + self.modules_dir = nxt_modules.replace(os.sep, '/') + self.instance = None + self.nxt_qapp = QtWidgets.QApplication.instance() + + @classmethod + def setup(cls): + self = cls() + bpy.ops.preferences.addon_disable(module='nxt_' + self.name) + addons_dir = os.path.join(self.user_dir, 'scripts/addons') + integration_filepath = self.get_integration_filepath() + shutil.copy(integration_filepath, addons_dir) + bpy.ops.preferences.addon_enable(module='nxt_' + self.name) + + @classmethod + def update(cls): + self = cls() + og_cwd = os.getcwd() + os.chdir(self.modules_dir) + super(Blender, self).update() + os.chdir(og_cwd) + + @classmethod + def launch_nxt(cls): + self = cls() + os.environ[NXT_DCC_ENV_VAR] = 'blender' + global __NXT_INTEGRATION__ + if not __NXT_INTEGRATION__: + __NXT_INTEGRATION__ = self + else: + self = __NXT_INTEGRATION__ + if self.instance: + self.instance.show() + return + if not self.nxt_qapp: + self.nxt_qapp = nxt_editor._new_qapp() + nxt_win = nxt_editor.show_new_editor(start_rpc=False) + else: + nxt_win = nxt_editor.show_new_editor(start_rpc=False) + if 'win32' in sys.platform: + # gives nxt it's own entry on taskbar + nxt_win.setWindowFlags(QtCore.Qt.Window) + + def unregister_nxt(): + self.instance = None + if self.nxt_qapp: + self.nxt_qapp.quit() + self.nxt_qapp = None + + nxt_win.close_signal.connect(unregister_nxt) + nxt_win.show() + atexit.register(nxt_win.close) + self.instance = nxt_win + return self + + def quit_nxt(self): + if self.instance: + self.instance.close() + atexit.unregister(self.instance.close) + if self.nxt_qapp: + self.nxt_qapp.quit() + global __NXT_INTEGRATION__ + __NXT_INTEGRATION__ = None + + def create_context(self): + placeholder_txt = 'Blender {}.{}'.format(*bpy.app.version) + args = ['-noaudio', '--background', '--python'] + self.instance.create_remote_context(placeholder_txt, + interpreter_exe=bpy.app.binary_path, + exe_script_args=args) + diff --git a/nxt_editor/integration/blender/nxt_blender.py b/nxt_editor/integration/blender/nxt_blender.py index 7e43d8d..9702e04 100644 --- a/nxt_editor/integration/blender/nxt_blender.py +++ b/nxt_editor/integration/blender/nxt_blender.py @@ -1,28 +1,41 @@ +""" +Loosely based on the example addon from this repo: +https://github.com/robertguetzkow/blender-python-examples +""" # Builtin import os import sys +import subprocess # External -from Qt import QtCore, QtWidgets import bpy -# Internal -from nxt.constants import NXT_DCC_ENV_VAR -from nxt_editor.constants import NXT_WEBSITE -import nxt_editor.main_window -import nxt_editor -os.environ[NXT_DCC_ENV_VAR] = 'blender' +try: + # External + from Qt import QtCore, QtWidgets + # Internal + from nxt_editor.constants import NXT_WEBSITE + from nxt_editor.integration import blender + nxt_installed = True +except ImportError: + nxt_installed = False + NXT_WEBSITE = 'https://nxt-dev.github.io/' + +nxt_package_name = 'nxt-editor' bl_info = { "name": "NXT Blender", "blender": (2, 80, 0), - "version": (0, 1, 0), + "version": (0, 2, 0), "location": "NXT > Open Editor", "wiki_url": "https://nxt-dev.github.io/", "tracker_url": "https://github.com/nxt-dev/nxt_editor/issues", "category": "nxt", - "warning": "This is an experimental version of nxt_blender. Save early, " - "save often." + "description": "NXT is a general purpose code compositor designed for " + "rigging, scene assembly, and automation. (This is an " + "experimental version of nxt_blender. Save " + "early, save often.)", + "warning": "This addon requires installation of dependencies." } @@ -38,45 +51,32 @@ class BLENDER_PLUGIN_VERSION(object): VERSION = VERSION_STR -__NXT_INSTANCE__ = None -__NXT_CREATED_QAPP__ = None - - -class OpenNxtEditor(bpy.types.Operator): - bl_label = "Open NXT Editor" - bl_idname = "nxt.nxt_editor" +class CreateBlenderContext(bpy.types.Operator): + bl_label = "Create Remote Blender NXT Context" + bl_idname = "nxt.create_blender_context" def execute(self, context): - global __NXT_INSTANCE__ - global __NXT_CREATED_QAPP__ - if __NXT_INSTANCE__: - __NXT_INSTANCE__.show() - return - if not __NXT_CREATED_QAPP__: - nxt_win = nxt_editor.launch_editor() + global nxt_installed + if nxt_installed: + b = blender.__NXT_INTEGRATION__ + if not b: + b = blender.Blender.launch_nxt() + b.create_context() else: - nxt_win = nxt_editor.show_new_editor() - if 'win32' in sys.platform: - # gives nxt it's own entry on taskbar - nxt_win.setWindowFlags(QtCore.Qt.Window) - - def unregister_nxt(): - global __NXT_INSTANCE__ - __NXT_INSTANCE__ = None - - nxt_win.close_signal.connect(unregister_nxt) - nxt_win.show() - __NXT_INSTANCE__ = nxt_win + show_dependency_warning() return {'FINISHED'} -class UpdateNxt(bpy.types.Operator): - bl_label = "Update NXT" - bl_idname = "nxt.nxt_update" +class OpenNxtEditor(bpy.types.Operator): + bl_label = "Open NXT Editor" + bl_idname = "nxt.nxt_editor" def execute(self, context): - import nxt_editor.integration - nxt_editor.integration.Blender.update() + global nxt_installed + if nxt_installed: + blender.Blender.launch_nxt() + else: + show_dependency_warning() return {'FINISHED'} @@ -97,8 +97,11 @@ def draw(self, context): layout = self.layout layout.operator("nxt.nxt_editor", text="Open Editor") layout.separator() - layout.operator("nxt.nxt_update", text="Update NXT (Requires Blender " - "Restart)") + layout.operator("nxt.nxt_update_dependencies", + text="Update NXT (Requires Blender Restart)") + layout.separator() + layout.operator('nxt.create_blender_context', text='Create Blender ' + 'Context') layout.separator() layout.operator("nxt.nxt_about", text="About") @@ -106,34 +109,148 @@ def menu_draw(self, context): self.layout.menu("TOPBAR_MT_nxt") -nxt_menu_operators = (TOPBAR_MT_nxt, OpenNxtEditor) +class NxtInstallDependencies(bpy.types.Operator): + bl_idname = 'nxt.nxt_install_dependencies' + bl_label = "Install NXT dependencies" + bl_description = ("Downloads and installs the required python packages " + "for NXT. Internet connection is required. " + "Blender may have to be started with elevated " + "permissions in order to install the package. " + "Alternatively you can pip install nxt-editor into your " + "Blender Python environment.") + bl_options = {"REGISTER", "INTERNAL"} + + @classmethod + def poll(cls, context): + global nxt_installed + return not nxt_installed + + def execute(self, context): + success = False + environ_copy = dict(os.environ) + environ_copy["PYTHONNOUSERSITE"] = "1" + pkg = 'nxt-editor' + pkg = 'D:/Projects/nxt_editor' + try: + subprocess.run([sys.executable, "-m", "pip", "install", pkg], + check=True, env=environ_copy) + except subprocess.CalledProcessError as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + if not success: + self.report({"INFO"}, 'Please restart Blender to ' + 'finish installing NXT.') + return {"FINISHED"} + + +class NxtUpdateDependencies(bpy.types.Operator): + bl_idname = 'nxt.nxt_update_dependencies' + bl_label = "Update NXT dependencies" + bl_description = ("Downloads and updates the required python packages " + "for NXT. Internet connection is required. " + "Blender may have to be started with elevated " + "permissions in order to install the package. " + "Alternatively you can pip install -U nxt-editor into " + "your Blender Python environment.") + bl_options = {"REGISTER", "INTERNAL"} + + @classmethod + def poll(cls, context): + global nxt_installed + return nxt_installed + + def execute(self, context): + try: + blender.Blender._update_package('nxt-editor') + except subprocess.CalledProcessError as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + self.report({"INFO"}, 'Please restart Blender to ' + 'finish updating NXT.') + return {"FINISHED"} + + +class NxtUninstallDependencies(bpy.types.Operator): + bl_idname = 'nxt.nxt_uninstall_dependencies' + bl_label = "Uninstall NXT dependencies" + bl_description = ("Uninstalls the NXT Python packages. " + "Blender may have to be started with elevated " + "permissions in order to install the package. " + "Alternatively you can pip uninstall nxt-editor from " + "your Blender Python environment.") + bl_options = {"REGISTER", "INTERNAL"} + + @classmethod + def poll(cls, context): + global nxt_installed + return nxt_installed + + def execute(self, context): + try: + blender.Blender.uninstall() + except subprocess.CalledProcessError as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + self.report({"INFO"}, 'Please restart Blender to ' + 'finish uninstalling NXT dependencies.') + return {"FINISHED"} + + +class NxtDependenciesPreferences(bpy.types.AddonPreferences): + bl_idname = __name__ + + def draw(self, context): + layout = self.layout + layout.operator(NxtInstallDependencies.bl_idname, icon="PLUGIN") + layout.operator(NxtUpdateDependencies.bl_idname, icon="SCRIPT") + layout.operator(NxtUninstallDependencies.bl_idname, icon="PANEL_CLOSE") + + +def show_dependency_warning(): + + def draw(self, context): + layout = self.layout + lines = [ + f"Please install the missing dependencies for the NXT add-on.", + "1. Open the preferences (Edit > Preferences > Add-ons).", + f"2. Search for the \"{bl_info.get('name')}\" add-on.", + "3. Open the details section of the add-on.", + f"4. Click on the \"{NxtInstallDependencies.bl_label}\" button.", + "This will download and install the missing Python packages. " + "You man need to start Blender with elevated permissions", + f"Alternatively you can pip install \"{nxt_package_name}\" into " + f"your Blender Python environment." + ] + + for line in lines: + layout.label(text=line) + bpy.context.window_manager.popup_menu(draw, title='NXT Warning!', + icon="ERROR") + + +nxt_operators = (TOPBAR_MT_nxt, OpenNxtEditor, NxtUpdateDependencies, + NxtUninstallDependencies, NxtDependenciesPreferences, + NxtInstallDependencies, CreateBlenderContext) def register(): - global __NXT_CREATED_QAPP__ - existing = QtWidgets.QApplication.instance() - if existing: - __NXT_CREATED_QAPP__ = False - else: - __NXT_CREATED_QAPP__ = True - nxt_editor._new_qapp() - bpy.utils.register_class(TOPBAR_MT_nxt) - bpy.utils.register_class(OpenNxtEditor) + global nxt_installed + for cls in nxt_operators: + bpy.utils.register_class(cls) bpy.utils.register_class(AboutNxt) - bpy.utils.register_class(UpdateNxt) bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR_MT_nxt.menu_draw) def unregister(): - global __NXT_CREATED_QAPP__ - if __NXT_CREATED_QAPP__: - QtWidgets.QApplication.instance().quit() - __NXT_CREATED_QAPP__ = False + try: + if blender.__NXT_INTEGRATION__: + blender.__NXT_INTEGRATION__.quit_nxt() + except NameError: + pass bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR_MT_nxt.menu_draw) - bpy.utils.unregister_class(TOPBAR_MT_nxt) - bpy.utils.unregister_class(OpenNxtEditor) + for cls in nxt_operators: + bpy.utils.unregister_class(cls) bpy.utils.unregister_class(AboutNxt) - bpy.utils.unregister_class(UpdateNxt) if __name__ == "__main__": diff --git a/nxt_editor/main_window.py b/nxt_editor/main_window.py index 8427945..d65018b 100644 --- a/nxt_editor/main_window.py +++ b/nxt_editor/main_window.py @@ -337,15 +337,15 @@ def set_waiting_cursor(state=True): @staticmethod def create_remote_context(place_holder_text='', interpreter_exe=sys.executable, - context_graph=None): + context_graph=None, exe_script_args=()): cur_context = nxt.remote.contexts.get_current_context_exe_name() pop_up = QtWidgets.QDialog() pop_up.setWindowTitle('Create context for "{}"'.format(cur_context)) v_layout = QtWidgets.QVBoxLayout() pop_up.setLayout(v_layout) label = QtWidgets.QPlainTextEdit() - info = ('Create context for "{}" your host ' - 'python interpreter\n' + info = ('Create remote context for your host ' + 'Python interpreter/DCC\n' 'Type your desired name in the box below ' 'and click create.'.format(cur_context)) label.setPlainText(info) @@ -358,6 +358,7 @@ def create_remote_context(place_holder_text='', v_layout.addLayout(h_layout) name = QtWidgets.QLineEdit() name.setPlaceholderText(str(place_holder_text)) + name.setText(str(place_holder_text)) create_button = QtWidgets.QPushButton('Create!') h_layout.addWidget(name) h_layout.addWidget(create_button) @@ -366,7 +367,8 @@ def do_create(): try: nxt.create_context(name.text(), interpreter_exe=interpreter_exe, - context_graph=context_graph) + context_graph=context_graph, + exe_script_args=exe_script_args) pop_up.close() except (IOError, NameError) as e: info = str(e) From 0b2e75613ccea50dab1e5580b3caf6a8615fae4c Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Mon, 25 Jan 2021 08:17:57 -0500 Subject: [PATCH 03/23] * Bug fix: When changing focus to and from NXT it was possible to get into a state where zooming became impossible. --- nxt_editor/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nxt_editor/main_window.py b/nxt_editor/main_window.py index 8427945..6171c7e 100644 --- a/nxt_editor/main_window.py +++ b/nxt_editor/main_window.py @@ -327,6 +327,12 @@ def handle_rpc_tailing_signals(self, state): self.rpc_log_tail.new_text.disconnect(raw_write_func) self.rpc_log_tail.new_text.disconnect(rich_write_func) + def event(self, event): + if event.type() == QtCore.QEvent.WindowDeactivate: + self._held_keys = [] + self.zoom_keys_down = False + return super(MainWindow, self).event(event) + @staticmethod def set_waiting_cursor(state=True): if state: From 7a1f80bc100a2daf5fa3aaa146fd7a173156865a Mon Sep 17 00:00:00 2001 From: MichaelAldrich <11843596+MichaelAldrich@users.noreply.github.com> Date: Tue, 26 Jan 2021 17:18:16 -0600 Subject: [PATCH 04/23] * Hookup closing signals and update both packages --- .../integration/unreal/Content/Python/init_unreal.py | 2 +- nxt_editor/integration/unreal/__init__.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nxt_editor/integration/unreal/Content/Python/init_unreal.py b/nxt_editor/integration/unreal/Content/Python/init_unreal.py index 318c919..95e915f 100644 --- a/nxt_editor/integration/unreal/Content/Python/init_unreal.py +++ b/nxt_editor/integration/unreal/Content/Python/init_unreal.py @@ -26,7 +26,7 @@ def install_nxt_to_interpreter(): def update_installed_nxt(): subprocess.check_call([get_python_exc_path(), '-m', 'pip', - 'install', '--upgrade', 'nxt-editor']) + 'install', '--upgrade', 'nxt-editor', 'nxt-core']) def uninstall_nxt_from_interpreter(): subprocess.check_call([get_python_exc_path(), '-m', 'pip', diff --git a/nxt_editor/integration/unreal/__init__.py b/nxt_editor/integration/unreal/__init__.py index 3d7277e..8524988 100644 --- a/nxt_editor/integration/unreal/__init__.py +++ b/nxt_editor/integration/unreal/__init__.py @@ -20,11 +20,14 @@ def launch_nxt_in_ue(): unreal.log('Found existing QApp') else: unreal.log('Building new QApp for nxt') - nxt_editor._new_qapp() + existing = nxt_editor._new_qapp() global __NXT_WINDOW if __NXT_WINDOW: __NXT_WINDOW.show() __NXT_WINDOW.raise_() else: - __NXT_WINDOW = nxt_editor.show_new_editor() \ No newline at end of file + __NXT_WINDOW = nxt_editor.show_new_editor() + + __NXT_WINDOW.close_signal.connect(existing.quit) + atexit.register(__NXT_WINDOW.close) \ No newline at end of file From 2aa9fe4d3cef230f8fe6f84cb3dadc5036a52a11 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 27 Jan 2021 00:12:21 -0600 Subject: [PATCH 05/23] Fix missing import and change close signal connection. --- nxt_editor/integration/unreal/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxt_editor/integration/unreal/__init__.py b/nxt_editor/integration/unreal/__init__.py index 8524988..063c64b 100644 --- a/nxt_editor/integration/unreal/__init__.py +++ b/nxt_editor/integration/unreal/__init__.py @@ -1,5 +1,6 @@ # Built-in import os +import atexit # External import unreal @@ -28,6 +29,6 @@ def launch_nxt_in_ue(): __NXT_WINDOW.raise_() else: __NXT_WINDOW = nxt_editor.show_new_editor() - - __NXT_WINDOW.close_signal.connect(existing.quit) + + __NXT_WINDOW.close_signal.connect(existing.exit) atexit.register(__NXT_WINDOW.close) \ No newline at end of file From a0a09d99901ac28b55609f22ce1b98b3d16624c1 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Wed, 27 Jan 2021 09:10:22 -0500 Subject: [PATCH 06/23] Removed explicit pip install path Addressing notes --- nxt_editor/__init__.py | 4 ++-- nxt_editor/integration/__init__.py | 21 +++++++++++++++---- nxt_editor/integration/blender/nxt_blender.py | 3 +-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/nxt_editor/__init__.py b/nxt_editor/__init__.py index 5682c0f..c96437e 100644 --- a/nxt_editor/__init__.py +++ b/nxt_editor/__init__.py @@ -95,7 +95,7 @@ def _new_qapp(): return app -def launch_editor(paths=None, start_rpc=True, call_exec=True): +def launch_editor(paths=None, start_rpc=True): """Launch an instance of the editor. Will attach to existing QApp if found, otherwise will create and open one. """ @@ -106,7 +106,7 @@ def launch_editor(paths=None, start_rpc=True, call_exec=True): app = _new_qapp() instance = show_new_editor(paths, start_rpc) app.setActiveWindow(instance) - if not existing and call_exec: + if not existing: app.exec_() return instance diff --git a/nxt_editor/integration/__init__.py b/nxt_editor/integration/__init__.py index 2abb891..5b768b2 100644 --- a/nxt_editor/integration/__init__.py +++ b/nxt_editor/integration/__init__.py @@ -100,13 +100,26 @@ def update(self): if self.check_for_nxt_editor(): self._update_package('nxt-editor') - @ staticmethod - def uninstall(): + @staticmethod + def _uninstall_package(package_name): + """Calls a subprocess to pip uninstall the given package name. Will + NOT prompt the user to confrim uninstall. + + :param package_name: pip package name + :raises: subprocess.CalledProcessError + :return: None + """ environ_copy = dict(os.environ) environ_copy["PYTHONNOUSERSITE"] = "1" subprocess.run([sys.executable, "-m", "pip", "uninstall", - "nxt-editor", "nxt-core"], check=True, env=environ_copy) - print('Please restart your DCC or Python interpreter') + package_name, '-y'], check=True, env=environ_copy) + + def uninstall(self): + if self.check_for_nxt_core(): + self._uninstall_package('nxt-core') + if self.check_for_nxt_editor(): + self._uninstall_package('nxt-editor') + # print('Please restart your DCC or Python interpreter') def launch_nxt(self): raise NotImplementedError('Your DCC needs it own nxt launch method.') diff --git a/nxt_editor/integration/blender/nxt_blender.py b/nxt_editor/integration/blender/nxt_blender.py index 9702e04..f99857c 100644 --- a/nxt_editor/integration/blender/nxt_blender.py +++ b/nxt_editor/integration/blender/nxt_blender.py @@ -130,7 +130,6 @@ def execute(self, context): environ_copy = dict(os.environ) environ_copy["PYTHONNOUSERSITE"] = "1" pkg = 'nxt-editor' - pkg = 'D:/Projects/nxt_editor' try: subprocess.run([sys.executable, "-m", "pip", "install", pkg], check=True, env=environ_copy) @@ -187,7 +186,7 @@ def poll(cls, context): def execute(self, context): try: - blender.Blender.uninstall() + blender.Blender().uninstall() except subprocess.CalledProcessError as e: self.report({"ERROR"}, str(e)) return {"CANCELLED"} From 330b4351812dd12d31947993140a405090318c37 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 2 Feb 2021 08:53:55 -0500 Subject: [PATCH 07/23] * Bug fix: Fixed bug that caused the invalid runtime node dialog to spawn every time a snippet was run. Fixes #155 --- nxt_editor/stage_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxt_editor/stage_model.py b/nxt_editor/stage_model.py index f5d38f3..f2e6633 100644 --- a/nxt_editor/stage_model.py +++ b/nxt_editor/stage_model.py @@ -2544,14 +2544,15 @@ def execute_snippet(self, code_string, node_path, rt_layer=None, logger.warning('No code to execute!') return self.about_to_execute.emit(True) - rt = rt_layer + rt_layer = rt_layer or self.current_rt_layer np = [node_path] - valid, bad_paths = self.validate_runtime_layer(rt_layer=rt, + valid, bad_paths = self.validate_runtime_layer(rt_layer=rt_layer, node_paths=np) if not rt_layer or not valid: new_rt = self.prompt_runtime_rebuild(must_rebuild=bool(bad_paths)) if new_rt: rt_layer = new_rt + self.current_rt_layer = new_rt elif not rt_layer: return if not rt_layer or not hasattr(rt_layer, '_console'): From 973a6eacce61d97b4d880bd3b7fc38cea5d916fa Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 2 Feb 2021 11:07:08 -0500 Subject: [PATCH 08/23] * Bug fix: When a workflow button executed its descendants, nodes under a disabled descendant would still execute. This was considered unexpected as it did not follow the "normal" execute order rules. --- nxt_editor/dockwidgets/widget_builder.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index 7541916..cd41a8c 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -18,6 +18,7 @@ from nxt_editor import user_dir from nxt_editor.dockwidgets.dock_widget_base import DockWidgetBase from nxt import DATA_STATE, nxt_path +from nxt import nxt_node logger = logging.getLogger(nxt_editor.LOGGER_NAME) @@ -881,15 +882,22 @@ def execute_node_path(self, node_path, attr_name): # filter out widget nodes filtered_nodes = [] - widget_descendants = [] + dont_run = [] for path in descendants: widget_type = get_widget_type(path, self.stage_model) if widget_type: des = self.stage_model.comp_layer.descendants(path, ordered=True) - widget_descendants += des - if not widget_type and path not in widget_descendants: - filtered_nodes.append(path) + dont_run += des + if not widget_type and path not in dont_run: + enabled = self.stage_model.get_node_enabled(path) + anc_enabled = self.stage_model.get_node_ancestor_enabled(path) + if enabled and anc_enabled: + filtered_nodes.append(path) + else: + des = self.stage_model.comp_layer.descendants(path, + ordered=True) + dont_run += [path] + des node_paths = [exec_path] + filtered_nodes if user_dir.user_prefs.get(RECOMP_PREF, True): From 4af8384ec9b8a8bb12050b735e7c6d612d7cce98 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Feb 2021 17:16:52 -0600 Subject: [PATCH 09/23] * fix unreal plugin build paths --- build/make_unreal_plugin.nxt | 60 ++++++++++++++++++++++++++++++++++++ build/release.nxt | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 build/make_unreal_plugin.nxt diff --git a/build/make_unreal_plugin.nxt b/build/make_unreal_plugin.nxt new file mode 100644 index 0000000..638ef59 --- /dev/null +++ b/build/make_unreal_plugin.nxt @@ -0,0 +1,60 @@ +{ + "version": "1.17", + "alias": "make_unreal_plugin", + "color": "#879fda", + "mute": false, + "solo": false, + "meta_data": { + "positions": { + "/make_plugin": [ + -144.0, + -49.0 + ] + } + }, + "nodes": { + "/": { + "code": [ + "import os", + "import shutil" + ] + }, + "/make_plugin": { + "start_point": true, + "attrs": { + "icon_path": { + "type": "raw", + "value": "${file::../nxt_editor/resources/icons/nxt_128.png}" + }, + "result_dir": { + "type": "raw", + "value": "${path::nxt_unreal}" + }, + "unreal_integration_dir": { + "type": "raw", + "value": "${file::../nxt_editor/integration/unreal}" + }, + "uplugin_path": { + "type": "raw", + "value": "${file::${unreal_integration_dir}/nxt_unreal.uplugin}" + } + }, + "code": [ + "if os.path.exists('${result_dir}'):", + " shutil.rmtree('${result_dir}')", + "shutil.copytree('${unreal_integration_dir}', '${result_dir}')", + "resources_dir = '${result_dir}/Resources'", + "os.makedirs(resources_dir)", + "target_icon_path = os.path.join(resources_dir, 'Icon128.png')", + "shutil.copyfile('${icon_path}', target_icon_path)", + "leftover_init = os.path.join(self.result_dir, '__init__.py')", + "leftover_pycache = os.path.join(self.result_dir, '__pycache__')", + "os.remove(leftover_init)", + "try:", + " shutil.rmtree(leftover_pycache)", + "except FileNotFoundError:", + " pass" + ] + } + } +} \ No newline at end of file diff --git a/build/release.nxt b/build/release.nxt index 2a0840a..be60d4d 100644 --- a/build/release.nxt +++ b/build/release.nxt @@ -5,7 +5,7 @@ "mute": false, "solo": false, "references": [ - "H:/Projects/nxt/nxt_editor/build/make_unreal_plugin.nxt", + "make_unreal_plugin.nxt", "make_maya_plugin.nxt", "make_blender_plugin.nxt", "../../nxt/build/release.nxt" From 712e238686de96299abaeb290c98fdac1c6f2539 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Thu, 4 Feb 2021 09:48:26 -0500 Subject: [PATCH 10/23] + Code editor now displays a "Double Click To Edit" message. Fixes #133 * Bug fix: Code editor wasn't displaying the data state in the top right like it used to. --- nxt_editor/dockwidgets/code_editor.py | 39 +++++++++++++++++---------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/nxt_editor/dockwidgets/code_editor.py b/nxt_editor/dockwidgets/code_editor.py index 6c7476a..b9dc033 100644 --- a/nxt_editor/dockwidgets/code_editor.py +++ b/nxt_editor/dockwidgets/code_editor.py @@ -319,19 +319,26 @@ def update_editor(self, node_list=()): # for faster updates. And avoid that early exit check at the top. get_code = self.stage_model.get_node_code_string code_string = get_code(self.node_path, - self.stage_model.data_state, - self.stage_model.comp_layer) + self.stage_model.data_state, + self.stage_model.comp_layer) cached_state = self.stage_model.data_state == DATA_STATE.CACHED + self.actual_display_state = DATA_STATE.RAW if code_string and cached_state: self.actual_display_state = DATA_STATE.CACHED elif not code_string and cached_state: self.actual_display_state = DATA_STATE.RAW code_string = get_code(self.node_path, DATA_STATE.RAW, - self.stage_model.comp_layer) + self.stage_model.comp_layer) + else: + self.actual_display_state = self.stage_model.data_state if self.editing_active: - self.overlay_widget.main_color = self.overlay_widget.base_color + self.overlay_widget.hide() + elif self.code_is_local: + self.overlay_widget.main_color = None + self.overlay_widget.show() else: self.overlay_widget.main_color = self.overlay_widget.ext_color + self.overlay_widget.show() self.overlay_widget.update() self.editor.verticalScrollBar().blockSignals(True) self.cached_code_lines = code_string.split('\n') @@ -397,11 +404,6 @@ def update_code_is_local(self): self.editor.current_line_highlight = not is_local self.overlay_widget.main_color = self.overlay_widget.base_color self.update_background() - if self.editing_active or is_local: - self.overlay_widget.hide() - else: - self.overlay_widget.main_color = self.overlay_widget.ext_color - self.overlay_widget.show() self.code_is_local = is_local def localize_code(self): @@ -1280,26 +1282,35 @@ def __init__(self, parent=None): self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) self.data_state = '' + self.click_msg = 'Double Click To Edit' def paintEvent(self, event): painter = QtGui.QPainter() painter.begin(self) + painter.setFont(QtGui.QFont("Roboto", 14)) + font_metrics = QtGui.QFontMetrics(painter.font()) painter.setRenderHint(QtGui.QPainter.Antialiasing) # actual_display_state code_editor = self._parent.ce_widget model = code_editor.stage_model self.data_state = code_editor.actual_display_state painter.setPen(QtCore.Qt.white) - font_matrics = QtGui.QFontMetrics(painter.font()) - offset = font_matrics.boundingRect(self.data_state).width() - offset += painter.font().pointSize() + # Draw top right data state text + offset = font_metrics.boundingRect(self.data_state).width() + offset += painter.font().pointSize() * 1.5 painter.drawText(self.rect().right() - offset, painter.font().pointSize() * 1.5, self.data_state) + # Draw center message text + msg_offset = font_metrics.boundingRect(self.click_msg).width() + msg_offset += painter.font().pointSize() + pos = (self.rect().center().x() - (msg_offset*.5), + self.rect().center().y()) + painter.drawText(*pos, self.click_msg) painter.setCompositionMode(QtGui.QPainter.CompositionMode_Darken) - path = QtGui.QPainterPath() path.addRoundedRect(QtCore.QRectF(self.rect()), 9, 9) - painter.fillPath(path, QtGui.QBrush(self.main_color)) + if self.main_color: + painter.fillPath(path, QtGui.QBrush(self.main_color)) painter.setCompositionMode(QtGui.QPainter.CompositionMode_Screen) display_is_raw = self.data_state == DATA_STATE.RAW mode_is_cache = model.data_state == DATA_STATE.CACHED From be3c18946f3064000cb336708c19d31a6370dece Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Thu, 4 Feb 2021 10:49:31 -0500 Subject: [PATCH 11/23] * Bug fix: QSignal traffic jams could cause the workflow tools to throw exceptions. * Bug fix: Clearing breakpoints would emit node's changed twice. --- nxt_editor/dockwidgets/widget_builder.py | 6 +++++- nxt_editor/stage_model.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index cd41a8c..b612400 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -40,6 +40,7 @@ def __init__(self, graph_model=None, title='Workflow Tools', parent=None, minimum_width=minimum_width, minimum_height=minimum_height) + self.updating = False self.setObjectName('Workflow Tools') self.default_title = title @@ -263,6 +264,8 @@ def get_window_title(self): return title or None def update_window(self, changed_paths=None): + if self.updating: + return if not self.isVisible(): return @@ -286,7 +289,7 @@ def update_window(self, changed_paths=None): break if not update: return - + self.updating = True # window title title = self.get_window_title() self.setWindowTitle(title or self.default_title) @@ -311,6 +314,7 @@ def update_window(self, changed_paths=None): widget=self.window_frame, items=None, parent=self) + self.updating = False def set_window_style(self): background_color = self.stage_model.get_node_attr_value( diff --git a/nxt_editor/stage_model.py b/nxt_editor/stage_model.py index f2e6633..052d282 100644 --- a/nxt_editor/stage_model.py +++ b/nxt_editor/stage_model.py @@ -2073,7 +2073,6 @@ def clear_breakpoints(self, layer=None): layer = layer or self.top_layer cmd = ClearBreakpoints(model=self, layer_path=layer.real_path) self.undo_stack.push(cmd) - self.nodes_changed.emit(cmd.prev_breaks) self.breaks_changed.emit([]) def _add_breakpoint(self, node_path, layer): From 763c2f811a1cef062bf1ea9ed66b3555f7712332 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+imlucasbrown@users.noreply.github.com> Date: Fri, 5 Feb 2021 10:26:37 -0500 Subject: [PATCH 12/23] - Removed Blender installer script as its functionality has been moved to the `nxt_blender.py` addon. --- .../integration/blender/blender_installer.py | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 nxt_editor/integration/blender/blender_installer.py diff --git a/nxt_editor/integration/blender/blender_installer.py b/nxt_editor/integration/blender/blender_installer.py deleted file mode 100644 index 878e601..0000000 --- a/nxt_editor/integration/blender/blender_installer.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import sys - -try: - import pip - _ = pip.main - _ = pip._internal.main - -except (ImportError, AttributeError): - import ensurepip - ensurepip.bootstrap() - os.environ.pop('PIP_REQ_TRACKER', None) - -try: - import nxt_editor -except ImportError: - import bpy - b_major, b_minor, b_patch = bpy.app.version - user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' - 'Foundation/Blender/' - '{}.{}'.format(b_major, b_minor)) - t = os.path.join(user_dir, 'scripts/addons/modules') - t = t.replace(os.sep, '/') - args = ['install', 'D:/Projects/nxt_editor', '--target', t] - if hasattr(pip, 'main'): - pip.main(args) - else: - pip._internal.main(args) - # The next time blender starts it will see this path automatically - cwd = os.getcwd() - os.chdir(t) - sys.path.append(t) - os.chdir(cwd) - -# Install nxt_blender addon -from nxt_editor import integration -integration.Blender.setup() From 1d9c6d0aa4fe2b3688f70380a968a9ba972df973 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Fri, 5 Feb 2021 10:38:19 -0500 Subject: [PATCH 13/23] updated Blender readme --- nxt_editor/integration/blender/README.md | 54 +++++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/nxt_editor/integration/blender/README.md b/nxt_editor/integration/blender/README.md index e7b2c04..d9215be 100644 --- a/nxt_editor/integration/blender/README.md +++ b/nxt_editor/integration/blender/README.md @@ -4,22 +4,54 @@ This is a Blender addon for nxt. Note that it will access the internet to instal ### By hand (if you're familiar with pip) 1. Locate the path to blenders Python interpreter - - In Blender, you can run `sys.exec_prefix` to find the folder containing the Python executable. + - In Blender, you can run `sys.exec_prefix` to find the folder containing the Python executable 2. Open Terminal or CMD (If you're using Windows) -3. Run: `/path/to/python.exe -m pip install nxt-editor` +3. Run: `/path/to/blender/python.exe -m pip install nxt-editor` 4. Start Blender 5. Open the addon manager (Edit > Preferences > Add-ons) -6. Click "Install" and select the `nxt_blender.py` file provided with this addon zip. -7. To launch NXT navigate the newly created `NXT` menu and select `Open Editor`. +6. Click "Install" and select the `nxt_blender.py` file provided with this addon zip +7. To launch NXT navigate the newly created `NXT` menu and select `Open Editor` -_Note: If you install NXT Blender this way the "Update" button may not work in the NXT menu._ ### Automated -1. Open Blender's script editor. -2. Drag and drop `blender_installer.py` into the script editor and click the play button or press `Alt+P` (default run script hotkey) -3. The installation may take a minute or two, during this time Blender will be unresponsive. - - Optionally open the System Console window before running the script, so you can see what's happening. +1. Open the addon manager (Edit > Preferences > Add-ons) +2. Click "Install" and select the `nxt_blender.py` file provided with this addon zip +3. Enable the `NXT Blender` and twirl down the addon preferences +3. Click `Install NXT dependencies` + - The installation may take a minute or two, during this time Blender will be unresponsive + - Optionally open the console window before running the script, so you can see what's happening +4. Restart Blender # Usage -- Ensure the `NXT Blender` addon is enabled. -- To launch NXT navigate the newly created `NXT` menu and select `Open Editor`. \ No newline at end of file +- Ensure the `NXT Blender` addon is enabled +- To launch NXT navigate the newly created `NXT` menu and select `Open Editor` + +# Updating +### By hand (if you're familiar with pip) +1. In terminal or cmd run: `/path/to/blender/python.exe -m pip install -U nxt-editor nxt` +2. Restart Blender + +### Automated +1. Open the addon manager (Edit > Preferences > Add-ons) +3. Locate the `NXT Blender` and twirl down the addon preferences +3. Click `Update NXT dependencies` +4. Restart Blender + +_or_ + +1. Navigate to the NXT menu +2. Click `Update NXT` +3. Restart Blender + +# Uninstall +### By hand (if you're familiar with pip) +1. Open the addon manager (Edit > Preferences > Add-ons) +2. Locate the `NXT Blender` and twirl down the addon preferences +3. Click 'Remove' +1. In terminal or cmd run: `/path/to/blender/python.exe -m pip uninstall nxt-editor nxt -y` + +### Automated +1. Open the addon manager (Edit > Preferences > Add-ons) +3. Locate the `NXT Blender` and twirl down the addon preferences +3. Click `Uninstall NXT dependencies` +3. When that finishes, click the 'Remove' button \ No newline at end of file From 56b6f95efde7a9e3ac060b09d46e93bed7eb7609 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Sun, 7 Feb 2021 16:19:40 -0500 Subject: [PATCH 14/23] + Added collapse/un-collapse animations. * Improved visibility of collapse arrows. * Bug fix: Collapsed nodes from the same layer as ancestor parent wouldn't generate a collapse arrow. * Bug fix: Sometimes an exception about non-iterable object types would be raised When updating the workflow tools window. --- nxt_editor/dockwidgets/widget_builder.py | 5 +- nxt_editor/node_graphics_item.py | 135 ++++++++++++++++++++--- nxt_editor/stage_model.py | 2 +- nxt_editor/stage_view.py | 27 +++-- 4 files changed, 145 insertions(+), 24 deletions(-) diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index b612400..5147186 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -1,6 +1,7 @@ # Built-in import logging import ast +import collections.abc # External from Qt import QtWidgets @@ -275,7 +276,9 @@ def update_window(self, changed_paths=None): return update = False if changed_paths else True - for path in (changed_paths or []): + if not isinstance(changed_paths, collections.abc.Iterable): + changed_paths = [] + for path in changed_paths: node_path, _ = nxt_path.path_attr_partition(path) is_widget = get_widget_type(node_path, self.stage_model) if is_widget: diff --git a/nxt_editor/node_graphics_item.py b/nxt_editor/node_graphics_item.py index e448210..c37f37c 100644 --- a/nxt_editor/node_graphics_item.py +++ b/nxt_editor/node_graphics_item.py @@ -22,7 +22,7 @@ logger = logging.getLogger(nxt_editor.LOGGER_NAME) -class NodeGraphicsItem(QtWidgets.QGraphicsItem): +class NodeGraphicsItem(QtWidgets.QGraphicsObject): """The graphics item used to represent nodes in the graph. Contains instances of NodeGraphicsPlug for each attribute on the associated node. Contains functionality for arranging children into stacks. @@ -44,11 +44,12 @@ def __init__(self, model, node_path, view): # item settings self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) - #self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache) + self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | - QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges) + QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges | + QtWidgets.QGraphicsItem.ItemNegativeZStacksBehindParent) self.setAcceptHoverEvents(True) # draw settings @@ -85,6 +86,97 @@ def __init__(self, model, node_path, view): # draw node self.update_from_model() + # Setup groups + # In + self.in_anim_group = QtCore.QParallelAnimationGroup() + self.in_anim_group.finished.connect(self.finished_anim) + # Out + self.out_anim_group = QtCore.QParallelAnimationGroup() + self.out_anim_group.finished.connect(self.finished_anim) + + def _setup_anim_properties(self): + # Position anim property + self.pos_anim = QtCore.QPropertyAnimation(self, b"pos", self) + # Set graphics effect + effect = QtWidgets.QGraphicsOpacityEffect(self) + effect.setOpacity(1) + self.setGraphicsEffect(effect) + # Opacity anim property + self.opacity_anim = QtCore.QPropertyAnimation(effect, b"opacity", + effect) + # Lower power caching + self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache) + + def setup_in_anim(self): + self._setup_anim_properties() + self.in_anim_group.addAnimation(self.pos_anim) + self.in_anim_group.addAnimation(self.opacity_anim) + + def setup_out_anim(self): + self._setup_anim_properties() + self.out_anim_group.addAnimation(self.pos_anim) + self.out_anim_group.addAnimation(self.opacity_anim) + + def finished_anim(self): + self.setGraphicsEffect(None) + self.in_anim_group.clear() + self.out_anim_group.clear() + self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + self.view.update() + self.view._animating.remove(self) + + def get_is_animating(self): + i = self.in_anim_group.State.Running == self.in_anim_group.state() + o = self.out_anim_group.State.Running == self.out_anim_group.state() + return i or o + + def anim_into_place(self, end_pos): + if self.get_is_animating(): + return + if end_pos == self.pos(): + return + self.view._animating.append(self) + self.setup_in_anim() + if not self.view.do_animations: + self.setPos(end_pos) + self.in_anim_group.finished.emit() + return + + self.opacity_anim.setStartValue(0) + self.opacity_anim.setEndValue(1) + self.opacity_anim.setDuration(80) + + curve = QtCore.QEasingCurve(QtCore.QEasingCurve.OutBack) + curve.setAmplitude(.8) + self.pos_anim.setEasingCurve(curve) + + self.pos_anim.setDuration(100) + self.pos_anim.setEndValue(end_pos) + self.in_anim_group.start() + + def anim_out(self): + if self.get_is_animating(): + return + self.view._animating.append(self) + self.setup_out_anim() + if not self.view.do_animations: + self.out_anim_group.finished.emit() + return + self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache) + self.opacity_anim.setStartValue(1) + self.opacity_anim.setEndValue(0) + self.opacity_anim.setDuration(80) + + self.pos_anim.setDuration(80) + self.pos_anim.setEasingCurve(QtCore.QEasingCurve.Linear) + x_move = self.stack_offset * -1 * .5 + if not self.parentItem() or not self.parentItem().parentItem(): + y_move = 0.0 + else: + y_move = (self.parentItem().boundingRect().height() * -1.0) * .5 + self.pos_anim.setEndValue(QtCore.QPointF(x_move, y_move)) + self.out_anim_group.start() + def update_color(self): layers = self.model.get_layers_with_opinion(self.node_path) n_colors = [] @@ -160,7 +252,7 @@ def itemChange(self, change, value): ml = QtWidgets.QApplication.mouseButtons() == QtCore.Qt.LeftButton shift = QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier force_snap = self.view.alignment_actions.snap_action.isChecked() - if (ml & shift) or force_snap: + if (ml & shift) or force_snap and not self.get_is_animating(): value = self.closest_grid_point(value) return value @@ -372,7 +464,7 @@ def draw_title(self, painter): center_offset = (arrow_width * (num * .5) - arrow_width * .5) cur_offset = (i * arrow_width) pos = ((self.max_width * .5) + center_offset - cur_offset) - arrow.setPos(pos, self.title_rect_height) + arrow.setPos(pos, self.boundingRect().height()) self.collapse_arrows += [arrow] i += 1 @@ -721,9 +813,8 @@ def arrange_descendants(self): children_paths = self.model.get_children(self.node_path, ordered=True, include_implied=True) prev_y = 0 - offset_z = len(children_paths) prev_child = None - index = 0 + index = 1 for child_path in children_paths: child = self.view.get_node_graphic(child_path) if not child: @@ -737,11 +828,9 @@ def arrange_descendants(self): else: y = self.get_selection_rect().height() y += prev_y - child.setPos(self.stack_offset, y) - child.setZValue(offset_z) - + new_pos = QtCore.QPointF(self.stack_offset, y) + child.anim_into_place(new_pos) prev_y = y - offset_z -= 1 child.arrange_descendants() prev_child = child index += 1 @@ -931,6 +1020,7 @@ def __init__(self, parent=None, filled=False, color=None): is_str = isinstance(self.color, str) if is_str: self.color = QtGui.QColor(self.color) + self.setZValue(30) def boundingRect(self): """Override of QtWidgets.QGraphicsItem boundingRect. If this rectangle @@ -946,18 +1036,35 @@ def paint(self, painter, option, widget): if self.filled: brush = QtGui.QBrush(self.color) painter.setBrush(brush) + if self.color.lightness() < 100: + pen_color = QtGui.QColor(self.color.lighter(300)) + else: + pen_color = QtGui.QColor(self.color.darker(300)) + pen = QtGui.QPen(pen_color) + pen.setJoinStyle(QtCore.Qt.RoundJoin) painter.setPen(QtCore.Qt.NoPen) else: - painter.setBrush(QtCore.Qt.NoBrush) - painter.setPen(QtCore.Qt.white) + brush = QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.Dense6Pattern) + painter.setBrush(brush) + pen = QtGui.QPen(QtCore.Qt.white) + pen.setStyle(QtCore.Qt.DotLine) + painter.setPen(pen) # draw triangle points = [ - QtCore.QPointF(0-(self.width*.5), 0), + QtCore.QPointF(0 - (self.width * .5), 0), QtCore.QPointF(self.width*.5, 0), QtCore.QPointF(0, self.height) ] painter.drawPolygon(points) + # Draw outline + if self.filled: + painter.setBrush(QtCore.Qt.NoBrush) + painter.setPen(pen) + painter.drawLine(0 - (self.width * .5), 0, + 0, self.height) + painter.drawLine(0, self.height, + self.width * .5, 0) def itemChange(self, change, value): """Override of QtWidgets.QGraphicsItem itemChange.""" diff --git a/nxt_editor/stage_model.py b/nxt_editor/stage_model.py index 052d282..6a93f8d 100644 --- a/nxt_editor/stage_model.py +++ b/nxt_editor/stage_model.py @@ -748,7 +748,7 @@ def get_descendant_colors(self, base_path): base_node = self.comp_layer.lookup(base_path) if base_node is None: return layer_colors - layers = [self.get_node_source_layer(base_path, self.comp_layer)] + layers = [] des = self.get_descendants(base_path, self.comp_layer, True) for d in des: node = self.comp_layer.lookup(d) diff --git a/nxt_editor/stage_view.py b/nxt_editor/stage_view.py index 14c9fef..849aaab 100644 --- a/nxt_editor/stage_view.py +++ b/nxt_editor/stage_view.py @@ -38,6 +38,8 @@ class StageView(QtWidgets.QGraphicsView): def __init__(self, model, parent=None): super(StageView, self).__init__(parent=parent) self.main_window = parent + self.do_animations = True + self._animating = [] # EXEC ACTIONS self.exec_actions = parent.execute_actions self.addActions(self.exec_actions.actions()) @@ -294,7 +296,10 @@ def draw_graph(self, dirty): else: node_paths = self.model.get_descendants(nxt_path.WORLD, include_implied=True) + og_do_anims = self.do_animations + self.do_animations = False self.handle_nodes_changed(node_paths) + self.do_animations = og_do_anims def draw_node(self, node_path): graphic = self.get_node_graphic(node_path) @@ -1134,6 +1139,7 @@ def on_model_selection_changed(self, new_selection): def handle_nodes_changed(self, node_paths): updated_paths = [] roots_hit = set() + new_nodes = [] for path in node_paths: if path == nxt_path.WORLD: # never draw world node. @@ -1216,6 +1222,10 @@ def handle_node_move(self, node_path, pos): node_item.setPos(pos[0], pos[1]) def handle_collapse_changed(self, node_paths): + while self._animating: + QtWidgets.QApplication.processEvents() + og_do_anims = self.do_animations + self.do_animations = True comp_layer = self.model.comp_layer roots_hit = set() for path in node_paths: @@ -1231,7 +1241,8 @@ def handle_collapse_changed(self, node_paths): include_implied=True) for child_path in children: self.remove_node_graphic(child_path) - roots_hit.add(nxt_path.get_root_path(path)) + if children: + roots_hit.add(nxt_path.get_root_path(path)) else: descendants = self.model.get_descendants(path, include_implied=True) @@ -1241,6 +1252,7 @@ def handle_collapse_changed(self, node_paths): if not root_graphic: continue root_graphic.arrange_descendants() + self.do_animations = og_do_anims def remove_node_graphic(self, node_path): if node_path not in self._node_graphics: @@ -1249,13 +1261,12 @@ def remove_node_graphic(self, node_path): if not graphic: return self.remove_node_connection_graphics(node_path) - self.scene().removeItem(graphic) - # because node graphics are parented to one another, removing parent - # implicitly removes descendants. - for key in list(self._node_graphics.keys()): - if nxt_path.is_ancestor(key, node_path): - self._node_graphics.pop(key) - self.remove_node_connection_graphics(key) + + def handle_del(): + self.scene().removeItem(graphic) + + graphic.out_anim_group.finished.connect(handle_del) + graphic.anim_out() def get_node_graphic(self, name): return self._node_graphics.get(name, None) From af4da59aadeaf739318a000a0617508bf3e32ba0 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+imlucasbrown@users.noreply.github.com> Date: Mon, 8 Feb 2021 07:37:08 -0500 Subject: [PATCH 15/23] * Bug fix: Corrected folder paths for Blender. * Blender's `Update NXT` button (not the update dependencies button) will now update the addon file as well as the backend. --- nxt_editor/integration/blender/__init__.py | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/nxt_editor/integration/blender/__init__.py b/nxt_editor/integration/blender/__init__.py index a9b722f..aa97cd2 100644 --- a/nxt_editor/integration/blender/__init__.py +++ b/nxt_editor/integration/blender/__init__.py @@ -23,12 +23,19 @@ def __init__(self): if b_major != 2 or b_minor < 80: raise RuntimeError('Blender version is not compatible with this ' 'version of nxt.') - user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' - 'Foundation/Blender/' - '{}.{}'.format(b_major, b_minor)) - self.user_dir = user_dir - nxt_modules = os.path.join(user_dir, 'scripts/addons/modules') - self.modules_dir = nxt_modules.replace(os.sep, '/') + + if 'darwin' in sys.platform: + user_dir = os.path.expandvars('/Users/$USER/Library/Application Support/Blender/{}.{}/') + elif 'linux' in sys.platform: + user_dir = os.path.expandvars('$HOME/.config/blender/{}.{}/') + elif 'win' in sys.platform: + user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' + 'Foundation/Blender/' + '{}.{}') + else: + raise SystemError('Unsupported operating system!') + + self.user_dir = user_dir.format(b_major, b_minor) self.instance = None self.nxt_qapp = QtWidgets.QApplication.instance() @@ -45,9 +52,13 @@ def setup(cls): def update(cls): self = cls() og_cwd = os.getcwd() - os.chdir(self.modules_dir) super(Blender, self).update() os.chdir(og_cwd) + addons_dir = os.path.join(self.user_dir, 'scripts/addons') + addons_dir = addons_dir.replace(os.sep, '/') + + addon_file = os.path.join(os.path.dirname(__file__), 'nxt_blender.py') + shutil.copy(addon_file, addons_dir) @classmethod def launch_nxt(cls): From 217eb4843db6a601cedf76383e4d4ab4f595ff6a Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Mon, 8 Feb 2021 12:46:41 -0500 Subject: [PATCH 16/23] + Node LODs + Added user prefs for `fps`, `lod`, and `animation` all on by default. In your user prefs you can toggle these using the keys previously stated; for `lod` it must be an int or float value, `0` to disable, default is `0.4`. Must restart editor for changes to these prefs to take effect. * Minor drawing optimizations ... FPS counter added to the bottom left of the stage view. (Low numbers aren't bad if you're idle) --- nxt_editor/connection_graphics_item.py | 17 +- nxt_editor/node_graphics_item.py | 240 +++++++++++++++---------- nxt_editor/stage_model.py | 4 +- nxt_editor/stage_view.py | 86 +++++---- nxt_editor/user_dir.py | 3 + 5 files changed, 209 insertions(+), 141 deletions(-) diff --git a/nxt_editor/connection_graphics_item.py b/nxt_editor/connection_graphics_item.py index a68df04..526a621 100644 --- a/nxt_editor/connection_graphics_item.py +++ b/nxt_editor/connection_graphics_item.py @@ -10,6 +10,7 @@ from . import colors from nxt import nxt_path, nxt_node import nxt_editor +from nxt_editor.node_graphics_item import MIN_LOD logger = logging.getLogger(nxt_editor.LOGGER_NAME) @@ -98,9 +99,19 @@ def rebuild_line(self): self.update() def paint(self, painter, option, widget): - painter.setRenderHints(QtGui.QPainter.Antialiasing | - QtGui.QPainter.SmoothPixmapTransform) - pen = QtGui.QPen(self.color, self.thickness, self.pen_style) + lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform( + painter.worldTransform()) + if lod > MIN_LOD: + painter.setRenderHints(QtGui.QPainter.Antialiasing | + QtGui.QPainter.SmoothPixmapTransform) + thick_mult = 1 + pen_style = self.pen_style + else: + painter.setRenderHints(False) + thick_mult = 3 + pen_style = QtCore.Qt.PenStyle.SolidLine + pen = QtGui.QPen(self.color, self.thickness * thick_mult, + self.pen_style) # if self.tgt_path in self.model.selection: # pen.setColor(colors.SELECTED) # elif self.is_hovered: diff --git a/nxt_editor/node_graphics_item.py b/nxt_editor/node_graphics_item.py index c37f37c..ca71eb3 100644 --- a/nxt_editor/node_graphics_item.py +++ b/nxt_editor/node_graphics_item.py @@ -17,10 +17,13 @@ from . import colors from nxt.stage import INTERNAL_ATTRS from .label_edit import NameEditDialog +from .user_dir import USER_PREF, user_prefs logger = logging.getLogger(nxt_editor.LOGGER_NAME) +MIN_LOD = user_prefs.get(USER_PREF.LOD, .4) + class NodeGraphicsItem(QtWidgets.QGraphicsObject): """The graphics item used to represent nodes in the graph. Contains @@ -268,16 +271,22 @@ def paint(self, painter, option, widget): """Override of QtWidgets.QGraphicsItem paint. Handles all visuals of the Node. Split up into 3 functions for organization. """ - painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing | QtGui.QPainter.SmoothPixmapTransform) - self.draw_title(painter) - self.draw_attributes(painter) - self.draw_border(painter) + lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform(painter.worldTransform()) + if lod > MIN_LOD: + painter.setRenderHints(QtGui.QPainter.Antialiasing | + QtGui.QPainter.TextAntialiasing | + QtGui.QPainter.SmoothPixmapTransform) + else: + painter.setRenderHints(False) + self.draw_title(painter, lod) + self.draw_attributes(painter, lod) + self.draw_border(painter, lod) def closest_grid_point(self, position): snapped_pos = self.model.snap_pos_to_grid((position.x(), position.y())) return QtCore.QPointF(*snapped_pos) - def draw_border(self, painter): + def draw_border(self, painter, lod=1.): """Draws border, called exclusively by paint. :param painter: painter from paint. @@ -305,7 +314,7 @@ def draw_border(self, painter): self.get_selection_rect().width() - 2, self.get_selection_rect().height() - 2)) - def draw_title(self, painter): + def draw_title(self, painter, lod=1.): """Draw title of the node. Called exclusively in paint. :param painter: painter from paint. @@ -399,76 +408,92 @@ def draw_title(self, painter): if self.exec_out_plug: self.scene().removeItem(self.exec_out_plug) self.exec_out_plug = None - - # draw attr dots - offset = -6 - for fill in self.attr_dots: - painter.setBrush(QtCore.Qt.white) - if fill: + if lod > MIN_LOD: + # draw attr dots + offset = -6 + for fill in self.attr_dots: painter.setBrush(QtCore.Qt.white) - else: - painter.setBrush(QtCore.Qt.NoBrush) - dots_color = QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor) - painter.setPen(QtGui.QPen(dots_color, 0.5)) - dot_x = self.max_width - 15 - dot_y = (self.title_rect_height / 2) + offset - painter.drawEllipse(QtCore.QPointF(dot_x, dot_y), 2, 2) - offset += 6 + if fill: + painter.setBrush(QtCore.Qt.white) + else: + painter.setBrush(QtCore.Qt.NoBrush) + dots_color = QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor) + painter.setPen(QtGui.QPen(dots_color, 0.5)) + dot_x = self.max_width - 15 + dot_y = (self.title_rect_height / 2) + offset + painter.drawEllipse(QtCore.QPointF(dot_x, dot_y), 2, 2) + offset += 6 # draw title - painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor)) - if not self.node_enabled: - painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(150)) + painter.setFont(self.title_font) title_str = nxt_path.node_name_from_node_path(self.node_path) font_metrics = QtGui.QFontMetrics(self.title_font) width = self.max_width - 40 if self.error_list: width -= 20 - title = font_metrics.elidedText(title_str, QtCore.Qt.ElideRight, width) - painter.drawText(15, 0, self.max_width - 15, self.title_rect_height, - QtCore.Qt.AlignVCenter, title) - - # draw error - if self.error_list: - pos = QtCore.QPointF(self.max_width-45, self.title_rect_height/4) - error_item = ErrorItem(font=QtGui.QFont('Roboto', 16, 75), - pos=pos, text='!') - error_item.setParentItem(self) - error_item.setZValue(50) - self.error_item = error_item + if lod > MIN_LOD: + painter.setPen( + QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor)) + if not self.node_enabled: + painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(150)) + title = font_metrics.elidedText(title_str, + QtCore.Qt.ElideRight, width) + painter.drawText(15, 0, self.max_width - 15, self.title_rect_height, + QtCore.Qt.AlignVCenter, title) else: - if self.error_item: - self.scene().removeItem(self.error_item) - self.error_item.deleteLater() - self.error_item = None + painter.setBrush(QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor)) + if not self.node_enabled: + painter.setBrush(QtGui.QColor(QtCore.Qt.white).darker(150)) + proxy_rect = font_metrics.boundingRect(title_str) + r_width = proxy_rect.width() * .8 + height = proxy_rect.height() + painter.drawRect(15, height * .8, + min(r_width, width), height * .2) + + if lod > MIN_LOD: + # draw error + if self.error_list: + pos = QtCore.QPointF(self.max_width-45, self.title_rect_height/4) + error_item = ErrorItem(font=QtGui.QFont('Roboto', 16, 75), + pos=pos, text='!') + error_item.setParentItem(self) + error_item.setZValue(50) + self.error_item = error_item + else: + if self.error_item: + self.scene().removeItem(self.error_item) + self.error_item.deleteLater() + self.error_item = None + # draw collapse state arrow for arrow in self.collapse_arrows: self.scene().removeItem(arrow) - self.collapse_arrows = [] - # TODO calculation needed arrows should be done outside drawing - # draw collapse state arrow - if self.collapse_state: - des_colors = self.model.get_descendant_colors(self.node_path) - filled = self.model.has_children(self.node_path) - if not filled: - des_colors = [QtCore.Qt.white] - elif not des_colors: - disp = self.model.comp_layer - des_colors = [self.model.get_node_color(self.node_path, disp)] - i = 0 - num = len(des_colors) - for c in des_colors: - arrow = CollapseArrow(self, filled=filled, color=c) - arrow_width = arrow.width * 1.1 - center_offset = (arrow_width * (num * .5) - arrow_width * .5) - cur_offset = (i * arrow_width) - pos = ((self.max_width * .5) + center_offset - cur_offset) - arrow.setPos(pos, self.boundingRect().height()) - self.collapse_arrows += [arrow] - i += 1 - - def draw_attributes(self, painter): + if lod > MIN_LOD: + self.collapse_arrows = [] + # TODO calculation needed arrows should be done outside drawing + + if self.collapse_state: + des_colors = self.model.get_descendant_colors(self.node_path) + filled = self.model.has_children(self.node_path) + if not filled: + des_colors = [QtCore.Qt.white] + elif not des_colors: + disp = self.model.comp_layer + des_colors = [self.model.get_node_color(self.node_path, disp)] + i = 0 + num = len(des_colors) + for c in des_colors: + arrow = CollapseArrow(self, filled=filled, color=c) + arrow_width = arrow.width * 1.1 + center_offset = (arrow_width * (num * .5) - arrow_width * .5) + cur_offset = (i * arrow_width) + pos = ((self.max_width * .5) + center_offset - cur_offset) + arrow.setPos(pos, self.boundingRect().height()) + self.collapse_arrows += [arrow] + i += 1 + + def draw_attributes(self, painter, lod=1.): """Draw attributes for this node. Called exclusively by paint. :param painter: painter from paint. @@ -487,44 +512,60 @@ def draw_attributes(self, painter): self._attr_plug_graphics.setdefault(attr_name, {}) attr_plug_graphics = self._attr_plug_graphics[attr_name] current_in_plug = attr_plug_graphics.get('in_plug') - in_pos = self.get_attr_in_pos(attr_name, scene=False) - if current_in_plug: - current_in_plug.setPos(in_pos) - current_in_plug.color = target_color - current_in_plug.update() - else: - in_plug = NodeGraphicsPlug(pos=in_pos, - radius=self.ATTR_PLUG_RADIUS, - color=target_color, - attr_name_represented=attr_name, - is_input=True) - attr_plug_graphics['in_plug'] = in_plug - in_plug.setParentItem(self) + if lod > MIN_LOD: + in_pos = self.get_attr_in_pos(attr_name, scene=False) + if current_in_plug: + current_in_plug.show() + current_in_plug.setPos(in_pos) + current_in_plug.color = target_color + current_in_plug.update() + else: + current_in_plug = NodeGraphicsPlug(pos=in_pos, + radius=self.ATTR_PLUG_RADIUS, + color=target_color, + attr_name_represented=attr_name, + is_input=True) + attr_plug_graphics['in_plug'] = current_in_plug + current_in_plug.setParentItem(self) + elif current_in_plug: + current_in_plug.hide() current_out_plug = attr_plug_graphics.get('out_plug') - out_pos = self.get_attr_out_pos(attr_name, scene=False) - if current_out_plug: - current_out_plug.setPos(out_pos) - current_out_plug.color = target_color - current_out_plug.update() - else: - out_plug = NodeGraphicsPlug(pos=out_pos, - radius=self.ATTR_PLUG_RADIUS, - color=target_color, - attr_name_represented=attr_name, - is_input=False) - attr_plug_graphics['out_plug'] = out_plug - out_plug.setParentItem(self) + if lod > MIN_LOD: + out_pos = self.get_attr_out_pos(attr_name, scene=False) + if current_out_plug: + current_out_plug.show() + current_out_plug.setPos(out_pos) + current_out_plug.color = target_color + current_out_plug.update() + else: + out_plug = NodeGraphicsPlug(pos=out_pos, + radius=self.ATTR_PLUG_RADIUS, + color=target_color, + attr_name_represented=attr_name, + is_input=False) + attr_plug_graphics['out_plug'] = out_plug + out_plug.setParentItem(self) + elif current_out_plug: + current_out_plug.hide() # draw attr_name - painter.setPen(attr_details['title_color']) - painter.setFont(attr_details['title_font']) rect = attr_details['bg_rect'] + painter.setFont(attr_details['title_font']) font_metrics = QtGui.QFontMetrics(self.attr_font) title = font_metrics.elidedText(attr_name, QtCore.Qt.ElideRight, self.max_width - 20) - painter.drawText(rect.x() + 10, rect.y() - 1, rect.width(), - rect.height(), QtCore.Qt.AlignVCenter, title) + if lod > MIN_LOD: + painter.setPen(attr_details['title_color']) + painter.drawText(rect.x() + 10, rect.y() - 1, rect.width(), + rect.height(), QtCore.Qt.AlignVCenter, title) + else: + proxy_rect = font_metrics.boundingRect(title) + height = proxy_rect.height() + width = proxy_rect.width() + painter.setBrush(attr_details['title_color'].darker(150)) + painter.drawRect(rect.x() + 10, rect.y() + height*.8, + width, height*.2) def calculate_attribute_draw_details(self): """Calculate position of all known attr names. Details stored in @@ -913,7 +954,14 @@ def boundingRect(self): def paint(self, painter, option, widget): """Override of QtWidgets.QGraphicsItem paint. Handles all visuals of the Plug.""" - painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing | QtGui.QPainter.SmoothPixmapTransform) + lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform( + painter.worldTransform()) + if lod > MIN_LOD: + painter.setRenderHints(QtGui.QPainter.Antialiasing | + QtGui.QPainter.TextAntialiasing | + QtGui.QPainter.SmoothPixmapTransform) + else: + painter.setRenderHints(False) if self.is_hovered: painter.setPen(QtGui.QPen(QtCore.Qt.white, self.hover_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) else: @@ -925,7 +973,7 @@ def paint(self, painter, option, widget): # create triangle polygon = QtGui.QPolygonF() step_angle = 120 - for i in range(4): + for i in [0, 1, 2, 3]: step = step_angle * i x = self.radius * 1.2 * math.cos(math.radians(step)) y = self.radius * 1.2 * math.sin(math.radians(step)) @@ -942,8 +990,6 @@ def paint(self, painter, option, widget): painter.setBrush(QtCore.Qt.green) painter.drawPolygon(polygon) - painter.setPen(QtCore.Qt.black) - painter.setFont(QtGui.QFont('Roboto', 12)) elif self.is_break: painter.drawRect(self.radius * -1, self.radius * -1, self.radius * 2, self.radius * 2) else: diff --git a/nxt_editor/stage_model.py b/nxt_editor/stage_model.py index 6a93f8d..2716e7c 100644 --- a/nxt_editor/stage_model.py +++ b/nxt_editor/stage_model.py @@ -2866,7 +2866,7 @@ def can_build_run(self): return True def setup_build(self, node_paths, rt_layer=None): - # Reset timer vars + # Reset once_sec_timer vars self.build_start_time = time.time() self.build_paused_time = .0 self.last_step_time = .0 @@ -2927,7 +2927,7 @@ def step_build(self): pause_delta = self.build_paused_time - time.time() self.build_start_time -= pause_delta # Always reset the paused time as a build step is the same as paused - # in regard to the build timer + # in regard to the build once_sec_timer self.build_paused_time = .0 if not self.can_build_run(): logger.error("Cannot step execution. Build is not ready.") diff --git a/nxt_editor/stage_view.py b/nxt_editor/stage_view.py index 849aaab..ce53622 100644 --- a/nxt_editor/stage_view.py +++ b/nxt_editor/stage_view.py @@ -15,7 +15,7 @@ from nxt_editor.node_graphics_item import NodeGraphicsItem, NodeGraphicsPlug from nxt_editor.connection_graphics_item import AttrConnectionGraphic from nxt_editor.commands import * - +from .user_dir import USER_PREF, user_prefs logger = logging.getLogger(nxt_editor.LOGGER_NAME) @@ -38,7 +38,15 @@ class StageView(QtWidgets.QGraphicsView): def __init__(self, model, parent=None): super(StageView, self).__init__(parent=parent) self.main_window = parent - self.do_animations = True + self._do_anim_pref = user_prefs.get(USER_PREF.ANIMATION, True) + self.do_animations = self._do_anim_pref + self.once_sec_timer = QtCore.QTimer(self) + self.once_sec_timer.timeout.connect(self.calculate_fps) + self.frames = 0 + self.fps = 0 + self.once_sec_timer.setInterval(1000) + if user_prefs.get(USER_PREF.FPS, True): + self.once_sec_timer.start() self._animating = [] # EXEC ACTIONS self.exec_actions = parent.execute_actions @@ -66,9 +74,11 @@ def __init__(self, model, parent=None): self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.horizontalScrollBar().setValue(0) self.verticalScrollBar().setValue(0) - + self.setOptimizationFlag(self.DontSavePainterState, enabled=True) + self.setOptimizationFlag(self.DontAdjustForAntialiasing, enabled=True) # scene - self.setScene(QtWidgets.QGraphicsScene()) + self._scene = QtWidgets.QGraphicsScene() + self.setScene(self._scene) # TODO Currently setting scene rect and never changing it. We hope for expanding graphs in the future. self.scene().setSceneRect(QtCore.QRect(-5000, -5000, 10000, 10000)) # rubber band @@ -134,23 +144,38 @@ def __init__(self, model, parent=None): # filepath HUD self.filepath_label = HUDItem(text=None) self.filepath_label.setFont(QtGui.QFont("Roboto Mono", 8)) - self.filepath_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.filepath_label.setAlignment(QtCore.Qt.AlignLeft | + QtCore.Qt.AlignTop) self.update_filepath() self.hud_layout.addWidget(self.filepath_label, 0, 0) # resolved HUD - self.resolved_label = HUDItem(text='resolved', - fade_time=1000, - start_color=QtGui.QColor(255, 255, 255, 255), - end_color=QtGui.QColor(255, 255, 255, 100)) + self.resolved_label = HUDItem(text='resolved', fade_time=1000) self.resolved_label.setFont(QtGui.QFont("Roboto", 12, weight=75)) - self.resolved_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) + self.resolved_label.setAlignment(QtCore.Qt.AlignRight | + QtCore.Qt.AlignTop) self.hud_layout.addWidget(self.resolved_label, 0, 3) + self.fps_label = HUDItem(text='resolved', fade_time=0) + self.fps_label.setFont(QtGui.QFont("Roboto", 12, weight=75)) + self.fps_label.setAlignment(QtCore.Qt.AlignRight | + QtCore.Qt.AlignBottom) + if user_prefs.get(USER_PREF.FPS, True): + self.hud_layout.addWidget(self.fps_label, 1, 3) + self.SEL_ADD_MODIFIERS = QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier self.SEL_TOGGLE_MODIFIERS = QtCore.Qt.KeyboardModifiers(QtCore.Qt.ShiftModifier) self.SEL_RMV_MODIFIERS = QtCore.Qt.KeyboardModifiers(QtCore.Qt.ControlModifier) + def calculate_fps(self): + self.fps = (self.frames + self.fps) * .5 + self.fps_label.setText(str(round(self.fps))) + self.frames = 0 + + def drawForeground(self, painter, rect): + super(StageView, self).drawForeground(painter, rect) + self.frames += 1 + def focusInEvent(self, event): super(StageView, self).focusInEvent(event) @@ -712,9 +737,6 @@ def mousePressEvent(self, event): # start panning action self.panning = True self._previous_mouse_pos = None - for node in self._node_graphics.values(): - coord_cache = QtWidgets.QGraphicsItem.ItemCoordinateCache - node.setCacheMode(coord_cache) event.accept() return @@ -923,9 +945,6 @@ def mouseReleaseEvent(self, event): if self.panning and event.button() == QtCore.Qt.MiddleButton: self._previous_mouse_pos = None self.panning = False - for node in self._node_graphics.values(): - pretty_mode = QtWidgets.QGraphicsItem.DeviceCoordinateCache - node.setCacheMode(pretty_mode) self._current_pan_distance = 0.0 event.accept() return @@ -949,39 +968,28 @@ def mouseDoubleClickEvent(self, event): item.collapse_node() def wheelEvent(self, event): - num_degrees = event.delta() / 8. - num_steps = num_degrees / 15. - self._num_scheduled_scalings += num_steps - if self._num_scheduled_scalings * num_steps < 0.: - self._num_scheduled_scalings = num_steps - anim = QtCore.QTimeLine(duration=150, parent=self) - anim.setUpdateInterval(20) - anim.valueChanged.connect(self.scaling_time) - anim.finished.connect(self.anim_finished) self._view_pos = event.pos() self._scene_pos = self.mapToScene(self._view_pos) - anim.start() - event.accept() - return - def scaling_time(self, f): - zoom_pref_key = user_dir.USER_PREF.ZOOM_MULT - pref_mult = user_dir.user_prefs.get(zoom_pref_key, 1.0) - mult = 150 / pref_mult - factor = 1.0 + self._num_scheduled_scalings / mult - if 0 < factor < 1: + try: + new_scale = event.delta() * .001 + 1.0 + except AttributeError: + new_scale = 1.1 + + if 0 < new_scale < 1: if self.scale_factor > self._scale_minimum: - self.scale(factor, factor) + self.scale(new_scale, new_scale) self._center_view() - else: self.scale(1, 1) - elif factor > 1: + elif new_scale > 1: if self.scale_factor < self._scale_maximum: - self.scale(factor, factor) + self.scale(new_scale, new_scale) self._center_view() else: self.scale(1, 1) + event.accept() + return def _center_view(self): view_center = self.mapToScene(self.viewport().rect().center()) @@ -1225,7 +1233,7 @@ def handle_collapse_changed(self, node_paths): while self._animating: QtWidgets.QApplication.processEvents() og_do_anims = self.do_animations - self.do_animations = True + self.do_animations = self._do_anim_pref comp_layer = self.model.comp_layer roots_hit = set() for path in node_paths: diff --git a/nxt_editor/user_dir.py b/nxt_editor/user_dir.py index 57fae5d..26cbd87 100644 --- a/nxt_editor/user_dir.py +++ b/nxt_editor/user_dir.py @@ -68,6 +68,9 @@ class USER_PREF(): TREE_INDENT = 'layer_tree_indent' FIND_REP_NODE_PATTERNS = 'find_replace_nodes_patterns' FIND_REP_ATTRS = 'find_replace_attrs' + FPS = 'fps' + LOD = 'lod' + ANIMATION = 'animation' class EDITOR_CACHE(): From 9db21da18b4917e31aa5bce9bfff9695ad93fb23 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Mon, 8 Feb 2021 14:28:13 -0500 Subject: [PATCH 17/23] Addressing notes --- nxt_editor/integration/blender/__init__.py | 23 ++++------------------ 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/nxt_editor/integration/blender/__init__.py b/nxt_editor/integration/blender/__init__.py index aa97cd2..09f83bb 100644 --- a/nxt_editor/integration/blender/__init__.py +++ b/nxt_editor/integration/blender/__init__.py @@ -23,19 +23,8 @@ def __init__(self): if b_major != 2 or b_minor < 80: raise RuntimeError('Blender version is not compatible with this ' 'version of nxt.') - - if 'darwin' in sys.platform: - user_dir = os.path.expandvars('/Users/$USER/Library/Application Support/Blender/{}.{}/') - elif 'linux' in sys.platform: - user_dir = os.path.expandvars('$HOME/.config/blender/{}.{}/') - elif 'win' in sys.platform: - user_dir = os.path.expanduser('~/AppData/Roaming/Blender ' - 'Foundation/Blender/' - '{}.{}') - else: - raise SystemError('Unsupported operating system!') - - self.user_dir = user_dir.format(b_major, b_minor) + addons_dir = bpy.utils.user_resource('SCRIPTS', 'addons') + self.addons_dir = addons_dir.replace(os.sep, '/') self.instance = None self.nxt_qapp = QtWidgets.QApplication.instance() @@ -43,9 +32,8 @@ def __init__(self): def setup(cls): self = cls() bpy.ops.preferences.addon_disable(module='nxt_' + self.name) - addons_dir = os.path.join(self.user_dir, 'scripts/addons') integration_filepath = self.get_integration_filepath() - shutil.copy(integration_filepath, addons_dir) + shutil.copy(integration_filepath, self.addons_dir) bpy.ops.preferences.addon_enable(module='nxt_' + self.name) @classmethod @@ -54,11 +42,8 @@ def update(cls): og_cwd = os.getcwd() super(Blender, self).update() os.chdir(og_cwd) - addons_dir = os.path.join(self.user_dir, 'scripts/addons') - addons_dir = addons_dir.replace(os.sep, '/') - addon_file = os.path.join(os.path.dirname(__file__), 'nxt_blender.py') - shutil.copy(addon_file, addons_dir) + shutil.copy(addon_file, self.addons_dir) @classmethod def launch_nxt(cls): From 846e6aa6204f749e07fc37b66ce3cc673b1e50d7 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 9 Feb 2021 12:42:22 -0500 Subject: [PATCH 18/23] Version up for release --- nxt_editor/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxt_editor/version.json b/nxt_editor/version.json index 9dfb411..0845990 100644 --- a/nxt_editor/version.json +++ b/nxt_editor/version.json @@ -1,7 +1,7 @@ { "EDITOR": { "MAJOR": 3, - "MINOR": 6, - "PATCH": 2 + "MINOR": 7, + "PATCH": 0 } } From 809335c44450dbef8d55d5b86aba40451bf6adc9 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 9 Feb 2021 13:00:32 -0500 Subject: [PATCH 19/23] py2 compatibility fixes --- nxt_editor/dockwidgets/code_editor.py | 5 ++--- nxt_editor/dockwidgets/widget_builder.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nxt_editor/dockwidgets/code_editor.py b/nxt_editor/dockwidgets/code_editor.py index b9dc033..7ed7aef 100644 --- a/nxt_editor/dockwidgets/code_editor.py +++ b/nxt_editor/dockwidgets/code_editor.py @@ -1303,9 +1303,8 @@ def paintEvent(self, event): # Draw center message text msg_offset = font_metrics.boundingRect(self.click_msg).width() msg_offset += painter.font().pointSize() - pos = (self.rect().center().x() - (msg_offset*.5), - self.rect().center().y()) - painter.drawText(*pos, self.click_msg) + painter.drawText(self.rect().center().x() - (msg_offset*.5), + self.rect().center().y(), self.click_msg) painter.setCompositionMode(QtGui.QPainter.CompositionMode_Darken) path = QtGui.QPainterPath() path.addRoundedRect(QtCore.QRectF(self.rect()), 9, 9) diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index 5147186..0aaf128 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -1,7 +1,10 @@ # Built-in import logging import ast -import collections.abc +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable # External from Qt import QtWidgets From c067dda146b58588d42f0b6d947ee1e11ac0a27c Mon Sep 17 00:00:00 2001 From: MichaelAldrich <11843596+MichaelAldrich@users.noreply.github.com> Date: Tue, 9 Feb 2021 14:25:27 -0500 Subject: [PATCH 20/23] * Update make maya plugin graph to run correctly in python 2 or 3 --- build/make_maya_plugin.nxt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build/make_maya_plugin.nxt b/build/make_maya_plugin.nxt index f29d579..e9847b6 100644 --- a/build/make_maya_plugin.nxt +++ b/build/make_maya_plugin.nxt @@ -170,7 +170,11 @@ "os.makedirs('${mod_folder}/scripts/Qt')", "", "with open('${mod_folder}/scripts/Qt/__init__.py', 'w+') as fp:", - " fp.write(result.content)" + " if isinstance(result.content, str):", + " fp.write(result.content)", + " else:", + " fp.write(result.content.decode())", + "" ] } } From 7149c288f300e39c1cee35f39c36da202013dd84 Mon Sep 17 00:00:00 2001 From: Michael Aldrich <11843596+MichaelAldrich@users.noreply.github.com> Date: Tue, 9 Feb 2021 14:53:54 -0500 Subject: [PATCH 21/23] Update unreal plugin readme --- nxt_editor/integration/unreal/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nxt_editor/integration/unreal/README.md b/nxt_editor/integration/unreal/README.md index 5732b6f..d5f9269 100644 --- a/nxt_editor/integration/unreal/README.md +++ b/nxt_editor/integration/unreal/README.md @@ -3,9 +3,10 @@ This is an Unreal plugin to connect to the nxt python package. The nxt python package will be downloaded from the internet as part of this installation. 1. Move this plugin either into your project or engine's plugin directory. -2. Activate the plugin in the engine plugin browser. -3. Install the nxt python package - - __automated -__ From top menu option "nxt", select "Install nxt package to active python" +2. Ensure that the python editor scripting plugin is activated. +3. Activate the plugin in the engine plugin browser. Exit the engine. +4. Install the nxt python package + - __automated -__ From unreal editor top menu option "nxt", select "Install nxt package to active python" - __by hand -__ Locate engine python(`sys.prefix`) in commmand prompt or terminal and run: `python -m pip install nxt-editor` (Note on windows this will be `python.exe`) 4. Restart Editor From 0afa98cb2c6c244265dfdf9c845e595059f53224 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 9 Feb 2021 15:11:40 -0500 Subject: [PATCH 22/23] Py2 compatibility fixes Old Qt compatibility fixes --- nxt_editor/dockwidgets/widget_builder.py | 2 +- nxt_editor/node_graphics_item.py | 21 ++++++++++++++++----- nxt_editor/stage_view.py | 5 ++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index 0aaf128..79bde18 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -279,7 +279,7 @@ def update_window(self, changed_paths=None): return update = False if changed_paths else True - if not isinstance(changed_paths, collections.abc.Iterable): + if not isinstance(changed_paths, Iterable): changed_paths = [] for path in changed_paths: node_path, _ = nxt_path.path_attr_partition(path) diff --git a/nxt_editor/node_graphics_item.py b/nxt_editor/node_graphics_item.py index ca71eb3..58f160e 100644 --- a/nxt_editor/node_graphics_item.py +++ b/nxt_editor/node_graphics_item.py @@ -9,6 +9,7 @@ from Qt import QtWidgets from Qt import QtGui from Qt import QtCore +from PySide2 import __version_info__ as qt_version # Internal import nxt_editor @@ -24,8 +25,16 @@ MIN_LOD = user_prefs.get(USER_PREF.LOD, .4) +_pyside_version = qt_version -class NodeGraphicsItem(QtWidgets.QGraphicsObject): + +if _pyside_version[1] < 11: + graphic_type = QtWidgets.QGraphicsItem +else: + graphic_type = QtWidgets.QGraphicsObject + + +class NodeGraphicsItem(graphic_type): """The graphics item used to represent nodes in the graph. Contains instances of NodeGraphicsPlug for each attribute on the associated node. Contains functionality for arranging children into stacks. @@ -139,8 +148,9 @@ def anim_into_place(self, end_pos): if end_pos == self.pos(): return self.view._animating.append(self) - self.setup_in_anim() - if not self.view.do_animations: + if self.view.do_animations: + self.setup_in_anim() + else: self.setPos(end_pos) self.in_anim_group.finished.emit() return @@ -161,8 +171,9 @@ def anim_out(self): if self.get_is_animating(): return self.view._animating.append(self) - self.setup_out_anim() - if not self.view.do_animations: + if self.view.do_animations: + self.setup_out_anim() + else: self.out_anim_group.finished.emit() return self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache) diff --git a/nxt_editor/stage_view.py b/nxt_editor/stage_view.py index ce53622..b27b473 100644 --- a/nxt_editor/stage_view.py +++ b/nxt_editor/stage_view.py @@ -12,7 +12,8 @@ # Interal import nxt_editor from nxt import nxt_node, tokens -from nxt_editor.node_graphics_item import NodeGraphicsItem, NodeGraphicsPlug +from nxt_editor.node_graphics_item import (NodeGraphicsItem, NodeGraphicsPlug, + _pyside_version) from nxt_editor.connection_graphics_item import AttrConnectionGraphic from nxt_editor.commands import * from .user_dir import USER_PREF, user_prefs @@ -39,6 +40,8 @@ def __init__(self, model, parent=None): super(StageView, self).__init__(parent=parent) self.main_window = parent self._do_anim_pref = user_prefs.get(USER_PREF.ANIMATION, True) + if _pyside_version[1] < 11: + self._do_anim_pref = False self.do_animations = self._do_anim_pref self.once_sec_timer = QtCore.QTimer(self) self.once_sec_timer.timeout.connect(self.calculate_fps) From e6311260f92d5749dcdfa8b50b7daaddd611f018 Mon Sep 17 00:00:00 2001 From: ImLucasBrown <54835354+ImLucasBrown@users.noreply.github.com> Date: Tue, 9 Feb 2021 15:52:44 -0500 Subject: [PATCH 23/23] Fixed UE plugin graph to have a broader exception --- build/make_unreal_plugin.nxt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/make_unreal_plugin.nxt b/build/make_unreal_plugin.nxt index 638ef59..6e02431 100644 --- a/build/make_unreal_plugin.nxt +++ b/build/make_unreal_plugin.nxt @@ -52,7 +52,7 @@ "os.remove(leftover_init)", "try:", " shutil.rmtree(leftover_pycache)", - "except FileNotFoundError:", + "except:", " pass" ] }