diff --git a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py index b7a056e3835..2168a9ee558 100644 --- a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py +++ b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py @@ -10,6 +10,7 @@ import logging log = logging.getLogger(__name__) + class AfterEffectsServerStub(): """ Stub for calling function on client (Photoshop js) side. @@ -47,7 +48,11 @@ def read(self, layer, layers_meta=None): return layers_meta.get(str(layer.id)) def get_metadata(self): - layers_data = {} + """ + Get stored JSON with metadata from AE.Metadata.Label field + Returns: + (dict) + """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_metadata') ) @@ -85,7 +90,10 @@ def imprint(self, layer, data, all_layers=None, layers_meta=None): layers_meta[str(layer.id)] = data # Ensure only valid ids are stored. if not all_layers: - all_layers = self.get_items(False) + # loaders create FootagetItem now + all_layers = self.get_items(comps=True, + folders=False, + footages=True) item_ids = [int(item.id) for item in all_layers] cleaned_data = {} for id in layers_meta: @@ -103,8 +111,8 @@ def get_active_document_full_name(self): Returns just a name of active document via ws call Returns(string): file name """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_active_document_full_name')) + res = self.websocketserver.call(self.client.call( + 'AfterEffects.get_active_document_full_name')) return res @@ -113,35 +121,96 @@ def get_active_document_name(self): Returns just a name of active document via ws call Returns(string): file name """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_active_document_name')) + res = self.websocketserver.call(self.client.call( + 'AfterEffects.get_active_document_name')) return res - def get_items(self, layers=True): + def get_items(self, comps, folders=False, footages=False): + """ + Get all items from Project panel according to arguments. + There are mutliple different types: + CompItem (could have multiple layers - source for Creator) + FolderItem (collection type, currently not used + FootageItem (imported file - created by Loader) + Args: + comps (bool): return CompItems + folders (bool): return FolderItem + footages (bool: return FootageItem + + Returns: + (list) of namedtuples + """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_items', - layers=layers) + comps=comps, + folders=folders, + footages=footages) ) return self._to_records(res) - def import_file(self, path, item_name): + def get_selected_items(self, comps, folders=False, footages=False): + """ + Same as get_items but using selected items only + Args: + comps (bool): return CompItems + folders (bool): return FolderItem + footages (bool: return FootageItem + + Returns: + (list) of namedtuples + + """ + res = self.websocketserver.call(self.client.call + ('AfterEffects.get_selected_items', + comps=comps, + folders=folders, + footages=footages) + ) + return self._to_records(res) + + def import_file(self, path, item_name, import_options=None): + """ + Imports file as a FootageItem. Used in Loader + Args: + path (string): absolute path for asset file + item_name (string): label for created FootageItem + import_options (dict): different files (img vs psd) need different + config + + """ res = self.websocketserver.call(self.client.call( 'AfterEffects.import_file', path=path, - item_name=item_name) + item_name=item_name, + import_options=import_options) ) - return self._to_records(res).pop() + records = self._to_records(res) + if records: + return records.pop() + + log.debug("Couldn't import {} file".format(path)) def replace_item(self, item, path, item_name): - """ item is currently comp, might be layer, investigate TODO """ + """ Replace FootageItem with new file + + Args: + item (dict): + path (string):absolute path + item_name (string): label on item in Project list + + """ self.websocketserver.call(self.client.call ('AfterEffects.replace_item', item_id=item.id, path=path, item_name=item_name)) def delete_item(self, item): - """ item is currently comp, might be layer, investigate TODO """ + """ Deletes FootageItem with new file + Args: + item (dict): + + """ self.websocketserver.call(self.client.call ('AfterEffects.delete_item', item_id=item.id @@ -151,6 +220,20 @@ def is_saved(self): # TODO return True + def set_label_color(self, item_id, color_idx): + """ + Used for highlight additional information in Project panel. + Green color is loaded asset, blue is created asset + Args: + item_id (int): + color_idx (int): 0-16 Label colors from AE Project view + """ + self.websocketserver.call(self.client.call + ('AfterEffects.set_label_color', + item_id=item_id, + color_idx=color_idx + )) + def save(self): """ Saves active document diff --git a/pype/plugins/aftereffects/create/create_render.py b/pype/plugins/aftereffects/create/create_render.py new file mode 100644 index 00000000000..f38a4766e46 --- /dev/null +++ b/pype/plugins/aftereffects/create/create_render.py @@ -0,0 +1,52 @@ +from avalon import api +from avalon.vendor import Qt +from avalon import aftereffects + +import logging + +log = logging.getLogger(__name__) + + +class CreateRender(api.Creator): + """Render folder for publish.""" + + name = "renderDefault" + label = "Render" + family = "render" + + def process(self): + # Photoshop can have multiple LayerSets with the same name, which does + # not work with Avalon. + txt = "Instance with name \"{}\" already exists.".format(self.name) + stub = aftereffects.stub() # only after After Effects is up + for layer in stub.get_items(comps=True, + folders=False, + footages=False): + if self.name.lower() == layer.name.lower(): + msg = Qt.QtWidgets.QMessageBox() + msg.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg.setText(txt) + msg.exec_() + return False + log.debug("options:: {}".format(self.options)) + print("options:: {}".format(self.options)) + if (self.options or {}).get("useSelection"): + log.debug("useSelection") + print("useSelection") + items = stub.get_selected_items(comps=True, + folders=False, + footages=False) + else: + items = stub.get_items(comps=True, + folders=False, + footages=False) + log.debug("items:: {}".format(items)) + print("items:: {}".format(items)) + if not items: + raise ValueError("Nothing to create. Select composition " + + "if 'useSelection' or create at least " + + "one composition.") + + for item in items: + stub.imprint(item, self.data) + stub.set_label_color(item.id, 14) # Cyan options 0 - 16 diff --git a/pype/plugins/aftereffects/load/load_image.py b/pype/plugins/aftereffects/load/load_resource.py similarity index 59% rename from pype/plugins/aftereffects/load/load_image.py rename to pype/plugins/aftereffects/load/load_resource.py index 245f66c728a..73b0a4aae35 100644 --- a/pype/plugins/aftereffects/load/load_image.py +++ b/pype/plugins/aftereffects/load/load_resource.py @@ -5,25 +5,55 @@ stub = aftereffects.stub() -class ImageLoader(api.Loader): +class ResourceLoader(api.Loader): """Load images Stores the imported asset in a container named after the asset. """ - families = ["image"] + families = ["image", + "render2d", + "source", + "plate", + "render", + "prerender", + "review", + "preview", + "workfile"] representations = ["*"] def load(self, context, name=None, namespace=None, data=None): - print("Load:::") - layer_name = lib.get_unique_layer_name(stub.get_items(False), - context["asset"]["name"], - name) + comp_name = lib.get_unique_layer_name(stub.get_items(comps=True), + context["asset"]["name"], + name) + + import_options = {} + + file = self.fname + + repr_cont = context["representation"]["context"] + if "#" not in file: + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace(frame, "#" * padding) + import_options['sequence'] = True + + if not file: + repr_id = context["representation"]["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + if '.psd' in file: + import_options['ImportAsType'] = 'ImportAsType.COMP' + #with photoshop.maintained_selection(): - comp = stub.import_file(self.fname, layer_name) + comp = stub.import_file(self.fname, comp_name, import_options) self[:] = [comp] - namespace = namespace or layer_name + namespace = namespace or comp_name return aftereffects.containerise( name, @@ -44,7 +74,7 @@ def update(self, container, representation): layer_name = "{}_{}".format(context["asset"], context["subset"]) # switching assets if namespace_from_container != layer_name: - layer_name = lib.get_unique_layer_name(stub.get_items(False), + layer_name = lib.get_unique_layer_name(stub.get_items(comps=True), context["asset"], context["subset"]) else: # switching version - keep same name @@ -64,7 +94,7 @@ def remove(self, container): """ layer = container.pop("layer") stub.imprint(layer, {}) - stub.delete_layer(layer.id) + stub.delete_item(layer.id) def switch(self, container, representation): self.update(container, representation) diff --git a/pype/resources/app_icons/aftereffects.png b/pype/resources/app_icons/aftereffects.png new file mode 100644 index 00000000000..56754e2be23 Binary files /dev/null and b/pype/resources/app_icons/aftereffects.png differ