diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index cea1bdc023..2ad1255d27 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -16,7 +16,10 @@ from .pipeline import ( uninstall, list_instances, remove_instance, - containerise + containerise, + get_context_data, + update_context_data, + get_context_title ) from .workio import ( @@ -51,6 +54,9 @@ __all__ = [ "list_instances", "remove_instance", "containerise", + "get_context_data", + "update_context_data", + "get_context_title", "file_extensions", "has_unsaved_changes", diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index 389d74505d..0ed799991e 100644 Binary files a/openpype/hosts/aftereffects/api/extension.zxp and b/openpype/hosts/aftereffects/api/extension.zxp differ diff --git a/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml index 668cb3fc24..a39f5781bb 100644 --- a/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml +++ b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml @@ -1,5 +1,5 @@ - diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 8f82c9709d..91df433908 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -417,7 +417,9 @@ function getRenderInfo(){ var file_url = item.file.toString(); return JSON.stringify({ - "file_name": file_url + "file_name": file_url, + "width": render_item.comp.width, + "height": render_item.comp.height }) } diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 3ed2de0e9d..b26244f131 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -2,7 +2,6 @@ import os import sys from Qt import QtWidgets -from bson.objectid import ObjectId import pyblish.api from avalon import io @@ -34,24 +33,6 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -def check_inventory(): - if not lib.any_outdated(): - return - - # Warn about outdated containers. - print("Starting new QApplication..") - app = QtWidgets.QApplication(sys.argv) - message_box = QtWidgets.QMessageBox() - message_box.setIcon(QtWidgets.QMessageBox.Warning) - msg = "There are outdated containers in the scene." - message_box.setText(msg) - message_box.exec_() - - -def application_launch(): - check_inventory() - - def install(): print("Installing Pype config...") @@ -75,6 +56,11 @@ def uninstall(): deregister_creator_plugin_path(CREATE_PATH) +def application_launch(): + """Triggered after start of app""" + check_inventory() + + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle layer visibility on instance toggles.""" instance[0].Visible = new_value @@ -109,65 +95,6 @@ def get_asset_settings(): } -def containerise(name, - namespace, - comp, - context, - loader=None, - suffix="_CON"): - """ - Containerisation enables a tracking of version, author and origin - for loaded assets. - - Creates dictionary payloads that gets saved into file metadata. Each - container contains of who loaded (loader) and members (single or multiple - in case of background). - - Arguments: - name (str): Name of resulting assembly - namespace (str): Namespace under which to host container - comp (Comp): Composition to containerise - context (dict): Asset information - loader (str, optional): Name of loader used to produce this container. - suffix (str, optional): Suffix of container, defaults to `_CON`. - - Returns: - container (str): Name of container assembly - """ - data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, - "name": name, - "namespace": namespace, - "loader": str(loader), - "representation": str(context["representation"]["_id"]), - "members": comp.members or [comp.id] - } - - stub = get_stub() - stub.imprint(comp, data) - - return comp - - -def _get_stub(): - """ - Handle pulling stub from PS to run operations on host - Returns: - (AEServerStub) or None - """ - try: - stub = get_stub() # only after Photoshop is up - except lib.ConnectionNotEstablishedYet: - print("Not connected yet, ignoring") - return - - if not stub.get_active_document_name(): - return - - return stub - - def ls(): """Yields containers from active AfterEffects document. @@ -208,6 +135,78 @@ def ls(): yield data +def check_inventory(): + """Checks loaded containers if they are of highest version""" + if not lib.any_outdated(): + return + + host = pyblish.api.registered_host() + outdated_containers = [] + for container in host.ls(): + representation = container['representation'] + representation_doc = io.find_one( + { + "_id": io.ObjectId(representation), + "type": "representation" + }, + projection={"parent": True} + ) + if representation_doc and not lib.is_latest(representation_doc): + outdated_containers.append(container) + + # Warn about outdated containers. + print("Starting new QApplication..") + _app = QtWidgets.QApplication(sys.argv) + + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() + + +def containerise(name, + namespace, + comp, + context, + loader=None, + suffix="_CON"): + """ + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Creates dictionary payloads that gets saved into file metadata. Each + container contains of who loaded (loader) and members (single or multiple + in case of background). + + Arguments: + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + comp (AEItem): Composition to containerise + context (dict): Asset information + loader (str, optional): Name of loader used to produce this container. + suffix (str, optional): Suffix of container, defaults to `_CON`. + + Returns: + container (str): Name of container assembly + """ + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace, + "loader": str(loader), + "representation": str(context["representation"]["_id"]), + "members": comp.members or [comp.id] + } + + stub = get_stub() + stub.imprint(comp.id, data) + + return comp + + +# created instances section def list_instances(): """ List all created instances from current workfile which @@ -228,16 +227,8 @@ def list_instances(): layers_meta = stub.get_metadata() for instance in layers_meta: - if instance.get("schema") and \ - "container" in instance.get("schema"): - continue - - uuid_val = instance.get("uuid") - if uuid_val: - instance['uuid'] = uuid_val - else: - instance['uuid'] = instance.get("members")[0] # legacy - instances.append(instance) + if instance.get("id") == "pyblish.avalon.instance": + instances.append(instance) return instances @@ -258,8 +249,60 @@ def remove_instance(instance): if not stub: return - stub.remove_instance(instance.get("uuid")) - item = stub.get_item(instance.get("uuid")) - if item: - stub.rename_item(item.id, - item.name.replace(stub.PUBLISH_ICON, '')) + inst_id = instance.get("instance_id") or instance.get("uuid") # legacy + if not inst_id: + log.warning("No instance identifier for {}".format(instance)) + return + + stub.remove_instance(inst_id) + + if instance.get("members"): + item = stub.get_item(instance["members"][0]) + if item: + stub.rename_item(item.id, + item.name.replace(stub.PUBLISH_ICON, '')) + + +# new publisher section +def get_context_data(): + meta = _get_stub().get_metadata() + for item in meta: + if item.get("id") == "publish_context": + item.pop("id") + return item + + return {} + + +def update_context_data(data, changes): + item = data + item["id"] = "publish_context" + _get_stub().imprint(item["id"], item) + + +def get_context_title(): + """Returns title for Creator window""" + import avalon.api + + project_name = avalon.api.Session["AVALON_PROJECT"] + asset_name = avalon.api.Session["AVALON_ASSET"] + task_name = avalon.api.Session["AVALON_TASK"] + return "{}/{}/{}".format(project_name, asset_name, task_name) + + +def _get_stub(): + """ + Handle pulling stub from PS to run operations on host + Returns: + (AEServerStub) or None + """ + try: + stub = get_stub() # only after Photoshop is up + except lib.ConnectionNotEstablishedYet: + print("Not connected yet, ignoring") + return + + if not stub.get_active_document_name(): + return + + return stub diff --git a/openpype/hosts/aftereffects/api/workio.py b/openpype/hosts/aftereffects/api/workio.py index 70815bda6b..d6c732285a 100644 --- a/openpype/hosts/aftereffects/api/workio.py +++ b/openpype/hosts/aftereffects/api/workio.py @@ -51,4 +51,4 @@ def _active_document(): print("Nothing opened") pass - return document_name \ No newline at end of file + return document_name diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index b0893310c1..8719a8f46e 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -28,6 +28,9 @@ class AEItem(object): workAreaDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) + instance_id = attr.ib(default=None) # New Publisher + width = attr.ib(default=None) + height = attr.ib(default=None) class AfterEffectsServerStub(): @@ -110,11 +113,11 @@ class AfterEffectsServerStub(): self.log.debug("Couldn't find layer metadata") - def imprint(self, item, data, all_items=None, items_meta=None): + def imprint(self, item_id, data, all_items=None, items_meta=None): """ Save item metadata to Label field of metadata of active document Args: - item (AEItem): + item_id (int|str): id of FootageItem or instance_id for workfiles data(string): json representation for single layer all_items (list of item): for performance, could be injected for usage in loop, if not, single call will be @@ -132,8 +135,9 @@ class AfterEffectsServerStub(): is_new = True for item_meta in items_meta: - if item_meta.get('members') \ - and str(item.id) == str(item_meta.get('members')[0]): + if ((item_meta.get('members') and + str(item_id) == str(item_meta.get('members')[0])) or + item_meta.get("instance_id") == item_id): is_new = False if data: item_meta.update(data) @@ -153,10 +157,12 @@ class AfterEffectsServerStub(): item_ids = [int(item.id) for item in all_items] cleaned_data = [] for meta in result_meta: - # for creation of instance OR loaded container - if 'instance' in meta.get('id') or \ - int(meta.get('members')[0]) in item_ids: - cleaned_data.append(meta) + # do not added instance with nonexistend item id + if meta.get("members"): + if int(meta["members"][0]) not in item_ids: + continue + + cleaned_data.append(meta) payload = json.dumps(cleaned_data, indent=4) @@ -167,7 +173,7 @@ class AfterEffectsServerStub(): def get_active_document_full_name(self): """ - Returns just a name of active document via ws call + Returns absolute path of active document via ws call Returns(string): file name """ res = self.websocketserver.call(self.client.call( @@ -314,15 +320,13 @@ class AfterEffectsServerStub(): Keep matching item in file though. Args: - instance_id(string): instance uuid + instance_id(string): instance id """ cleaned_data = [] for instance in self.get_metadata(): - uuid_val = instance.get("uuid") - if not uuid_val: - uuid_val = instance.get("members")[0] # legacy - if uuid_val != instance_id: + inst_id = instance.get("instance_id") or instance.get("uuid") + if inst_id != instance_id: cleaned_data.append(instance) payload = json.dumps(cleaned_data, indent=4) @@ -357,7 +361,7 @@ class AfterEffectsServerStub(): item_id (int): Returns: - (namedtuple) + (AEItem) """ res = self.websocketserver.call(self.client.call @@ -418,7 +422,7 @@ class AfterEffectsServerStub(): """ Get render queue info for render purposes Returns: - (namedtuple): with 'file_name' field + (AEItem): with 'file_name' field """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_render_info')) @@ -606,7 +610,10 @@ class AfterEffectsServerStub(): d.get('workAreaStart'), d.get('workAreaDuration'), d.get('frameRate'), - d.get('file_name')) + d.get('file_name'), + d.get("instance_id"), + d.get("width"), + d.get("height")) ret.append(item) return ret diff --git a/openpype/hosts/aftereffects/plugins/create/create_local_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py similarity index 64% rename from openpype/hosts/aftereffects/plugins/create/create_local_render.py rename to openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py index 9d2cdcd7be..04413acbcf 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_local_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py @@ -1,7 +1,7 @@ -from openpype.hosts.aftereffects.plugins.create import create_render +from openpype.hosts.aftereffects.plugins.create import create_legacy_render -class CreateLocalRender(create_render.CreateRender): +class CreateLocalRender(create_legacy_render.CreateRender): """ Creator to render locally. Created only after default render on farm. So family 'render.local' is diff --git a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py new file mode 100644 index 0000000000..e4fbb47a33 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py @@ -0,0 +1,62 @@ +from openpype.pipeline import create +from openpype.pipeline import CreatorError +from openpype.hosts.aftereffects.api import ( + get_stub, + list_instances +) + + +class CreateRender(create.LegacyCreator): + """Render folder for publish. + + Creates subsets in format 'familyTaskSubsetname', + eg 'renderCompositingMain'. + + Create only single instance from composition at a time. + """ + + name = "renderDefault" + label = "Render on Farm" + family = "render" + defaults = ["Main"] + + def process(self): + stub = get_stub() # only after After Effects is up + items = [] + if (self.options or {}).get("useSelection"): + items = stub.get_selected_items( + comps=True, folders=False, footages=False + ) + if len(items) > 1: + raise CreatorError( + "Please select only single composition at time." + ) + + if not items: + raise CreatorError(( + "Nothing to create. Select composition " + "if 'useSelection' or create at least " + "one composition." + )) + + existing_subsets = [ + instance['subset'].lower() + for instance in list_instances() + ] + + item = items.pop() + if self.name.lower() in existing_subsets: + txt = "Instance with name \"{}\" already exists.".format(self.name) + raise CreatorError(txt) + + self.data["members"] = [item.id] + self.data["uuid"] = item.id # for SubsetManager + self.data["subset"] = ( + self.data["subset"] + .replace(stub.PUBLISH_ICON, '') + .replace(stub.LOADED_ICON, '') + ) + + stub.imprint(item, self.data) + stub.set_label_color(item.id, 14) # Cyan options 0 - 16 + stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 831085a5f1..8bddbb2e99 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,38 +1,61 @@ +from avalon import api as avalon_api + +from openpype import resources +from openpype.lib import BoolDef, UISeparatorDef +from openpype.hosts.aftereffects import api from openpype.pipeline import ( - CreatorError, - LegacyCreator -) -from openpype.hosts.aftereffects.api import ( - get_stub, - list_instances + Creator, + CreatedInstance, + CreatorError ) -class CreateRender(LegacyCreator): - """Render folder for publish. - - Creates subsets in format 'familyTaskSubsetname', - eg 'renderCompositingMain'. - - Create only single instance from composition at a time. - """ - - name = "renderDefault" - label = "Render on Farm" +class RenderCreator(Creator): + identifier = "render" + label = "Render" family = "render" - defaults = ["Main"] + description = "Render creator" - def process(self): - stub = get_stub() # only after After Effects is up - if (self.options or {}).get("useSelection"): + create_allow_context_change = True + + def get_icon(self): + return resources.get_openpype_splash_filepath() + + def collect_instances(self): + for instance_data in api.list_instances(): + # legacy instances have family=='render' or 'renderLocal', use them + creator_id = (instance_data.get("creator_identifier") or + instance_data.get("family", '').replace("Local", '')) + if creator_id == self.identifier: + instance_data = self._handle_legacy(instance_data) + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + api.get_stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) + + def remove_instances(self, instances): + for instance in instances: + api.remove_instance(instance) + self._remove_instance_from_context(instance) + + def create(self, subset_name, data, pre_create_data): + stub = api.get_stub() # only after After Effects is up + if pre_create_data.get("use_selection"): items = stub.get_selected_items( comps=True, folders=False, footages=False ) + else: + items = stub.get_items(comps=True, folders=False, footages=False) + if len(items) > 1: raise CreatorError( "Please select only single composition at time." ) - if not items: raise CreatorError(( "Nothing to create. Select composition " @@ -40,24 +63,54 @@ class CreateRender(LegacyCreator): "one composition." )) - existing_subsets = [ - instance['subset'].lower() - for instance in list_instances() + for inst in self.create_context.instances: + if subset_name == inst.subset_name: + raise CreatorError("{} already exists".format( + inst.subset_name)) + + data["members"] = [items[0].id] + new_instance = CreatedInstance(self.family, subset_name, data, self) + if "farm" in pre_create_data: + use_farm = pre_create_data["farm"] + new_instance.creator_attributes["farm"] = use_farm + + api.get_stub().imprint(new_instance.id, + new_instance.data_to_store()) + self._add_instance_to_context(new_instance) + + def get_default_variants(self): + return ["Main"] + + def get_instance_attr_defs(self): + return [BoolDef("farm", label="Render on farm")] + + def get_pre_create_attr_defs(self): + output = [ + BoolDef("use_selection", default=True, label="Use selection"), + UISeparatorDef(), + BoolDef("farm", label="Render on farm") ] + return output - item = items.pop() - if self.name.lower() in existing_subsets: - txt = "Instance with name \"{}\" already exists.".format(self.name) - raise CreatorError(txt) + def get_detail_description(self): + return """Creator for Render instances""" - self.data["members"] = [item.id] - self.data["uuid"] = item.id # for SubsetManager - self.data["subset"] = ( - self.data["subset"] - .replace(stub.PUBLISH_ICON, '') - .replace(stub.LOADED_ICON, '') - ) + def _handle_legacy(self, instance_data): + """Converts old instances to new format.""" + if not instance_data.get("members"): + instance_data["members"] = [instance_data.get("uuid")] - stub.imprint(item, self.data) - stub.set_label_color(item.id, 14) # Cyan options 0 - 16 - stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) + if instance_data.get("uuid"): + # uuid not needed, replaced with unique instance_id + api.get_stub().remove_instance(instance_data.get("uuid")) + instance_data.pop("uuid") + + if not instance_data.get("task"): + instance_data["task"] = avalon_api.Session.get("AVALON_TASK") + + if not instance_data.get("creator_attributes"): + is_old_farm = instance_data["family"] != "renderLocal" + instance_data["creator_attributes"] = {"farm": is_old_farm} + instance_data["family"] = self.family + + return instance_data diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py new file mode 100644 index 0000000000..2d9d42ee8c --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -0,0 +1,75 @@ +from avalon import io + +import openpype.hosts.aftereffects.api as api +from openpype.pipeline import ( + AutoCreator, + CreatedInstance +) + + +class AEWorkfileCreator(AutoCreator): + identifier = "workfile" + family = "workfile" + + def get_instance_attr_defs(self): + return [] + + def collect_instances(self): + for instance_data in api.list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + subset_name = instance_data["subset"] + instance = CreatedInstance( + self.family, subset_name, instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + # nothing to change on workfiles + pass + + def create(self, options=None): + existing_instance = None + for instance in self.create_context.instances: + if instance.family == self.family: + existing_instance = instance + break + + variant = '' + project_name = io.Session["AVALON_PROJECT"] + asset_name = io.Session["AVALON_ASSET"] + task_name = io.Session["AVALON_TASK"] + host_name = io.Session["AVALON_APP"] + + if existing_instance is None: + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + data.update(self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name + )) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + + api.get_stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name diff --git a/openpype/hosts/aftereffects/plugins/load/load_background.py b/openpype/hosts/aftereffects/plugins/load/load_background.py index be43cae44e..d346df504a 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_background.py +++ b/openpype/hosts/aftereffects/plugins/load/load_background.py @@ -90,7 +90,7 @@ class BackgroundLoader(AfterEffectsLoader): container["namespace"] = comp_name container["members"] = comp.members - stub.imprint(comp, container) + stub.imprint(comp.id, container) def remove(self, container): """ @@ -99,10 +99,9 @@ class BackgroundLoader(AfterEffectsLoader): Args: container (dict): container to be removed - used to get layer_id """ - print("!!!! container:: {}".format(container)) stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_item(layer.id) def switch(self, container, representation): diff --git a/openpype/hosts/aftereffects/plugins/load/load_file.py b/openpype/hosts/aftereffects/plugins/load/load_file.py index 9eb9e80a2c..6ab69c6bfa 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_file.py +++ b/openpype/hosts/aftereffects/plugins/load/load_file.py @@ -96,9 +96,9 @@ class FileLoader(AfterEffectsLoader): # with aftereffects.maintained_selection(): # TODO stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name) stub.imprint( - layer, {"representation": str(representation["_id"]), - "name": context["subset"], - "namespace": layer_name} + layer.id, {"representation": str(representation["_id"]), + "name": context["subset"], + "namespace": layer_name} ) def remove(self, container): @@ -109,7 +109,7 @@ class FileLoader(AfterEffectsLoader): """ stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_item(layer.id) def switch(self, container, representation): diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_audio.py b/openpype/hosts/aftereffects/plugins/publish/collect_audio.py index 80679725e6..8647ba498b 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_audio.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_audio.py @@ -17,12 +17,11 @@ class CollectAudio(pyblish.api.ContextPlugin): def process(self, context): for instance in context: - if instance.data["family"] == 'render.farm': + if 'render.farm' in instance.data.get("families", []): comp_id = instance.data["comp_id"] if not comp_id: self.log.debug("No comp_id filled in instance") - # @iLLiCiTiT QUESTION Should return or continue? - return + continue context.data["audioFile"] = os.path.normpath( get_stub().get_audio_url(comp_id) ).replace("\\", "/") diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 3e44acd7e9..fa23bf92b0 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -21,135 +21,129 @@ class AERenderInstance(RenderInstance): projectEntity = attr.ib(default=None) stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) + publish_attributes = attr.ib(default=None) + file_name = attr.ib(default=None) class CollectAERender(abstract_collect_render.AbstractCollectRender): - order = pyblish.api.CollectorOrder + 0.400 + order = pyblish.api.CollectorOrder + 0.405 label = "Collect After Effects Render Layers" hosts = ["aftereffects"] - # internal - family_remapping = { - "render": ("render.farm", "farm"), # (family, label) - "renderLocal": ("render", "local") - } padding_width = 6 rendered_extension = 'png' - stub = get_stub() + _stub = None + + @classmethod + def get_stub(cls): + if not cls._stub: + cls._stub = get_stub() + return cls._stub def get_instances(self, context): instances = [] + instances_to_remove = [] - app_version = self.stub.get_app_version() + app_version = CollectAERender.get_stub().get_app_version() app_version = app_version[0:4] current_file = context.data["currentFile"] version = context.data["version"] - asset_entity = context.data["assetEntity"] + project_entity = context.data["projectEntity"] - compositions = self.stub.get_items(True) + compositions = CollectAERender.get_stub().get_items(True) compositions_by_id = {item.id: item for item in compositions} - for inst in self.stub.get_metadata(): - schema = inst.get('schema') - # loaded asset container skip it - if schema and 'container' in schema: + for inst in context: + if not inst.data.get("active", True): continue - if not inst["members"]: - raise ValueError("Couldn't find id, unable to publish. " + - "Please recreate instance.") - item_id = inst["members"][0] + family = inst.data["family"] + if family not in ["render", "renderLocal"]: # legacy + continue - work_area_info = self.stub.get_work_area(int(item_id)) + item_id = inst.data["members"][0] + + work_area_info = CollectAERender.get_stub().get_work_area( + int(item_id)) if not work_area_info: self.log.warning("Orphaned instance, deleting metadata") - self.stub.remove_instance(int(item_id)) + inst_id = inst.get("instance_id") or item_id + CollectAERender.get_stub().remove_instance(inst_id) continue - frameStart = work_area_info.workAreaStart - - frameEnd = round(work_area_info.workAreaStart + - float(work_area_info.workAreaDuration) * - float(work_area_info.frameRate)) - 1 + frame_start = work_area_info.workAreaStart + frame_end = round(work_area_info.workAreaStart + + float(work_area_info.workAreaDuration) * + float(work_area_info.frameRate)) - 1 fps = work_area_info.frameRate # TODO add resolution when supported by extension - if inst["family"] in self.family_remapping.keys() \ - and inst["active"]: - remapped_family = self.family_remapping[inst["family"]] - instance = AERenderInstance( - family=remapped_family[0], - families=[remapped_family[0]], - version=version, - time="", - source=current_file, - label="{} - {}".format(inst["subset"], remapped_family[1]), - subset=inst["subset"], - asset=context.data["assetEntity"]["name"], - attachTo=False, - setMembers='', - publish=True, - renderer='aerender', - name=inst["subset"], - resolutionWidth=asset_entity["data"].get( - "resolutionWidth", - project_entity["data"]["resolutionWidth"]), - resolutionHeight=asset_entity["data"].get( - "resolutionHeight", - project_entity["data"]["resolutionHeight"]), - pixelAspect=1, - tileRendering=False, - tilesX=0, - tilesY=0, - frameStart=frameStart, - frameEnd=frameEnd, - frameStep=1, - toBeRenderedOn='deadline', - fps=fps, - app_version=app_version - ) + task_name = inst.data.get("task") # legacy - comp = compositions_by_id.get(int(item_id)) - if not comp: - raise ValueError("There is no composition for item {}". - format(item_id)) - instance.comp_name = comp.name - instance.comp_id = item_id - instance._anatomy = context.data["anatomy"] - instance.anatomyData = context.data["anatomyData"] + render_q = CollectAERender.get_stub().get_render_info() + if not render_q: + raise ValueError("No file extension set in Render Queue") - instance.outputDir = self._get_output_dir(instance) - instance.context = context + subset_name = inst.data["subset"] + instance = AERenderInstance( + family=family, + families=inst.data.get("families", []), + version=version, + time="", + source=current_file, + label="{} - {}".format(subset_name, family), + subset=subset_name, + asset=inst.data["asset"], + task=task_name, + attachTo=False, + setMembers='', + publish=True, + renderer='aerender', + name=subset_name, + resolutionWidth=render_q.width, + resolutionHeight=render_q.height, + pixelAspect=1, + tileRendering=False, + tilesX=0, + tilesY=0, + frameStart=frame_start, + frameEnd=frame_end, + frameStep=1, + toBeRenderedOn='deadline', + fps=fps, + app_version=app_version, + publish_attributes=inst.data.get("publish_attributes"), + file_name=render_q.file_name + ) - settings = get_project_settings(os.getenv("AVALON_PROJECT")) - reviewable_subset_filter = \ - (settings["deadline"] - ["publish"] - ["ProcessSubmittedJobOnFarm"] - ["aov_filter"]) + comp = compositions_by_id.get(int(item_id)) + if not comp: + raise ValueError("There is no composition for item {}". + format(item_id)) + instance.outputDir = self._get_output_dir(instance) + instance.comp_name = comp.name + instance.comp_id = item_id - if inst["family"] == "renderLocal": - # for local renders - instance.anatomyData["version"] = instance.version - instance.anatomyData["subset"] = instance.subset - instance.stagingDir = tempfile.mkdtemp() - instance.projectEntity = project_entity + is_local = "renderLocal" in inst.data["family"] # legacy + if inst.data.get("creator_attributes"): + is_local = not inst.data["creator_attributes"].get("farm") + if is_local: + # for local renders + instance = self._update_for_local(instance, project_entity) + else: + fam = "render.farm" + if fam not in instance.families: + instance.families.append(fam) - if self.hosts[0] in reviewable_subset_filter.keys(): - for aov_pattern in \ - reviewable_subset_filter[self.hosts[0]]: - if re.match(aov_pattern, instance.subset): - instance.families.append("review") - instance.review = True - break - - self.log.info("New instance:: {}".format(instance)) - instances.append(instance) + instances.append(instance) + instances_to_remove.append(inst) + for instance in instances_to_remove: + context.remove(instance) return instances def get_expected_files(self, render_instance): @@ -168,15 +162,11 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): start = render_instance.frameStart end = render_instance.frameEnd - # pull file name from Render Queue Output module - render_q = self.stub.get_render_info() - if not render_q: - raise ValueError("No file extension set in Render Queue") - _, ext = os.path.splitext(os.path.basename(render_q.file_name)) + _, ext = os.path.splitext(os.path.basename(render_instance.file_name)) base_dir = self._get_output_dir(render_instance) expected_files = [] - if "#" not in render_q.file_name: # single frame (mov)W + if "#" not in render_instance.file_name: # single frame (mov)W path = os.path.join(base_dir, "{}_{}_{}.{}".format( render_instance.asset, render_instance.subset, @@ -216,3 +206,24 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): # for submit_publish_job return base_dir + + def _update_for_local(self, instance, project_entity): + """Update old saved instances to current publishing format""" + instance.stagingDir = tempfile.mkdtemp() + instance.projectEntity = project_entity + fam = "render.local" + if fam not in instance.families: + instance.families.append(fam) + + settings = get_project_settings(os.getenv("AVALON_PROJECT")) + reviewable_subset_filter = (settings["deadline"] + ["publish"] + ["ProcessSubmittedJobOnFarm"] + ["aov_filter"].get(self.hosts[0])) + for aov_pattern in reviewable_subset_filter: + if re.match(aov_pattern, instance.subset): + instance.families.append("review") + instance.review = True + break + + return instance diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index cb5a2bad4f..64a81b58eb 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -11,16 +11,45 @@ class CollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.1 def process(self, context): - task = api.Session["AVALON_TASK"] + existing_instance = None + for instance in context: + if instance.data["family"] == "workfile": + self.log.debug("Workfile instance found, won't create new") + existing_instance = instance + break + current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) + if existing_instance is None: # old publish + instance = self._get_new_instance(context, scene_file) + else: + instance = existing_instance + + # creating representation + representation = { + 'name': 'aep', + 'ext': 'aep', + 'files': scene_file, + "stagingDir": staging_dir, + } + + if not instance.data.get("representations"): + instance.data["representations"] = [] + instance.data["representations"].append(representation) + + instance.data["publish"] = instance.data["active"] # for DL + + def _get_new_instance(self, context, scene_file): + task = api.Session["AVALON_TASK"] version = context.data["version"] asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - shared_instance_data = { + instance_data = { + "active": True, "asset": asset_entity["name"], + "task": task, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], @@ -59,20 +88,6 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "representations": list() }) - # adding basic script data - instance.data.update(shared_instance_data) + instance.data.update(instance_data) - # creating representation - representation = { - 'name': 'aep', - 'ext': 'aep', - 'files': scene_file, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(representation) - - self.log.info('Publishing After Effects workfile') - - for i in context: - self.log.debug(f"{i.data['families']}") + return instance diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index b738068a7b..7323a0b125 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -12,7 +12,7 @@ class ExtractLocalRender(openpype.api.Extractor): order = openpype.api.Extractor.order - 0.47 label = "Extract Local Render" hosts = ["aftereffects"] - families = ["render"] + families = ["renderLocal", "render.local"] def process(self, instance): stub = get_stub() diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py b/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py index e20598b311..eb2977309f 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py @@ -1,15 +1,16 @@ +import pyblish.api + import openpype.api from openpype.hosts.aftereffects.api import get_stub -class ExtractSaveScene(openpype.api.Extractor): +class ExtractSaveScene(pyblish.api.ContextPlugin): """Save scene before extraction.""" order = openpype.api.Extractor.order - 0.48 label = "Extract Save Scene" hosts = ["aftereffects"] - families = ["workfile"] - def process(self, instance): + def process(self, context): stub = get_stub() stub.save() diff --git a/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml b/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml index 36fa90456e..0591020ed3 100644 --- a/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml +++ b/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml @@ -12,6 +12,8 @@ One of the settings in a scene doesn't match to asset settings in database. ### How to repair? Change values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there. + + In the scene it is right mouse click on published composition > `Composition Settings`. ### __Detailed Info__ (optional) diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py new file mode 100644 index 0000000000..03ec184524 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -0,0 +1,54 @@ +import json +import pyblish.api +from openpype.hosts.aftereffects.api import list_instances + + +class PreCollectRender(pyblish.api.ContextPlugin): + """ + Checks if render instance is of old type, adds to families to both + existing collectors work same way. + + Could be removed in the future when no one uses old publish. + """ + + label = "PreCollect Render" + order = pyblish.api.CollectorOrder + 0.400 + hosts = ["aftereffects"] + + family_remapping = { + "render": ("render.farm", "farm"), # (family, label) + "renderLocal": ("render.local", "local") + } + + def process(self, context): + if context.data.get("newPublishing"): + self.log.debug("Not applicable for New Publisher, skip") + return + + for inst in list_instances(): + if inst.get("creator_attributes"): + raise ValueError("Instance created in New publisher, " + "cannot be published in Pyblish.\n" + "Please publish in New Publisher " + "or recreate instances with legacy Creators") + + if inst["family"] not in self.family_remapping.keys(): + continue + + if not inst["members"]: + raise ValueError("Couldn't find id, unable to publish. " + + "Please recreate instance.") + + instance = context.create_instance(inst["subset"]) + inst["families"] = [self.family_remapping[inst["family"]][0]] + instance.data.update(inst) + + self._debug_log(instance) + + def _debug_log(self, instance): + def _default_json(value): + return str(value) + + self.log.info( + json.dumps(instance.data, indent=4, default=_default_json) + ) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py index 37cecfbcc4..47671e104a 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -28,7 +28,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): data = stub.read(instance[0]) data["asset"] = api.Session["AVALON_ASSET"] - stub.imprint(instance[0], data) + stub.imprint(instance[0].instance_id, data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 273ccd295e..14e224fdc2 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -5,11 +5,15 @@ import re import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin +) from openpype.hosts.aftereffects.api import get_asset_settings -class ValidateSceneSettings(pyblish.api.InstancePlugin): +class ValidateSceneSettings(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): """ Ensures that Composition Settings (right mouse on comp) are same as in FTrack on task. @@ -59,15 +63,20 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + # Skip the instance if is not active by data on the instance + if not self.is_active(instance.data): + return + expected_settings = get_asset_settings() self.log.info("config from DB::{}".format(expected_settings)) - if any(re.search(pattern, os.getenv('AVALON_TASK')) + task_name = instance.data["anatomyData"]["task"]["name"] + if any(re.search(pattern, task_name) for pattern in self.skip_resolution_check): expected_settings.pop("resolutionWidth") expected_settings.pop("resolutionHeight") - if any(re.search(pattern, os.getenv('AVALON_TASK')) + if any(re.search(pattern, task_name) for pattern in self.skip_timelines_check): expected_settings.pop('fps', None) expected_settings.pop('frameStart', None) @@ -87,10 +96,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): duration = instance.data.get("frameEndHandle") - \ instance.data.get("frameStartHandle") + 1 - self.log.debug("filtered config::{}".format(expected_settings)) + self.log.debug("validated items::{}".format(expected_settings)) current_settings = { "fps": fps, + "frameStart": instance.data.get("frameStart"), + "frameEnd": instance.data.get("frameEnd"), + "handleStart": instance.data.get("handleStart"), + "handleEnd": instance.data.get("handleEnd"), "frameStartHandle": instance.data.get("frameStartHandle"), "frameEndHandle": instance.data.get("frameEndHandle"), "resolutionWidth": instance.data.get("resolutionWidth"), @@ -103,24 +116,22 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): invalid_keys = set() for key, value in expected_settings.items(): if value != current_settings[key]: - invalid_settings.append( - "{} expected: {} found: {}".format(key, value, - current_settings[key]) - ) + msg = "'{}' expected: '{}' found: '{}'".format( + key, value, current_settings[key]) + + if key == "duration" and expected_settings.get("handleStart"): + msg += "Handles included in calculation. Remove " \ + "handles in DB or extend frame range in " \ + "Composition Setting." + + invalid_settings.append(msg) invalid_keys.add(key) - if ((expected_settings.get("handleStart") - or expected_settings.get("handleEnd")) - and invalid_settings): - msg = "Handles included in calculation. Remove handles in DB " +\ - "or extend frame range in Composition Setting." - invalid_settings[-1]["reason"] = msg - - msg = "Found invalid settings:\n{}".format( - "\n".join(invalid_settings) - ) - if invalid_settings: + msg = "Found invalid settings:\n{}".format( + "\n".join(invalid_settings) + ) + invalid_keys_str = ",".join(invalid_keys) break_str = "
" invalid_setting_str = "Found invalid settings:
{}".\ diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py index 7c768e280c..cce161b51c 100644 --- a/openpype/lib/abstract_collect_render.py +++ b/openpype/lib/abstract_collect_render.py @@ -30,6 +30,7 @@ class RenderInstance(object): source = attr.ib() # path to source scene file label = attr.ib() # label to show in GUI subset = attr.ib() # subset name + task = attr.ib() # task name asset = attr.ib() # asset name (AVALON_ASSET) attachTo = attr.ib() # subset name to attach render to setMembers = attr.ib() # list of nodes/members producing render output @@ -138,7 +139,9 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): try: if "workfile" in instance.data["families"]: instance.data["publish"] = True - if "renderFarm" in instance.data["families"]: + # TODO merge renderFarm and render.farm + if ("renderFarm" in instance.data["families"] or + "render.farm" in instance.data["families"]): instance.data["remove"] = True except KeyError: # be tolerant if 'families' is missing. diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 308be6da64..b7aaa33d49 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -8,7 +8,6 @@ from .create import ( Creator, AutoCreator, CreatedInstance, - CreatorError, LegacyCreator, @@ -97,6 +96,7 @@ __all__ = ( "Creator", "AutoCreator", "CreatedInstance", + "CreatorError", "CreatorError", diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 16e3f669c3..09584ab37c 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -25,7 +25,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): # Update global data to context context.data.update(create_context.context_data_to_store()) - + context.data["newPublishing"] = True # Update context data for key in ("AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK"): value = create_context.dbcon.Session.get(key) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 5dcbb8fabd..1e53f2bcfa 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -113,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip"] + exclude_families = ["clip", "render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" @@ -131,11 +131,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_grouping_profiles = None def process(self, instance): - self.integrated_file_sizes = {} - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: - return + for ef in self.exclude_families: + if ( + instance.data["family"] == ef or + ef in instance.data["families"]): + self.log.debug("Excluded family '{}' in '{}' or {}".format( + ef, instance.data["family"], instance.data["families"])) + return + self.integrated_file_sizes = {} try: self.register(instance) self.log.info("Integrated Asset in to the database ...") @@ -228,7 +232,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Ensure at least one file is set up for transfer in staging dir. repres = instance.data.get("representations") - assert repres, "Instance has no files to transfer" + repres = instance.data.get("representations") + msg = "Instance {} has no files to transfer".format( + instance.data["family"]) + assert repres, msg assert isinstance(repres, (list, tuple)), ( "Instance 'files' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index 1c86509155..7a3f49452e 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -185,8 +185,8 @@ "linux": [] }, "renderSpace": "ACEScg", - "viewName": "ACES 1.0 SDR-video", - "displayName": "sRGB" + "displayName": "sRGB", + "viewName": "ACES 1.0 SDR-video" }, "colorManagementPreference": { "configFilePath": { diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 58659d5d41..7317a3da1c 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -315,6 +315,7 @@ "workfile" ], "hosts": [ + "aftereffects", "tvpaint" ], "task_types": [],