From c76051ea0483e62d879ce7f3a3d2e82115af6f3b Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 5 Nov 2024 14:55:42 -0500 Subject: [PATCH] Implement workfile instance serializer. --- client/ayon_flame/api/__init__.py | 4 +- client/ayon_flame/api/plugin.py | 47 ++++++- .../plugins/create/create_workfile.py | 120 +++++++++++++++--- 3 files changed, 153 insertions(+), 18 deletions(-) diff --git a/client/ayon_flame/api/__init__.py b/client/ayon_flame/api/__init__.py index bc34c90..452203a 100644 --- a/client/ayon_flame/api/__init__.py +++ b/client/ayon_flame/api/__init__.py @@ -59,7 +59,9 @@ Creator, PublishableClip, ClipLoader, - OpenClipSolver + OpenClipSolver, + FlameCreator, + HiddenFlameCreator, ) from .workio import ( open_file, diff --git a/client/ayon_flame/api/plugin.py b/client/ayon_flame/api/plugin.py index fa77cee..b91952b 100644 --- a/client/ayon_flame/api/plugin.py +++ b/client/ayon_flame/api/plugin.py @@ -9,7 +9,7 @@ from ayon_core import style from ayon_core.lib import Logger, StringTemplate -from ayon_core.pipeline import LegacyCreator, LoaderPlugin +from ayon_core.pipeline import LegacyCreator, LoaderPlugin, HiddenCreator, Creator from ayon_core.pipeline.colorspace import get_remapped_colorspace_to_native from ayon_core.settings import get_current_project_settings @@ -325,6 +325,51 @@ def create_widget(self, *args, **kwargs): return widget.get_results_back() +class HiddenFlameCreator(HiddenCreator): + """HiddenCreator class wrapper + """ + settings_category = "flame" + + def collect_instances(self): + pass + + def update_instances(self, update_list): + pass + + def remove_instances(self, instances): + pass + + +class FlameCreator(Creator): + """Creator class wrapper + """ + settings_category = "flame" + + def __init__(self, *args, **kwargs): + super(Creator, self).__init__(*args, **kwargs) + self.presets = get_current_project_settings()[ + "flame"]["create"].get(self.__class__.__name__, {}) + + def create(self, product_name, instance_data, pre_create_data): + """Prepare data for new instance creation. + + Args: + product_name(str): Product name of created instance. + instance_data(dict): Base data for instance. + pre_create_data(dict): Data based on pre creation attributes. + Those may affect how creator works. + """ + # adding basic current context resolve objects + self.project = flib.get_current_project() + self.sequence = flib.get_current_sequence(flib.CTX.selection) + + selected = pre_create_data.get("use_selection", False) + self.selected = flib.get_sequence_segments( + self.sequence, + selected=selected + ) + + class PublishableClip: """ Convert a segment to publishable instance diff --git a/client/ayon_flame/plugins/create/create_workfile.py b/client/ayon_flame/plugins/create/create_workfile.py index d3cf286..e46fb67 100644 --- a/client/ayon_flame/plugins/create/create_workfile.py +++ b/client/ayon_flame/plugins/create/create_workfile.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" -import ayon_api +import json +from xml.etree import ElementTree as ET + from ayon_core.pipeline import ( AutoCreator, CreatedInstance, ) +from adsk.libwiretapPythonClientAPI import ( + WireTapClient, + WireTapServerHandle, + WireTapNodeHandle, + WireTapStr +) + +import ayon_flame.api as flapi + class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" @@ -14,22 +25,77 @@ class CreateWorkfile(AutoCreator): identifier = "io.ayon.creators.flame.workfile" label = "Workfile" product_type = "workfile" - + icon = "fa5.file" default_variant = "Main" - def collect_instances(self): + # https://forums.autodesk.com/t5/flame-forum/store-persistent-variable-with-flame-project/td-p/9437717 + _METADATA_KEY = "Nickname" + + @classmethod + def _get_project_metadata_handle(cls): + """ Initialize project metadata setup. + + Returns: + object. Flame wiretap handle for current project + """ + current_project = flapi.get_current_project() + + wiretap_client = WireTapClient() + wiretap_client.init() + + server = WireTapServerHandle("localhost:IFFFS") + project_node_handle = WireTapNodeHandle(server, "/projects/{current_project.name}") + return project_node_handle + + @classmethod + def _get_project_metadata(cls): + """ Returns the metadata stored at current project. + + Returns: + xml.etree.ElementTree. The project metadata data. + """ + project_node_handle = cls._get_project_metadata_handle() + metadata = WireTapStr() + project_node_handle.getMetaData("XML", "", 1, metadata) + return ET.fromstring(metadata.c_str()) + + @classmethod + def _dump_instance_data(cls, data): + """ Dump instance data into AyonData project tag. + + Args: + data (dict): The data to push to the project tag. + """ + metadata = cls._get_project_metadata() + nickname_entry, = metadata.findall(cls._METADATA_KEY) + nickname_entry.text = json.dumps(data) + @classmethod + def _load_instance_data(cls): + """ Returns the data stored in AyonData project tag if any. + + Returns: + dict. The metadata instance data. + """ + metadata = cls._get_project_metadata() + nickname_entry, = metadata.findall(cls._METADATA_KEY) + return json.loads(nickname_entry.text) if nickname_entry.text else {} + + def _create_new_instance(self): + """Create a new workfile instance. + + Returns: + dict. The data of the instance to be created. + """ variant = self.default_variant project_name = self.create_context.get_current_project_name() folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path) - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name - ) + folder_entity = self.create_context.get_current_folder_entity() + task_entity = self.create_context.get_current_task_entity() + product_name = self.get_product_name( project_name, folder_entity, @@ -58,13 +124,35 @@ def collect_instances(self): self._add_instance_to_context(current_instance) def create(self, options=None): - # no need to create if it is created - # in `collect_instances` - pass + """Auto-create an instance by default.""" + instance_data = self._load_instance_data() + if instance_data: + return + + self.log.info("Auto-creating workfile instance...") + data = self._create_new_instance() + current_instance = CreatedInstance( + self.product_type, data["productName"], data, self) + self._add_instance_to_context(current_instance) + + def collect_instances(self): + """Collect from timeline marker or create a new one.""" + data = self._load_instance_data() + if not data: + return + + instance = CreatedInstance( + self.product_type, data["productName"], data, self + ) + self._add_instance_to_context(instance) def update_instances(self, update_list): - # TODO: Implement - # This needs to be implemented to allow persisting any instance - # data on resets. We'll need to decide where to store workfile - # instance data reliably. Likely metadata on the *current project*? - pass + """Store changes in project metadata so they can be recollected. + + Args: + update_list(List[UpdateData]): Gets list of tuples. Each item + contain changed instance and its changes. + """ + for created_inst, _ in update_list: + data = created_inst.data_to_store() + self._dump_instance_data(data)