diff --git a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py index aeff1208294..b7a056e3835 100644 --- a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py +++ b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py @@ -7,6 +7,9 @@ from collections import namedtuple +import logging +log = logging.getLogger(__name__) + class AfterEffectsServerStub(): """ Stub for calling function on client (Photoshop js) side. @@ -44,10 +47,15 @@ def read(self, layer, layers_meta=None): return layers_meta.get(str(layer.id)) def get_metadata(self): + layers_data = {} res = self.websocketserver.call(self.client.call ('AfterEffects.get_metadata') ) - return self._to_records(res) + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + raise ValueError("Unparsable metadata {}".format(res)) + return layers_data or {} def imprint(self, layer, data, all_layers=None, layers_meta=None): """ @@ -65,6 +73,7 @@ def imprint(self, layer, data, all_layers=None, layers_meta=None): """ if not layers_meta: layers_meta = self.get_metadata() + # json.dumps writes integer values in a dictionary to string, so # anticipating it here. if str(layer.id) in layers_meta and layers_meta[str(layer.id)]: @@ -74,13 +83,11 @@ def imprint(self, layer, data, all_layers=None, layers_meta=None): layers_meta.pop(str(layer.id)) else: layers_meta[str(layer.id)] = data - # Ensure only valid ids are stored. if not all_layers: all_layers = self.get_items(False) - item_ids = [item.id for item in all_layers] + item_ids = [int(item.id) for item in all_layers] cleaned_data = {} - for id in layers_meta: if int(id) in item_ids: cleaned_data[id] = layers_meta[id] @@ -118,6 +125,28 @@ def get_items(self, layers=True): ) return self._to_records(res) + def import_file(self, path, item_name): + res = self.websocketserver.call(self.client.call( + 'AfterEffects.import_file', + path=path, + item_name=item_name) + ) + return self._to_records(res).pop() + + def replace_item(self, item, path, item_name): + """ item is currently comp, might be layer, investigate TODO """ + 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 """ + self.websocketserver.call(self.client.call + ('AfterEffects.delete_item', + item_id=item.id + )) + def is_saved(self): # TODO return True @@ -153,12 +182,16 @@ def _to_records(self, res): Returns: res(string): - json representation """ + if not res: + return [] + try: layers_data = json.loads(res) except json.decoder.JSONDecodeError: raise ValueError("Received broken JSON {}".format(res)) if not layers_data: return [] + ret = [] # convert to namedtuple to use dot donation if isinstance(layers_data, dict): # TODO refactore diff --git a/pype/plugins/aftereffects/load/load_image.py b/pype/plugins/aftereffects/load/load_image.py new file mode 100644 index 00000000000..245f66c728a --- /dev/null +++ b/pype/plugins/aftereffects/load/load_image.py @@ -0,0 +1,72 @@ +from avalon import api, aftereffects +from pype.plugins import lib +import re + +stub = aftereffects.stub() + + +class ImageLoader(api.Loader): + """Load images + + Stores the imported asset in a container named after the asset. + """ + + families = ["image"] + 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) + #with photoshop.maintained_selection(): + comp = stub.import_file(self.fname, layer_name) + + self[:] = [comp] + namespace = namespace or layer_name + + return aftereffects.containerise( + name, + namespace, + comp, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + """ Switch asset or change version """ + layer = container.pop("layer") + + context = representation.get("context", {}) + + namespace_from_container = re.sub(r'_\d{3}$', '', + container["namespace"]) + 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), + context["asset"], + context["subset"]) + else: # switching version - keep same name + layer_name = container["namespace"] + path = api.get_representation_path(representation) + # with aftereffects.maintained_selection(): # TODO + stub.replace_item(layer, path, layer_name) + stub.imprint( + layer, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + """ + Removes element from scene: deletes layer + removes from Headline + Args: + container (dict): container to be removed - used to get layer_id + """ + layer = container.pop("layer") + stub.imprint(layer, {}) + stub.delete_layer(layer.id) + + def switch(self, container, representation): + self.update(container, representation) + + diff --git a/pype/plugins/lib.py b/pype/plugins/lib.py new file mode 100644 index 00000000000..71a4d1e7d57 --- /dev/null +++ b/pype/plugins/lib.py @@ -0,0 +1,25 @@ +import re + +def get_unique_layer_name(layers, asset_name, subset_name): + """ + Gets all layer names and if 'name' is present in them, increases + suffix by 1 (eg. creates unique layer name - for Loader) + Args: + layers (list): of namedtuples, expects 'name' field present + asset_name (string): in format asset_subset (Hero) + subset_name (string): (LOD) + + Returns: + (string): name_00X (without version) + """ + name = "{}_{}".format(asset_name, subset_name) + names = {} + for layer in layers: + layer_name = re.sub(r'_\d{3}$', '', layer.name) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1)