diff --git a/pype/hosts/fusion/__init__.py b/pype/hosts/fusion/__init__.py index e69de29bb2..7f54e71a75 100644 --- a/pype/hosts/fusion/__init__.py +++ b/pype/hosts/fusion/__init__.py @@ -0,0 +1,47 @@ +import sys +import os + +from .pipeline import ( + install, + uninstall, + publish, + launch_workfiles_app +) + +from .utils import ( + setup +) + + +from .lib import ( + get_additional_data, + update_frame_range +) + +from .menu import launch_pype_menu + +host_dir = os.path.dirname(__file__) +script_dir = os.path.join(host_dir, "scripts") +sys.path.append(script_dir) + +__all__ = [ + # pipeline + "install", + "uninstall", + "publish", + "launch_workfiles_app", + + # utils + "setup", + "get_resolve_module", + + # lib + "get_additional_data", + "update_frame_range", + + # menu + "launch_pype_menu", + + # scripts + "set_rendermode" +] diff --git a/pype/hosts/fusion/menu.py b/pype/hosts/fusion/menu.py index 73ea937513..2a00c37ed6 100644 --- a/pype/hosts/fusion/menu.py +++ b/pype/hosts/fusion/menu.py @@ -15,6 +15,8 @@ from avalon.tools import ( libraryloader ) +import set_rendermode + def load_stylesheet(): path = os.path.join(os.path.dirname(__file__), "menu_style.qss") @@ -65,7 +67,7 @@ class PypeMenu(QtWidgets.QWidget): load_btn = QtWidgets.QPushButton("Load", self) inventory_btn = QtWidgets.QPushButton("Inventory", self) libload_btn = QtWidgets.QPushButton("Library", self) - rename_btn = QtWidgets.QPushButton("Rename", self) + rendermode_btn = QtWidgets.QPushButton("Set render mode", self) set_colorspace_btn = QtWidgets.QPushButton( "Set colorspace from presets", self ) @@ -88,7 +90,7 @@ class PypeMenu(QtWidgets.QWidget): layout.addWidget(Spacer(15, self)) - layout.addWidget(rename_btn) + layout.addWidget(rendermode_btn) layout.addWidget(Spacer(15, self)) @@ -103,7 +105,7 @@ class PypeMenu(QtWidgets.QWidget): load_btn.clicked.connect(self.on_load_clicked) inventory_btn.clicked.connect(self.on_inventory_clicked) libload_btn.clicked.connect(self.on_libload_clicked) - rename_btn.clicked.connect(self.on_rename_clicked) + rendermode_btn.clicked.connect(self.on_rendernode_clicked) set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked) reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) @@ -131,8 +133,9 @@ class PypeMenu(QtWidgets.QWidget): print("Clicked Library") libraryloader.show() - def on_rename_clicked(self): - print("Clicked Rename") + def on_rendernode_clicked(self): + print("Clicked Set Render Mode") + set_rendermode.main() def on_set_colorspace_clicked(self): print("Clicked Set Colorspace") diff --git a/pype/hosts/fusion/menu_style.qss b/pype/hosts/fusion/menu_style.qss new file mode 100644 index 0000000000..df4fd7e949 --- /dev/null +++ b/pype/hosts/fusion/menu_style.qss @@ -0,0 +1,29 @@ +QWidget { + background-color: #282828; + border-radius: 3; +} + +QPushButton { + border: 1px solid #090909; + background-color: #201f1f; + color: #ffffff; + padding: 5; +} + +QPushButton:focus { + background-color: "#171717"; + color: #d0d0d0; +} + +QPushButton:hover { + background-color: "#171717"; + color: #e64b3d; +} + +#PypeMenu { + border: 1px solid #fef9ef; +} + +#Spacer { + background-color: #282828; +} diff --git a/pype/hosts/fusion/pipeline.py b/pype/hosts/fusion/pipeline.py index 6ade71767f..d593f2b615 100644 --- a/pype/hosts/fusion/pipeline.py +++ b/pype/hosts/fusion/pipeline.py @@ -2,20 +2,17 @@ Basic avalon integration """ import os -# import sys + from avalon.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish from pypeapp import Logger +from pype import PLUGINS_DIR log = Logger().get_logger(__name__, "fusion") -# self = sys.modules[__name__] AVALON_CONFIG = os.environ["AVALON_CONFIG"] -PARENT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.dirname(PARENT_DIR) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") LOAD_PATH = os.path.join(PLUGINS_DIR, "fusion", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "fusion", "create") @@ -25,9 +22,6 @@ PUBLISH_PATH = os.path.join( PLUGINS_DIR, "fusion", "publish" ).replace("\\", "/") -AVALON_CONTAINERS = ":AVALON_CONTAINERS" -# IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True) - def install(): """Install fusion-specific functionality of avalon-core. @@ -52,7 +46,7 @@ def install(): pyblish.register_host("fusion") pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering DaVinci Resovle plug-ins..") + log.info("Registering Fusion plug-ins..") avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) @@ -74,7 +68,7 @@ def uninstall(): """ pyblish.deregister_host("fusion") pyblish.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering DaVinci Resovle plug-ins..") + log.info("Deregistering Fusion plug-ins..") avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) @@ -109,57 +103,6 @@ def on_pyblish_instance_toggled(instance, new_value, old_value): tool.SetAttrs({"TOOLB_PassThrough": passthrough}) -def containerise(obj, - name, - namespace, - context, - loader=None, - data=None): - """Bundle Fusion's object into an assembly and imprint it with metadata - - Containerisation enables a tracking of version, author and origin - for loaded assets. - - Arguments: - obj (obj): Resolve's object to imprint as container - name (str): Name of resulting assembly - namespace (str): Namespace under which to host container - context (dict): Asset information - loader (str, optional): Name of node used to produce this container. - - Returns: - obj (obj): containerised object - - """ - pass - - -def ls(): - """List available containers. - - This function is used by the Container Manager in Nuke. You'll - need to implement a for-loop that then *yields* one Container at - a time. - - See the `container.json` schema for details on how it should look, - and the Maya equivalent, which is in `avalon.maya.pipeline` - """ - pass - - -def parse_container(container): - """Return the container node's full container data. - - Args: - container (str): A container node name. - - Returns: - dict: The container schema data for this container node. - - """ - pass - - def launch_workfiles_app(*args): workdir = os.environ["AVALON_WORKDIR"] workfiles.show(workdir) diff --git a/pype/hosts/fusion/scripts/32bit/backgrounds_selected_to32bit.py b/pype/hosts/fusion/scripts/32bit/backgrounds_selected_to32bit.py new file mode 100644 index 0000000000..c0dcef5410 --- /dev/null +++ b/pype/hosts/fusion/scripts/32bit/backgrounds_selected_to32bit.py @@ -0,0 +1,12 @@ +from avalon.fusion import comp_lock_and_undo_chunk + + +def main(): + """Set all selected backgrounds to 32 bit""" + with comp_lock_and_undo_chunk(comp, 'Selected Backgrounds to 32bit'): + tools = comp.GetToolList(True, "Background").values() + for tool in tools: + tool.Depth = 5 + + +main() diff --git a/pype/hosts/fusion/scripts/32bit/backgrounds_to32bit.py b/pype/hosts/fusion/scripts/32bit/backgrounds_to32bit.py new file mode 100644 index 0000000000..92ca18a82d --- /dev/null +++ b/pype/hosts/fusion/scripts/32bit/backgrounds_to32bit.py @@ -0,0 +1,12 @@ +from avalon.fusion import comp_lock_and_undo_chunk + + +def main(): + """Set all backgrounds to 32 bit""" + with comp_lock_and_undo_chunk(comp, 'Backgrounds to 32bit'): + tools = comp.GetToolList(False, "Background").values() + for tool in tools: + tool.Depth = 5 + + +main() diff --git a/pype/hosts/fusion/scripts/32bit/loaders_selected_to32bit.py b/pype/hosts/fusion/scripts/32bit/loaders_selected_to32bit.py new file mode 100644 index 0000000000..6e3802d9ff --- /dev/null +++ b/pype/hosts/fusion/scripts/32bit/loaders_selected_to32bit.py @@ -0,0 +1,12 @@ +from avalon.fusion import comp_lock_and_undo_chunk + + +def main(): + """Set all selected loaders to 32 bit""" + with comp_lock_and_undo_chunk(comp, 'Selected Loaders to 32bit'): + tools = comp.GetToolList(True, "Loader").values() + for tool in tools: + tool.Depth = 5 + + +main() diff --git a/pype/hosts/fusion/scripts/32bit/loaders_to32bit.py b/pype/hosts/fusion/scripts/32bit/loaders_to32bit.py new file mode 100644 index 0000000000..d86bef35f3 --- /dev/null +++ b/pype/hosts/fusion/scripts/32bit/loaders_to32bit.py @@ -0,0 +1,12 @@ +from avalon.fusion import comp_lock_and_undo_chunk + + +def main(): + """Set all loaders to 32 bit""" + with comp_lock_and_undo_chunk(comp, 'Loaders to 32bit'): + tools = comp.GetToolList(False, "Loader").values() + for tool in tools: + tool.Depth = 5 + + +main() diff --git a/pype/hosts/fusion/scripts/duplicate_with_input_connections.py b/pype/hosts/fusion/scripts/duplicate_with_input_connections.py new file mode 100644 index 0000000000..9f4f4a8f0a --- /dev/null +++ b/pype/hosts/fusion/scripts/duplicate_with_input_connections.py @@ -0,0 +1,43 @@ +from avalon.fusion import comp_lock_and_undo_chunk + + +def is_connected(input): + """Return whether an input has incoming connection""" + return input.GetAttrs()["INPB_Connected"] + + +def duplicate_with_input_connections(): + """Duplicate selected tools with incoming connections.""" + + original_tools = comp.GetToolList(True).values() + if not original_tools: + return # nothing selected + + with comp_lock_and_undo_chunk(comp, "Duplicate With Input Connections"): + + # Generate duplicates + comp.Copy() + comp.SetActiveTool() + comp.Paste() + duplicate_tools = comp.GetToolList(True).values() + + # Copy connections + for original, new in zip(original_tools, duplicate_tools): + + original_inputs = original.GetInputList().values() + new_inputs = new.GetInputList().values() + assert len(original_inputs) == len(new_inputs) + + for original_input, new_input in zip(original_inputs, new_inputs): + + if is_connected(original_input): + + if is_connected(new_input): + # Already connected if it is between the copied tools + continue + + new_input.ConnectTo(original_input.GetConnectedOutput()) + assert is_connected(new_input), "Must be connected now" + + +duplicate_with_input_connections() diff --git a/pype/hosts/fusion/scripts/set_rendermode.py b/pype/hosts/fusion/scripts/set_rendermode.py new file mode 100644 index 0000000000..4b2049e2e5 --- /dev/null +++ b/pype/hosts/fusion/scripts/set_rendermode.py @@ -0,0 +1,125 @@ +from avalon.vendor.Qt import QtCore, QtWidgets +from avalon.vendor import qtawesome +import avalon.fusion as avalon +from avalon import style + + +_help = {"renderlocal": "Render the comp on your own machine and publish " + "it from that the destination folder", + "deadline": "Submit a Fusion render job to Deadline to use all other " + "computers and add a publish job"} + + +class SetRenderMode(QtWidgets.QWidget): + + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + self._comp = avalon.get_current_comp() + self._comp_name = self._get_comp_name() + + self.setWindowTitle("Set Render Mode") + self.setFixedSize(300, 175) + + layout = QtWidgets.QVBoxLayout() + + # region comp info + comp_info_layout = QtWidgets.QHBoxLayout() + + update_btn = QtWidgets.QPushButton(qtawesome.icon("fa.refresh", + color="white"), "") + update_btn.setFixedWidth(25) + update_btn.setFixedHeight(25) + + comp_information = QtWidgets.QLineEdit() + comp_information.setEnabled(False) + + comp_info_layout.addWidget(comp_information) + comp_info_layout.addWidget(update_btn) + # endregion comp info + + # region modes + mode_options = QtWidgets.QComboBox() + mode_options.addItems(_help.keys()) + + mode_information = QtWidgets.QTextEdit() + mode_information.setReadOnly(True) + # endregion modes + + accept_btn = QtWidgets.QPushButton("Accept") + + layout.addLayout(comp_info_layout) + layout.addWidget(mode_options) + layout.addWidget(mode_information) + layout.addWidget(accept_btn) + + self.setLayout(layout) + + self.comp_information = comp_information + self.update_btn = update_btn + + self.mode_options = mode_options + self.mode_information = mode_information + + self.accept_btn = accept_btn + + self.connections() + self.update() + + # Force updated render mode help text + self._update_rendermode_info() + + def connections(self): + """Build connections between code and buttons""" + + self.update_btn.clicked.connect(self.update) + self.accept_btn.clicked.connect(self._set_comp_rendermode) + self.mode_options.currentIndexChanged.connect( + self._update_rendermode_info) + + def update(self): + """Update all information in the UI""" + + self._comp = avalon.get_current_comp() + self._comp_name = self._get_comp_name() + self.comp_information.setText(self._comp_name) + + # Update current comp settings + mode = self._get_comp_rendermode() + index = self.mode_options.findText(mode) + self.mode_options.setCurrentIndex(index) + + def _update_rendermode_info(self): + rendermode = self.mode_options.currentText() + self.mode_information.setText(_help[rendermode]) + + def _get_comp_name(self): + return self._comp.GetAttrs("COMPS_Name") + + def _get_comp_rendermode(self): + return self._comp.GetData("pype.rendermode") or "renderlocal" + + def _set_comp_rendermode(self): + rendermode = self.mode_options.currentText() + self._comp.SetData("pype.rendermode", rendermode) + + self._comp.Print("Updated render mode to '%s'\n" % rendermode) + + def _validation(self): + ui_mode = self.mode_options.currentText() + comp_mode = self._get_comp_rendermode() + + return comp_mode == ui_mode + + +def main(): + import sys + app = QtWidgets.QApplication(sys.argv) + window = SetRenderMode() + window.setStyleSheet(style.load_stylesheet()) + window.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() diff --git a/pype/hosts/fusion/scripts/switch_ui.py b/pype/hosts/fusion/scripts/switch_ui.py new file mode 100644 index 0000000000..8f1466abe0 --- /dev/null +++ b/pype/hosts/fusion/scripts/switch_ui.py @@ -0,0 +1,201 @@ +import os +import glob +import logging + +import avalon.io as io +import avalon.api as api +import avalon.pipeline as pipeline +import avalon.fusion +import avalon.style as style +from avalon.vendor.Qt import QtWidgets, QtCore +from avalon.vendor import qtawesome as qta + + +log = logging.getLogger("Fusion Switch Shot") + + +class App(QtWidgets.QWidget): + + def __init__(self, parent=None): + + ################################################ + # |---------------------| |------------------| # + # |Comp | |Asset | # + # |[..][ v]| |[ v]| # + # |---------------------| |------------------| # + # | Update existing comp [ ] | # + # |------------------------------------------| # + # | Switch | # + # |------------------------------------------| # + ################################################ + + QtWidgets.QWidget.__init__(self, parent) + + layout = QtWidgets.QVBoxLayout() + + # Comp related input + comp_hlayout = QtWidgets.QHBoxLayout() + comp_label = QtWidgets.QLabel("Comp file") + comp_label.setFixedWidth(50) + comp_box = QtWidgets.QComboBox() + + button_icon = qta.icon("fa.folder", color="white") + open_from_dir = QtWidgets.QPushButton() + open_from_dir.setIcon(button_icon) + + comp_box.setFixedHeight(25) + open_from_dir.setFixedWidth(25) + open_from_dir.setFixedHeight(25) + + comp_hlayout.addWidget(comp_label) + comp_hlayout.addWidget(comp_box) + comp_hlayout.addWidget(open_from_dir) + + # Asset related input + asset_hlayout = QtWidgets.QHBoxLayout() + asset_label = QtWidgets.QLabel("Shot") + asset_label.setFixedWidth(50) + + asset_box = QtWidgets.QComboBox() + asset_box.setLineEdit(QtWidgets.QLineEdit()) + asset_box.setFixedHeight(25) + + refresh_icon = qta.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton() + refresh_btn.setIcon(refresh_icon) + + asset_box.setFixedHeight(25) + refresh_btn.setFixedWidth(25) + refresh_btn.setFixedHeight(25) + + asset_hlayout.addWidget(asset_label) + asset_hlayout.addWidget(asset_box) + asset_hlayout.addWidget(refresh_btn) + + # Options + options = QtWidgets.QHBoxLayout() + options.setAlignment(QtCore.Qt.AlignLeft) + + current_comp_check = QtWidgets.QCheckBox() + current_comp_check.setChecked(True) + current_comp_label = QtWidgets.QLabel("Use current comp") + + options.addWidget(current_comp_label) + options.addWidget(current_comp_check) + + accept_btn = QtWidgets.QPushButton("Switch") + + layout.addLayout(options) + layout.addLayout(comp_hlayout) + layout.addLayout(asset_hlayout) + layout.addWidget(accept_btn) + + self._open_from_dir = open_from_dir + self._comps = comp_box + self._assets = asset_box + self._use_current = current_comp_check + self._accept_btn = accept_btn + self._refresh_btn = refresh_btn + + self.setWindowTitle("Fusion Switch Shot") + self.setLayout(layout) + + self.resize(260, 140) + self.setMinimumWidth(260) + self.setFixedHeight(140) + + self.connections() + + # Update ui to correct state + self._on_use_current_comp() + self._refresh() + + def connections(self): + self._use_current.clicked.connect(self._on_use_current_comp) + self._open_from_dir.clicked.connect(self._on_open_from_dir) + self._refresh_btn.clicked.connect(self._refresh) + self._accept_btn.clicked.connect(self._on_switch) + + def _on_use_current_comp(self): + state = self._use_current.isChecked() + self._open_from_dir.setEnabled(not state) + self._comps.setEnabled(not state) + + def _on_open_from_dir(self): + + start_dir = self._get_context_directory() + comp_file, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Choose comp", start_dir) + + if not comp_file: + return + + # Create completer + self.populate_comp_box([comp_file]) + self._refresh() + + def _refresh(self): + # Clear any existing items + self._assets.clear() + + asset_names = [a["name"] for a in self.collect_assets()] + completer = QtWidgets.QCompleter(asset_names) + + self._assets.setCompleter(completer) + self._assets.addItems(asset_names) + + def _on_switch(self): + + if not self._use_current.isChecked(): + file_name = self._comps.itemData(self._comps.currentIndex()) + else: + comp = avalon.fusion.get_current_comp() + file_name = comp.GetAttrs("COMPS_FileName") + + asset = self._assets.currentText() + + import colorbleed.scripts.fusion_switch_shot as switch_shot + switch_shot.switch(asset_name=asset, filepath=file_name, new=True) + + def _get_context_directory(self): + + project = io.find_one({"type": "project", + "name": api.Session["AVALON_PROJECT"]}, + projection={"config": True}) + + template = project["config"]["template"]["work"] + dir = pipeline._format_work_template(template, api.Session) + + return dir + + def collect_slap_comps(self, directory): + items = glob.glob("{}/*.comp".format(directory)) + return items + + def collect_assets(self): + return list(io.find({"type": "asset", "silo": "film"})) + + def populate_comp_box(self, files): + """Ensure we display the filename only but the path is stored as well + + Args: + files (list): list of full file path [path/to/item/item.ext,] + + Returns: + None + """ + + for f in files: + filename = os.path.basename(f) + self._comps.addItem(filename, userData=f) + + +if __name__ == '__main__': + import sys + api.install(avalon.fusion) + + app = QtWidgets.QApplication(sys.argv) + window = App() + window.setStyleSheet(style.load_stylesheet()) + window.show() + sys.exit(app.exec_()) diff --git a/pype/hosts/fusion/scripts/update_selected_loader_ranges.py b/pype/hosts/fusion/scripts/update_selected_loader_ranges.py new file mode 100644 index 0000000000..f42a032e84 --- /dev/null +++ b/pype/hosts/fusion/scripts/update_selected_loader_ranges.py @@ -0,0 +1,32 @@ +"""Forces Fusion to 'retrigger' the Loader to update. + +Warning: + This might change settings like 'Reverse', 'Loop', trims and other + settings of the Loader. So use this at your own risk. + +""" + +from avalon.fusion import comp_lock_and_undo_chunk + + +with comp_lock_and_undo_chunk(comp, "Reload clip time ranges"): + tools = comp.GetToolList(True, "Loader").values() + for tool in tools: + + # Get tool attributes + tool_a = tool.GetAttrs() + clipTable = tool_a['TOOLST_Clip_Name'] + altclipTable = tool_a['TOOLST_AltClip_Name'] + startTime = tool_a['TOOLNT_Clip_Start'] + old_global_in = tool.GlobalIn[comp.CurrentTime] + + # Reapply + for index, _ in clipTable.items(): + time = startTime[index] + tool.Clip[time] = tool.Clip[time] + + for index, _ in altclipTable.items(): + time = startTime[index] + tool.ProxyFilename[time] = tool.ProxyFilename[time] + + tool.GlobalIn[comp.CurrentTime] = old_global_in diff --git a/pype/hosts/fusion/utility_scripts/Pype_menu.py b/pype/hosts/fusion/utility_scripts/Pype_menu.py index bf42c75cde..3d6a4607e1 100644 --- a/pype/hosts/fusion/utility_scripts/Pype_menu.py +++ b/pype/hosts/fusion/utility_scripts/Pype_menu.py @@ -1,6 +1,5 @@ import os import sys -import avalon.api as avalon import pype from pypeapp import Logger @@ -9,16 +8,17 @@ log = Logger().get_logger(__name__) def main(env): - from pype.hosts import fusion + from pype.hosts.fusion import menu + import avalon.fusion # Registers pype's Global pyblish plugins pype.install() # activate resolve from pype - avalon.install(bmdvr) + avalon.api.install(avalon.fusion) - log.info(f"Avalon registred hosts: {avalon.registered_host()}") + log.info(f"Avalon registred hosts: {avalon.api.registered_host()}") - bmdvr.launch_pype_menu() + menu.launch_pype_menu() if __name__ == "__main__": diff --git a/pype/hosts/fusion/utils.py b/pype/hosts/fusion/utils.py index e57723d43e..cb2098eaee 100644 --- a/pype/hosts/fusion/utils.py +++ b/pype/hosts/fusion/utils.py @@ -58,7 +58,10 @@ def _sync_utility_scripts(env=None): src = os.path.join(d, s) dst = os.path.join(us_dir, s) log.info(f"Copying `{src}` to `{dst}`...") - shutil.copy2(src, dst) + if not os.path.isdir(src): + shutil.copy2(src, dst) + else: + shutil.copytree(src, dst) def setup(env=None): diff --git a/pype/plugins/fusion/create/create_tiff_saver.py b/pype/plugins/fusion/create/create_tiff_saver.py index 4911650ed2..92f97366a3 100644 --- a/pype/plugins/fusion/create/create_tiff_saver.py +++ b/pype/plugins/fusion/create/create_tiff_saver.py @@ -23,7 +23,7 @@ class CreateTiffSaver(avalon.api.Creator): workdir = os.path.normpath(os.environ["AVALON_WORKDIR"]) filename = "{}..tiff".format(self.name) - filepath = os.path.join(workdir, "render", "preview", filename) + filepath = os.path.join(workdir, "render", filename) with fusion.comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers @@ -43,4 +43,3 @@ class CreateTiffSaver(avalon.api.Creator): # Set file format attributes saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 -