This repository has been archived by the owner on Sep 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5868 from ynput/feature/OP-6032_3dequalizer-integ…
…ration Integration: 3DEqualizer integration
- Loading branch information
Showing
53 changed files
with
2,174 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from .addon import ( | ||
EqualizerAddon, | ||
EQUALIZER_HOST_DIR, | ||
) | ||
|
||
__all__ = [ | ||
"EqualizerAddon", | ||
"EQUALIZER_HOST_DIR", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import os | ||
from openpype.modules import OpenPypeModule, IHostAddon | ||
|
||
EQUALIZER_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) | ||
|
||
|
||
class EqualizerAddon(OpenPypeModule, IHostAddon): | ||
name = "equalizer" | ||
host_name = "equalizer" | ||
heartbeat = 500 | ||
|
||
def initialize(self, module_settings): | ||
self.heartbeat = module_settings.get("heartbeat_interval", 500) | ||
self.enabled = True | ||
|
||
def add_implementation_envs(self, env, _app): | ||
# 3dEqualizer utilize TDE4_ROOT for its root directory | ||
# and PYTHON_CUSTOM_SCRIPTS_3DE4 as a colon separated list of | ||
# directories to look for additional python scripts. | ||
# (Windows: list is separated by semicolons). | ||
# Ad | ||
|
||
startup_path = os.path.join(EQUALIZER_HOST_DIR, "startup") | ||
if "PYTHON_CUSTOM_SCRIPTS_3DE4" in env: | ||
startup_path = os.path.join( | ||
env["PYTHON_CUSTOM_SCRIPTS_3DE4"], | ||
startup_path) | ||
|
||
env["PYTHON_CUSTOM_SCRIPTS_3DE4"] = startup_path | ||
env["AYON_TDE4_HEARTBEAT_INTERVAL"] = str(self.heartbeat) | ||
|
||
def get_launch_hook_paths(self, app): | ||
if app.host_name != self.host_name: | ||
return [] | ||
return [ | ||
os.path.join(EQUALIZER_HOST_DIR, "hooks") | ||
] | ||
|
||
def get_workfile_extensions(self): | ||
return [".3de"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from .host import EqualizerHost | ||
from .plugin import EqualizerCreator, ExtractScriptBase | ||
from .pipeline import Container, maintained_model_selection | ||
|
||
__all__ = [ | ||
"EqualizerHost", | ||
"EqualizerCreator", | ||
"Container", | ||
"ExtractScriptBase", | ||
"maintained_model_selection", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
"""3dequalizer host implementation. | ||
note: | ||
3dequalizer 7.1v2 uses Python 3.7.9 | ||
""" | ||
import json | ||
import os | ||
import re | ||
|
||
import pyblish.api | ||
import tde4 # noqa: F401 | ||
from attrs import asdict | ||
from attrs.exceptions import NotAnAttrsClassError | ||
from qtpy import QtCore, QtWidgets | ||
|
||
from openpype.host import HostBase, ILoadHost, IPublishHost, IWorkfileHost | ||
from openpype.hosts.equalizer import EQUALIZER_HOST_DIR | ||
from openpype.hosts.equalizer.api.pipeline import Container | ||
from openpype.pipeline import ( | ||
register_creator_plugin_path, | ||
register_loader_plugin_path, | ||
) | ||
|
||
CONTEXT_REGEX = re.compile( | ||
r"AYON_CONTEXT::(?P<context>.*?)::AYON_CONTEXT_END", | ||
re.DOTALL) | ||
PLUGINS_DIR = os.path.join(EQUALIZER_HOST_DIR, "plugins") | ||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") | ||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load") | ||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create") | ||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") | ||
|
||
|
||
class EqualizerHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): | ||
name = "equalizer" | ||
_instance = None | ||
|
||
def __new__(cls): | ||
# singleton - ensure only one instance of the host is created. | ||
# This is necessary because 3DEqualizer doesn't have a way to | ||
# store custom data, so we need to store it in the project notes. | ||
if not hasattr(cls, "_instance") or not cls._instance: | ||
cls._instance = super(EqualizerHost, cls).__new__(cls) | ||
return cls._instance | ||
|
||
def __init__(self): | ||
self._qapp = None | ||
super(EqualizerHost, self).__init__() | ||
|
||
def workfile_has_unsaved_changes(self): | ||
"""Return the state of the current workfile. | ||
3DEqualizer returns state as 1 or zero, so we need to invert it. | ||
Returns: | ||
bool: True if the current workfile has unsaved changes. | ||
""" | ||
return not bool(tde4.isProjectUpToDate()) | ||
|
||
def get_workfile_extensions(self): | ||
return [".3de"] | ||
|
||
def save_workfile(self, dst_path=None): | ||
if not dst_path: | ||
dst_path = tde4.getProjectPath() | ||
result = tde4.saveProject(dst_path, True) | ||
if not bool(result): | ||
raise RuntimeError(f"Failed to save workfile {dst_path}.") | ||
|
||
return dst_path | ||
|
||
def open_workfile(self, filepath): | ||
result = tde4.loadProject(filepath, True) | ||
if not bool(result): | ||
raise RuntimeError(f"Failed to open workfile {filepath}.") | ||
|
||
return filepath | ||
|
||
def get_current_workfile(self): | ||
return tde4.getProjectPath() | ||
|
||
def get_containers(self): | ||
context = self.get_context_data() | ||
if context: | ||
return context.get("containers", []) | ||
return [] | ||
|
||
def add_container(self, container: Container): | ||
context_data = self.get_context_data() | ||
containers = self.get_containers() | ||
|
||
for _container in containers: | ||
if _container["name"] == container.name and _container["namespace"] == container.namespace: # noqa: E501 | ||
containers.remove(_container) | ||
break | ||
|
||
try: | ||
containers.append(asdict(container)) | ||
except NotAnAttrsClassError: | ||
print("not an attrs class") | ||
containers.append(container) | ||
|
||
context_data["containers"] = containers | ||
self.update_context_data(context_data, changes={}) | ||
|
||
def get_context_data(self) -> dict: | ||
"""Get context data from the current workfile. | ||
3Dequalizer doesn't have any custom node or other | ||
place to store metadata, so we store context data in | ||
the project notes encoded as JSON and wrapped in a | ||
special guard string `AYON_CONTEXT::...::AYON_CONTEXT_END`. | ||
Returns: | ||
dict: Context data. | ||
""" | ||
|
||
# sourcery skip: use-named-expression | ||
m = re.search(CONTEXT_REGEX, tde4.getProjectNotes()) | ||
try: | ||
context = json.loads(m["context"]) if m else {} | ||
except ValueError: | ||
self.log.debug("context data is not valid json") | ||
context = {} | ||
|
||
return context | ||
|
||
def update_context_data(self, data, changes): | ||
"""Update context data in the current workfile. | ||
Serialize context data as json and store it in the | ||
project notes. If the context data is not found, create | ||
a placeholder there. See `get_context_data` for more info. | ||
Args: | ||
data (dict): Context data. | ||
changes (dict): Changes to the context data. | ||
Raises: | ||
RuntimeError: If the context data is not found. | ||
""" | ||
notes = tde4.getProjectNotes() | ||
m = re.search(CONTEXT_REGEX, notes) | ||
if not m: | ||
# context data not found, create empty placeholder | ||
tde4.setProjectNotes( | ||
f"{tde4.getProjectNotes()}\n" | ||
f"AYON_CONTEXT::::AYON_CONTEXT_END\n") | ||
|
||
original_data = self.get_context_data() | ||
|
||
updated_data = original_data.copy() | ||
updated_data.update(data) | ||
update_str = json.dumps(updated_data or {}, indent=4) | ||
|
||
tde4.setProjectNotes( | ||
re.sub( | ||
CONTEXT_REGEX, | ||
f"AYON_CONTEXT::{update_str}::AYON_CONTEXT_END", | ||
tde4.getProjectNotes() | ||
) | ||
) | ||
tde4.updateGUI() | ||
|
||
def install(self): | ||
if not QtCore.QCoreApplication.instance(): | ||
app = QtWidgets.QApplication([]) | ||
self._qapp = app | ||
self._qapp.setQuitOnLastWindowClosed(False) | ||
|
||
pyblish.api.register_host("equalizer") | ||
|
||
pyblish.api.register_plugin_path(PUBLISH_PATH) | ||
register_loader_plugin_path(LOAD_PATH) | ||
register_creator_plugin_path(CREATE_PATH) | ||
|
||
heartbeat_interval = os.getenv("AYON_TDE4_HEARTBEAT_INTERVAL") or 500 | ||
tde4.setTimerCallbackFunction( | ||
"EqualizerHost._timer", int(heartbeat_interval)) | ||
|
||
@staticmethod | ||
def _timer(): | ||
QtWidgets.QApplication.instance().processEvents( | ||
QtCore.QEventLoop.AllEvents) | ||
|
||
@classmethod | ||
def get_host(cls): | ||
return cls._instance | ||
|
||
def get_main_window(self): | ||
return self._qapp.activeWindow() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from attrs import field, define | ||
from openpype.pipeline import AVALON_CONTAINER_ID | ||
import contextlib | ||
import tde4 | ||
|
||
|
||
@define | ||
class Container(object): | ||
|
||
name: str = field(default=None) | ||
id: str = field(init=False, default=AVALON_CONTAINER_ID) | ||
namespace: str = field(default="") | ||
loader: str = field(default=None) | ||
representation: str = field(default=None) | ||
|
||
|
||
@contextlib.contextmanager | ||
def maintained_model_selection(): | ||
"""Maintain model selection during context.""" | ||
|
||
point_groups = tde4.getPGroupList() | ||
point_group = next( | ||
( | ||
pg for pg in point_groups | ||
if tde4.getPGroupType(pg) == "CAMERA" | ||
), None | ||
) | ||
selected_models = tde4.get3DModelList(point_group, 1)\ | ||
if point_group else [] | ||
try: | ||
yield | ||
finally: | ||
if point_group: | ||
# 3 restore model selection | ||
for model in tde4.get3DModelList(point_group, 0): | ||
if model in selected_models: | ||
tde4.set3DModelSelectionFlag(point_group, model, 1) | ||
else: | ||
tde4.set3DModelSelectionFlag(point_group, model, 0) |
Oops, something went wrong.